diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1ccb286..445d2e6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,8 @@ "lindacong.vscode-book-reader", "4ops.terraform", "fabiospampinato.vscode-todo-plus", - "jgclark.vscode-todo-highlight" + "jgclark.vscode-todo-highlight", + "corentinartaud.pdfpreview" ] } } diff --git a/etl/condition_report_etl.py b/etl/condition_report_etl.py new file mode 100644 index 0000000..5284172 --- /dev/null +++ b/etl/condition_report_etl.py @@ -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 \ No newline at end of file diff --git a/etl/dimitra_hubspot_notes_gather.py b/etl/dimitra_hubspot_notes_gather.py index 3abacfd..ec364e0 100644 --- a/etl/dimitra_hubspot_notes_gather.py +++ b/etl/dimitra_hubspot_notes_gather.py @@ -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"], diff --git a/etl/files/osmosis_condition_report.pdf b/etl/files/osmosis_condition_report.pdf new file mode 100644 index 0000000..cd33476 Binary files /dev/null and b/etl/files/osmosis_condition_report.pdf differ diff --git a/etl/load/topLevel.py b/etl/load/topLevel.py index 610c2e9..a2e2326 100644 --- a/etl/load/topLevel.py +++ b/etl/load/topLevel.py @@ -34,4 +34,5 @@ class Documents(BaseModel, table=True): target_table: str target_id: uuid.UUID -Documents.update_forward_refs() \ No newline at end of file +Documents.update_forward_refs() + diff --git a/etl/osmosis_complaince_address_to_files.py b/etl/osmosis_complaince_address_to_files.py index 1448375..0ab6ed9 100644 --- a/etl/osmosis_complaince_address_to_files.py +++ b/etl/osmosis_complaince_address_to_files.py @@ -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 \ No newline at end of file diff --git a/etl/pdfReader/pdfReaderToText.py b/etl/pdfReader/pdfReaderToText.py index 5ea7b42..34e37ad 100644 --- a/etl/pdfReader/pdfReaderToText.py +++ b/etl/pdfReader/pdfReaderToText.py @@ -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) \ No newline at end of file diff --git a/etl/pdfReader/reportType.py b/etl/pdfReader/reportType.py index c77ad9f..a94847a 100644 --- a/etl/pdfReader/reportType.py +++ b/etl/pdfReader/reportType.py @@ -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" \ No newline at end of file + OVERWRITING_U_VALUE_DECLARATION_FORM = "overwriting_u_value_declaration_form" + OSMOSIS_CONDITION_PAS_2035_REPORT = "osmosis_condition_pas_2035_report" diff --git a/etl/pdfReader/sitenotes.py b/etl/pdfReader/sitenotes.py index 7f36384..1c9f532 100644 --- a/etl/pdfReader/sitenotes.py +++ b/etl/pdfReader/sitenotes.py @@ -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], diff --git a/etl/scraper/scraper.py b/etl/scraper/scraper.py index 70a9cb5..550e05d 100644 --- a/etl/scraper/scraper.py +++ b/etl/scraper/scraper.py @@ -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()) diff --git a/etl/surveyedData/surveryedData.py b/etl/surveyedData/surveryedData.py index 427a9a4..8525278 100644 --- a/etl/surveyedData/surveryedData.py +++ b/etl/surveyedData/surveryedData.py @@ -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() diff --git a/etl/transform/conditionReportTypes.py b/etl/transform/conditionReportTypes.py new file mode 100644 index 0000000..f51e575 --- /dev/null +++ b/etl/transform/conditionReportTypes.py @@ -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 \ No newline at end of file diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..e69de29 diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..ce988a6 --- /dev/null +++ b/frontend/.env.development @@ -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 \ No newline at end of file diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..f05a186 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1 @@ +DATABASE_URL=postgresql://postgres:makingwarmhomes@terraform-20250331175522503500000002.cdgzupxvdyp0.eu-west-2.rds.amazonaws.com:5432/surveyDB \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 5ef6a52..1b7a73a 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +# .env* # vercel .vercel diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 974edff..b93eb51 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,11 @@ "name": "frontend", "version": "0.1.0", "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" }, @@ -16,8 +20,10 @@ "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", "@types/node": "^20", + "@types/pg": "^8.15.2", "@types/react": "^19", "@types/react-dom": "^19", + "drizzle-kit": "^0.31.1", "eslint": "^9", "eslint-config-next": "15.3.2", "tailwindcss": "^4", @@ -51,6 +57,13 @@ "node": ">=6.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -84,6 +97,867 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1299,12 +2173,86 @@ "version": "20.17.50", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/pg": { + "version": "8.15.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.2.tgz", + "integrity": "sha512-+BKxo5mM6+/A1soSHBI7ufUglqYXntChLDyTbvcAn1Lawi9J7J9Ok3jt6w7I0+T/UDJ4CyhHk66+GZbwmkYxSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", + "integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/@types/react": { "version": "19.1.6", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", @@ -2127,6 +3075,13 @@ "node": ">=8" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2470,6 +3425,159 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/drizzle-kit": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.1.tgz", + "integrity": "sha512-PUjYKWtzOzPtdtQlTHQG3qfv4Y0XT8+Eas6UbxCmxTj7qgMf+39dDujf1BP1I+qqZtw9uzwTh8jYtkMuCq+B0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.2", + "esbuild-register": "^3.5.0" + }, + "bin": { + "drizzle-kit": "bin.cjs" + } + }, + "node_modules/drizzle-orm": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.0.tgz", + "integrity": "sha512-/8wYepe887Gp1eCVILKitNegclGLPUMdK4oDDyDftKJOeHoCylY+XM++chMJloy9oKTceWOALiQMl2aAuFuPTw==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2680,6 +3788,60 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4772,6 +5934,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4880,6 +6049,105 @@ "dev": true, "license": "MIT" }, + "node_modules/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-7SKfdvP8CTNXjMUzfcVTaI+TDzBEeaUnVwiVGZQD1Hh33Kpev7liQba9uLd4CfN8r9mCVsD0JIpq03+Unpz+kg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.0", + "pg-pool": "^3.10.0", + "pg-protocol": "^1.10.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.5" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.5.tgz", + "integrity": "sha512-OOX22Vt0vOSRrdoUPKJ8Wi2OpE/o/h9T8X1s4qSkCedbNah9ei2W2765be8iMVxQUsvgT7zIAT2eIa9fs5+vtg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.0.tgz", + "integrity": "sha512-P2DEBKuvh5RClafLngkAuGe9OUlFV7ebu8w1kmaaOgPcpJd1RIFh7otETfI6hAR8YupOLFTY7nuvvIn7PLciUQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "devOptional": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.0.tgz", + "integrity": "sha512-DzZ26On4sQ0KmqnO34muPcmKbhrjmyiO4lCCR0VwEd7MjmiKf5NTg/6+apUEu0NF7ESa37CGzFxH513CoUmWnA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.0.tgz", + "integrity": "sha512-IpdytjudNuLv8nhlHs/UrVBhU0e78J0oIS/0AVdTbWxSOkFUVdsHC/NrorO6nXsQNDTT1kzDSOMJubBQviX18Q==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4938,6 +6206,65 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", + "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "devOptional": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5413,6 +6740,16 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5422,6 +6759,26 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -5875,7 +7232,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6036,6 +7393,15 @@ "node": ">=0.10.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index c4abd1f..2bdd217 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" } } diff --git a/frontend/src/app/db/db.ts b/frontend/src/app/db/db.ts new file mode 100644 index 0000000..f9b6771 --- /dev/null +++ b/frontend/src/app/db/db.ts @@ -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); diff --git a/frontend/src/app/db/schema/buildings.ts b/frontend/src/app/db/schema/buildings.ts new file mode 100644 index 0000000..d0aedb2 --- /dev/null +++ b/frontend/src/app/db/schema/buildings.ts @@ -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(), +}); \ No newline at end of file diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d6785dd..6336f8a 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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 ( -
| ID | +Address | +Postcode | +UPRN | +Landlord ID | +Domna ID | +
|---|---|---|---|---|---|
| {b.id} | +{b.address} | +{b.postcode} | +{b.uprn} | +{b.landlordId} | +{b.domnaId} | +