From 39ccf1987cb2a34931460e11a3e54aa43387f37e Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:16:11 +0000 Subject: [PATCH 1/9] created Object to hold usageData, refactored Inputs function to eliminate redundancies Co-authored-by: Derek McIntire --- heat-stack/app/routes/_heat+/single.tsx | 80 ++++++++++++++++++------- 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index ac3d2b47..eaee0986 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -496,10 +496,44 @@ export default function Inputs() { // const location = useLocation(); // console.log(`location:`, location); // `.state` is `null` const lastResult = useActionData() - const parsedLastResult = hasDataProperty(lastResult) - ? JSON.parse(lastResult.data, reviver) as Map: undefined; + console.log('lastResult', lastResult) + + let show_usage_data = lastResult !== undefined; + + //////////////////////// + // TODO: + // - use the UsageDataSchema type here? + // - use processed_energy_bills in Checkbox behavior + // + let currentUsageData = { + heat_load_output: undefined, + balance_point_graph: undefined, + processed_energy_bills: undefined, + } + + if (show_usage_data && hasDataProperty(lastResult)) { + try { + // Parse the JSON string from lastResult.data + const parsedLastResult = JSON.parse(lastResult.data, reviver) as Map; + console.log('parsedLastResult', parsedLastResult) + + // TODO: Parsing without reviver to get processed_energy_bills with Objects instead of maps + // Figure out how to use parsedLastResult instead + const parsedLastResultObject = JSON.parse(lastResult.data) + + currentUsageData.heat_load_output = Object.fromEntries(parsedLastResult?.get('heat_load_output')); + currentUsageData.balance_point_graph = Object.fromEntries(parsedLastResult?.get('balance_point_graph')); + + currentUsageData.processed_energy_bills = replacedMapToObject(parsedLastResultObject).processed_energy_bills + + } catch (error) { + // console.error('Error parsing lastResult data:', error); + } + } + + console.log('currentUsageData', currentUsageData) + - const heatLoadSummaryOutput = parsedLastResult ? Object.fromEntries(parsedLastResult?.get('heat_load_output')) : undefined; /* @ts-ignore */ // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) @@ -543,26 +577,28 @@ export default function Inputs() { function hasDataProperty(result: ActionResult): result is { data: string } { return result !== undefined && 'data' in result && typeof (result as any).data === 'string'; } - - let usage_data = null; - let show_usage_data = lastResult !== undefined; - console.log('lastResult', lastResult) - - // Ensure we handle the result properly - if (show_usage_data && lastResult && hasDataProperty(lastResult)) { - try { - // Parse the JSON string from lastResult.data - const parsedData = JSON.parse(lastResult.data); + //////////////////////// + // TO BE DELETED + // Old way of handing results - // Recursively transform any Maps in lastResult to objects - usage_data = replacedMapToObject(parsedData); // Get the relevant part of the transformed result - console.log('usage_data', usage_data) + // let usage_data = null; + + // // Ensure we handle the result properly + // if (show_usage_data && lastResult && hasDataProperty(lastResult)) { + // try { + // // Parse the JSON string from lastResult.data + // const parsedData = JSON.parse(lastResult.data); + + // // Recursively transform any Maps in lastResult to objects + // usage_data = replacedMapToObject(parsedData); // Get the relevant part of the transformed result + // console.log('usage_data', usage_data) - } catch (error) { - console.error('Error parsing lastResult data:', error); - } - } + // } catch (error) { + // console.error('Error parsing lastResult data:', error); + // } + // } + //////////////////////// type SchemaZodFromFormType = z.infer const [form, fields] = useForm({ @@ -600,11 +636,11 @@ export default function Inputs() { */} - + - {show_usage_data && } + {show_usage_data && } ) } From 6bf611ded9313d923d0c2cc63dd5c5f0e90a2a3b Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Fri, 31 Jan 2025 03:47:38 +0000 Subject: [PATCH 2/9] extracted rules-engine code to app/utils/rules-engine.ts Co-authored-by: Derek McIntire --- heat-stack/app/routes/_heat+/single.tsx | 131 +-------------------- heat-stack/app/utils/rules-engine.ts | 144 ++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 126 deletions(-) create mode 100644 heat-stack/app/utils/rules-engine.ts diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index eaee0986..5474fd9f 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -7,11 +7,15 @@ import { json, type ActionFunctionArgs } from '@remix-run/node' import { Form, redirect, useActionData, useLocation } from '@remix-run/react' import { parseMultipartFormData } from '@remix-run/server-runtime/dist/formData.js' import { createMemoryUploadHandler } from '@remix-run/server-runtime/dist/upload/memoryUploadHandler.js' -import * as pyodideModule from 'pyodide' import { type z } from 'zod' import { Button } from '#/app/components/ui/button.tsx' import { ErrorList } from '#app/components/ui/heat/CaseSummaryComponents/ErrorList.tsx' import GeocodeUtil from '#app/utils/GeocodeUtil.ts' +import { + executeGetAnalyticsFromFormJs, + executeParseGasBillPy, + executeRoundtripAnalyticsFromFormJs +} from '#app/utils/rules-engine.ts' import WeatherUtil from '#app/utils/WeatherUtil.ts' @@ -151,21 +155,6 @@ export async function action({ request, params }: ActionFunctionArgs) { const geocodeUtil = new GeocodeUtil() const weatherUtil = new WeatherUtil() - //////////////////////// - const getPyodide = async () => { - return await pyodideModule.loadPyodide({ - // This path is actually `public/pyodide-env`, but the browser knows where `public` is. Note that remix server needs `public/` - // TODO: figure out how to determine if we're in browser or remix server and use ternary. - indexURL: 'public/pyodide-env/', - }) - } - const runPythonScript = async () => { - const pyodide: any = await getPyodide() - return pyodide - } - // consider running https://github.com/codeforboston/home-energy-analysis-tool/blob/main/python/tests/test_rules_engine/test_engine.py - const pyodide: any = await runPythonScript() - ////////////////////// let {coordinates, state_id, county_id} = await geocodeUtil.getLL(address) let {x, y} = coordinates ?? {x: 0, y: 0}; @@ -192,52 +181,9 @@ export async function action({ request, params }: ActionFunctionArgs) { // console.log('parsedAndValidatedFormSchema', parsedAndValidatedFormSchema) - await pyodide.loadPackage( - 'public/pyodide-env/pydantic_core-2.14.5-cp311-cp311-emscripten_3_1_32_wasm32.whl', - ) - - /* NOTES for pydantic, typing-extensions, annotated_types: - pyodide should match pyodide-core somewhat. - typing-extensions needs specific version per https://github.com/pyodide/pyodide/issues/4234#issuecomment-1771735148 - try getting it from - - https://pypi.org/project/pydantic/#files - - https://pypi.org/project/typing-extensions/ - - https://pypi.org/project/annotated-types/#files - */ - await pyodide.loadPackage( - 'public/pyodide-env/pydantic-2.5.2-py3-none-any.whl', - ) - await pyodide.loadPackage( - 'public/pyodide-env/typing_extensions-4.8.0-py3-none-any.whl', - ) - await pyodide.loadPackage( - 'public/pyodide-env/annotated_types-0.5.0-py3-none-any.whl', - ) - - await pyodide.loadPackage( - 'public/pyodide-env/rules_engine-0.0.1-py3-none-any.whl', - ) // console.log("uploadedTextFile", uploadedTextFile) - /** - * Need to parse the gas bill first to determine the start and end dates of the bill - * so that we can request the weather for those dates. - */ - const executeParseGasBillPy = await pyodide.runPythonAsync(` - from rules_engine import parser - from rules_engine.pydantic_models import ( - FuelType, - HeatLoadInput, - TemperatureInput - ) - from rules_engine import engine - - def executeParse(csvDataJs): - naturalGasInputRecords = parser.parse_gas_bill(csvDataJs, parser.NaturalGasCompany.NATIONAL_GRID) - return naturalGasInputRecords.model_dump(mode="json") - executeParse - `) /** Example: * records: [ @@ -299,40 +245,7 @@ export async function action({ request, params }: ActionFunctionArgs) { /** Main form entrypoint */ - const executeGetAnalyticsFromFormJs = await pyodide.runPythonAsync(` - from rules_engine import parser - from rules_engine.pydantic_models import ( - FuelType, - HeatLoadInput, - TemperatureInput - ) - from rules_engine import engine, helpers - - def executeGetAnalyticsFromForm(summaryInputJs, temperatureInputJs, csvDataJs, state_id, county_id): - """ - second step: this will be the first time to draw the table - # two new geocode parameters may be needed for design temp: - # watch out for helpers.get_design_temp( addressMatches[0].geographies.counties[0]['STATE'] , addressMatches[0].geographies.counties[0]['COUNTY'] county_id) - # in addition to latitude and longitude from GeocodeUtil.ts object . - # pack the get_design_temp output into heat_load_input - """ - - summaryInputFromJs = summaryInputJs.as_object_map().values()._mapping - temperatureInputFromJs =temperatureInputJs.as_object_map().values()._mapping - - # We will just pass in this data - naturalGasInputRecords = parser.parse_gas_bill(csvDataJs, parser.NaturalGasCompany.NATIONAL_GRID) - - design_temp_looked_up = helpers.get_design_temp(state_id, county_id) - summaryInput = HeatLoadInput( **summaryInputFromJs, design_temperature=design_temp_looked_up) - - temperatureInput = TemperatureInput(**temperatureInputFromJs) - - outputs = engine.get_outputs_natural_gas(summaryInput, temperatureInput, naturalGasInputRecords) - return outputs.model_dump(mode="json") - executeGetAnalyticsFromForm - `) // type Analytics = z.infer; const gasBillDataWithUserAdjustments: any = executeGetAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, uploadedTextFile, state_id, county_id).toJs() @@ -341,41 +254,7 @@ export async function action({ request, params }: ActionFunctionArgs) { /** * second time and after, when table is modified, this becomes entrypoint */ - const executeRoundtripAnalyticsFromFormJs = await pyodide.runPythonAsync(` - from rules_engine import parser - from rules_engine.pydantic_models import ( - FuelType, - HeatLoadInput, - TemperatureInput, - ProcessedEnergyBillInput - ) - from rules_engine import engine, helpers - - - def executeRoundtripAnalyticsFromForm(summaryInputJs, temperatureInputJs, userAdjustedData, state_id, county_id): - """ - "processed_energy_bills" is the "roundtripping" parameter to be passed as userAdjustedData. - """ - - summaryInputFromJs = summaryInputJs.as_object_map().values()._mapping - temperatureInputFromJs =temperatureInputJs.as_object_map().values()._mapping - - design_temp_looked_up = helpers.get_design_temp(state_id, county_id) - # expect 1 for middlesex county: print("design temp check ",design_temp_looked_up, state_id, county_id) - summaryInput = HeatLoadInput( **summaryInputFromJs, design_temperature=design_temp_looked_up) - - temperatureInput = TemperatureInput(**temperatureInputFromJs) - - # third step, re-run of the table data - userAdjustedDataFromJsToPython = [ProcessedEnergyBillInput(**record) for record in userAdjustedData['processed_energy_bills'] ] - # print("py", userAdjustedDataFromJsToPython[0]) - - outputs2 = engine.get_outputs_normalized(summaryInput, None, temperatureInput, userAdjustedDataFromJsToPython) - # print("py2", outputs2.processed_energy_bills[0]) - return outputs2.model_dump(mode="json") - executeRoundtripAnalyticsFromForm - `) /** * Ask Alan, issue with list comprehension: diff --git a/heat-stack/app/utils/rules-engine.ts b/heat-stack/app/utils/rules-engine.ts new file mode 100644 index 00000000..06d3b703 --- /dev/null +++ b/heat-stack/app/utils/rules-engine.ts @@ -0,0 +1,144 @@ +import * as pyodideModule from 'pyodide' + +/* + LOAD PYODIDE +*/ +const getPyodide = async () => { + return await pyodideModule.loadPyodide({ + // This path is actually `public/pyodide-env`, but the browser knows where `public` is. Note that remix server needs `public/` + // TODO: figure out how to determine if we're in browser or remix server and use ternary. + indexURL: 'public/pyodide-env/', + }) +} +const runPythonScript = async () => { + const pyodide: any = await getPyodide() + return pyodide +} +// consider running https://github.com/codeforboston/home-energy-analysis-tool/blob/main/python/tests/test_rules_engine/test_engine.py +const pyodide: any = await runPythonScript() + +/* + LOAD PACKAGES + NOTES for pydantic, typing-extensions, annotated_types: + pyodide should match pyodide-core somewhat. + typing-extensions needs specific version per https://github.com/pyodide/pyodide/issues/4234#issuecomment-1771735148 + try getting it from + - https://pypi.org/project/pydantic/#files + - https://pypi.org/project/typing-extensions/ + - https://pypi.org/project/annotated-types/#files +*/ +await pyodide.loadPackage( + 'public/pyodide-env/pydantic_core-2.14.5-cp311-cp311-emscripten_3_1_32_wasm32.whl', +) +await pyodide.loadPackage( + 'public/pyodide-env/pydantic-2.5.2-py3-none-any.whl', +) +await pyodide.loadPackage( + 'public/pyodide-env/typing_extensions-4.8.0-py3-none-any.whl', +) +await pyodide.loadPackage( + 'public/pyodide-env/annotated_types-0.5.0-py3-none-any.whl', +) +await pyodide.loadPackage( + 'public/pyodide-env/rules_engine-0.0.1-py3-none-any.whl', +) + +/* + RULES-ENGINE CALLS +*/ +/** + * CSV Parser + * Need to parse the gas bill first to determine the start and end dates of the bill + * so that we can request the weather for those dates. + */ +export const executeParseGasBillPy = await pyodide.runPythonAsync(` + from rules_engine import parser + from rules_engine.pydantic_models import ( + FuelType, + HeatLoadInput, + TemperatureInput + ) + from rules_engine import engine + + def executeParse(csvDataJs): + naturalGasInputRecords = parser.parse_gas_bill(csvDataJs, parser.NaturalGasCompany.NATIONAL_GRID) + return naturalGasInputRecords.model_dump(mode="json") + executeParse +`) +/** + * Full call with csv data + * call to get_outputs_natural_gas + */ +export const executeGetAnalyticsFromFormJs = await pyodide.runPythonAsync(` + from rules_engine import parser + from rules_engine.pydantic_models import ( + FuelType, + HeatLoadInput, + TemperatureInput + ) + from rules_engine import engine, helpers + + def executeGetAnalyticsFromForm(summaryInputJs, temperatureInputJs, csvDataJs, state_id, county_id): + """ + second step: this will be the first time to draw the table + # two new geocode parameters may be needed for design temp: + # watch out for helpers.get_design_temp( addressMatches[0].geographies.counties[0]['STATE'] , addressMatches[0].geographies.counties[0]['COUNTY'] county_id) + # in addition to latitude and longitude from GeocodeUtil.ts object . + # pack the get_design_temp output into heat_load_input + """ + + summaryInputFromJs = summaryInputJs.as_object_map().values()._mapping + temperatureInputFromJs =temperatureInputJs.as_object_map().values()._mapping + + # We will just pass in this data + naturalGasInputRecords = parser.parse_gas_bill(csvDataJs, parser.NaturalGasCompany.NATIONAL_GRID) + + design_temp_looked_up = helpers.get_design_temp(state_id, county_id) + summaryInput = HeatLoadInput( **summaryInputFromJs, design_temperature=design_temp_looked_up) + + temperatureInput = TemperatureInput(**temperatureInputFromJs) + + outputs = engine.get_outputs_natural_gas(summaryInput, temperatureInput, naturalGasInputRecords) + + return outputs.model_dump(mode="json") + executeGetAnalyticsFromForm +`) +/** + * Full call with userAdjustedData + * second time and after, when table is modified, this becomes entrypoint + */ +export const executeRoundtripAnalyticsFromFormJs = await pyodide.runPythonAsync(` + from rules_engine import parser + from rules_engine.pydantic_models import ( + FuelType, + HeatLoadInput, + TemperatureInput, + ProcessedEnergyBillInput + ) + from rules_engine import engine, helpers + + + def executeRoundtripAnalyticsFromForm(summaryInputJs, temperatureInputJs, userAdjustedData, state_id, county_id): + """ + "processed_energy_bills" is the "roundtripping" parameter to be passed as userAdjustedData. + """ + + summaryInputFromJs = summaryInputJs.as_object_map().values()._mapping + temperatureInputFromJs =temperatureInputJs.as_object_map().values()._mapping + + design_temp_looked_up = helpers.get_design_temp(state_id, county_id) + # expect 1 for middlesex county: print("design temp check ",design_temp_looked_up, state_id, county_id) + summaryInput = HeatLoadInput( **summaryInputFromJs, design_temperature=design_temp_looked_up) + + temperatureInput = TemperatureInput(**temperatureInputFromJs) + + # third step, re-run of the table data + userAdjustedDataFromJsToPython = [ProcessedEnergyBillInput(**record) for record in userAdjustedData['processed_energy_bills'] ] + # print("py", userAdjustedDataFromJsToPython[0]) + + outputs2 = engine.get_outputs_normalized(summaryInput, None, temperatureInput, userAdjustedDataFromJsToPython) + + # print("py2", outputs2.processed_energy_bills[0]) + return outputs2.model_dump(mode="json") + executeRoundtripAnalyticsFromForm +`) \ No newline at end of file From e71dada4059df74706f57b5503c61b76118a8f4a Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Fri, 31 Jan 2025 03:59:26 +0000 Subject: [PATCH 3/9] extracted data parsing functions to /app/utils/data-parser.ts Co-authored-by: Derek McIntire --- heat-stack/app/routes/_heat+/single.tsx | 68 +----------------------- heat-stack/app/utils/data-parser.ts | 69 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 67 deletions(-) create mode 100644 heat-stack/app/utils/data-parser.ts diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 5474fd9f..f28e50a5 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -10,6 +10,7 @@ import { createMemoryUploadHandler } from '@remix-run/server-runtime/dist/upload import { type z } from 'zod' import { Button } from '#/app/components/ui/button.tsx' import { ErrorList } from '#app/components/ui/heat/CaseSummaryComponents/ErrorList.tsx' +import { replacedMapToObject, replacer, reviver } from '#app/utils/data-parser.ts' import GeocodeUtil from '#app/utils/GeocodeUtil.ts' import { executeGetAnalyticsFromFormJs, @@ -303,73 +304,6 @@ Traceback (most recent call last): File "", line 32, // return redirect(`/single`) } -/** Pass this to JSON.stringify() - * - * Usage: - * const originalValue = new Map([['a', 1]]); - * const str = JSON.stringify(originalValue, replacer); - * - * See https://stackoverflow.com/a/56150320 - */ -function replacer(key: any, value: any) { - if(value instanceof Map) { - return { - dataType: 'Map', - value: Array.from(value.entries()), // or with spread: value: [...value] - }; - } else { - return value; - } -} - -/** Pass this to JSON.parse() - * - * Usage: - * const originalValue = new Map([['a', 1]]); - * const str = JSON.stringify(originalValue, replacer); - * const newValue = JSON.parse(str, reviver); - * - * See https://stackoverflow.com/a/56150320 - */ -function reviver(key: any, value: any) { - if(typeof value === 'object' && value !== null) { - if (value.dataType === 'Map') { - return new Map(value.value); - } - } - return value; -} - -/** - * Translates an already replaced (see https://stackoverflow.com/a/56150320) and then parsed Map from pyodide into a plain js Object. - * @param input {Map} - * @returns {Object} - */ -function replacedMapToObject(input: any): any { - // Base case: if input is not an object or is null, return it as-is - if (typeof input !== 'object' || input === null) { - return input - } - - // Handle case where input is a Map-like object (with "dataType" as "Map" and a "value" array) - if (input.dataType === 'Map' && Array.isArray(input.value)) { - const obj: Record = {} // Initialize an empty object - for (const [key, value] of input.value) { - obj[key] = replacedMapToObject(value) // Recursively process nested Maps - } - return obj - } - - // Handle case where input is an array - if (Array.isArray(input)) { - return input.map(replacedMapToObject) // Recursively process each array element - } - - console.log('input', input) - // Return the input for any other types of objects - return input -} - export default function Inputs() { // const location = useLocation(); diff --git a/heat-stack/app/utils/data-parser.ts b/heat-stack/app/utils/data-parser.ts new file mode 100644 index 00000000..5c5d100d --- /dev/null +++ b/heat-stack/app/utils/data-parser.ts @@ -0,0 +1,69 @@ +// These functions are used to process data returned from the Action on +// /workspaces/home-energy-analysis-tool/heat-stack/app/routes/_heat+/single.tsx + +/** Pass this to JSON.stringify() + * + * Usage: + * const originalValue = new Map([['a', 1]]); + * const str = JSON.stringify(originalValue, replacer); + * + * See https://stackoverflow.com/a/56150320 + */ +export function replacer(key: any, value: any) { + if(value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } else { + return value; + } +} + +/** Pass this to JSON.parse() + * + * Usage: + * const originalValue = new Map([['a', 1]]); + * const str = JSON.stringify(originalValue, replacer); + * const newValue = JSON.parse(str, reviver); + * + * See https://stackoverflow.com/a/56150320 + */ +export function reviver(key: any, value: any) { + if(typeof value === 'object' && value !== null) { + if (value.dataType === 'Map') { + return new Map(value.value); + } + } + return value; +} + +/** + * Translates an already replaced (see https://stackoverflow.com/a/56150320) and then parsed Map from pyodide into a plain js Object. + * @param input {Map} + * @returns {Object} + */ +export function replacedMapToObject(input: any): any { + // Base case: if input is not an object or is null, return it as-is + if (typeof input !== 'object' || input === null) { + return input + } + + // Handle case where input is a Map-like object (with "dataType" as "Map" and a "value" array) + if (input.dataType === 'Map' && Array.isArray(input.value)) { + const obj: Record = {} // Initialize an empty object + for (const [key, value] of input.value) { + obj[key] = replacedMapToObject(value) // Recursively process nested Maps + } + return obj + } + + // Handle case where input is an array + if (Array.isArray(input)) { + return input.map(replacedMapToObject) // Recursively process each array element + } + + console.log('input', input) + // Return the input for any other types of objects + return input +} \ No newline at end of file From fe6f3e5f493b13141a6f1d44578a51fa38150336 Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Fri, 31 Jan 2025 04:24:56 +0000 Subject: [PATCH 4/9] comment cleanup Co-authored-by: Derek McIntire --- heat-stack/app/routes/_heat+/single.tsx | 157 ++++++++---------------- heat-stack/app/utils/rules-engine.ts | 23 +++- 2 files changed, 71 insertions(+), 109 deletions(-) diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index f28e50a5..623ff691 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -21,7 +21,7 @@ import WeatherUtil from '#app/utils/WeatherUtil.ts' -// TODO NEXT WEEK +// THESE ARE OLD NOTES, please someone go through and clean these up :) // - [x] Server side error checking/handling // - [x] ~Save to cookie and redirect to next form~ Put everything on the same page // - [x] - Get zod and Typescript to play nice @@ -246,58 +246,11 @@ export async function action({ request, params }: ActionFunctionArgs) { /** Main form entrypoint */ - - // type Analytics = z.infer; const gasBillDataWithUserAdjustments: any = executeGetAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, uploadedTextFile, state_id, county_id).toJs() - //console.log("gasBillDataWithUserAdjustments billing records [0]", gasBillDataWithUserAdjustments.get('processed_energy_bills')[0] ) - - /** - * second time and after, when table is modified, this becomes entrypoint - */ - - - /** - * Ask Alan, issue with list comprehension: -Traceback (most recent call last): File "", line 32, - in executeRoundtripAnalyticsFromForm TypeError: - list indices must be integers or slices, not str - */ - /* - For - 'processed_energy_bills' => [ - Map(9) { - 'period_start_date' => '2020-10-02', - 'period_end_date' => '2020-11-04', - 'usage' => 29, - 'analysis_type_override' => undefined, - 'inclusion_override' => false, - 'analysis_type' => 0, - 'default_inclusion' => false, - 'eliminated_as_outlier' => false, - 'whole_home_heat_loss_rate' => undefined - }, */ - - - // const billingRecords = gasBillDataWithUserAdjustments.get('processed_energy_bills') - // billingRecords.forEach((record: any) => { - // record.set('inclusion_override', true); - // }); - // gasBillDataWithUserAdjustments.set('processed_energy_bills', null) - // gasBillDataWithUserAdjustments.set('processed_energy_bills', billingRecords) - //console.log("(after customization) gasBillDataWithUserAdjustments billing records[0]", gasBillDataWithUserAdjustments.get('processed_energy_bills')[0]) - /* why is inclusion_override still false after roundtrip */ const calculatedData: any = executeRoundtripAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, gasBillDataWithUserAdjustments, state_id, county_id).toJs() - // console.log("calculatedData billing records[0]", calculatedData.get('processed_energy_bills')[0]); - // console.log("calculatedData", calculatedData); - // console.log("(after round trip) gasBillDataWithUserAdjustments billing records[0]", gasBillDataWithUserAdjustments.get('processed_energy_bills')[0]) - - // const otherResult = executePy(summaryInput, convertedDatesTIWD, exampleNationalGridCSV); - const str_version = JSON.stringify(calculatedData, replacer); - // const json_version = JSON.parse(str_version); - // console.log("str_version", str_version); // Consider adding to form data return json({data: str_version}); @@ -306,8 +259,54 @@ Traceback (most recent call last): File "", line 32, export default function Inputs() { - // const location = useLocation(); - // console.log(`location:`, location); // `.state` is `null` + /* @ts-ignore */ + // USAGE OF lastResult + // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) + + /** + * Example Data Returned + * Where temp1 is a temporary variable with the main Map of Maps (or undefined if page not yet submitted). + * + * 1 of 3: heat_load_output + * console.log("Summary Output", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('heat_load_output'): undefined) + * + * temp1.get('heat_load_output'): Map(9) { + * estimated_balance_point → 61.5, + * other_fuel_usage → 0.2857142857142857, + * average_indoor_temperature → 67, + * difference_between_ti_and_tbp → 5.5, + * design_temperature → 1, + * whole_home_heat_loss_rate → 48001.81184312083, + * standard_deviation_of_heat_loss_rate → 0.08066745182677547, + * average_heat_load → 3048115.0520381727, + * maximum_heat_load → 3312125.0171753373 + * } + * + * + * 2 of 3: processed_energy_bills + * console.log("EnergyUseHistoryChart table data", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('processed_energy_bills'): undefined) + * + * temp1.get('processed_energy_bills') + * Array(25) [ Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), … ] + * + * temp1.get('processed_energy_bills')[0] + * Map(9) { period_start_date → "2020-10-02", period_end_date → "2020-11-04", usage → 29, analysis_type_override → null, inclusion_override → true, analysis_type → 0, default_inclusion → false, eliminated_as_outlier → false, whole_home_heat_loss_rate → null } + * + * temp1.get('processed_energy_bills')[0].get('period_start_date') + * "2020-10-02" + * + * + * 3 of 3: balance_point_graph + * console.log("HeatLoad chart", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('balance_point_graph')?.get('records'): undefined) + * + * temp1.get('balance_point_graph').get('records') + Array(23) [ Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), … ] + temp1.get('balance_point_graph').get('records')[0] + Map(5) { balance_point → 60, heat_loss_rate → 51056.8007761249, change_in_heat_loss_rate → 0, percent_change_in_heat_loss_rate → 0, standard_deviation → 0.17628334816871494 } + temp1.get('balance_point_graph').get('records')[0].get('heat_loss_rate') + */ + /* @ts-ignore */ + const lastResult = useActionData() console.log('lastResult', lastResult) @@ -343,44 +342,8 @@ export default function Inputs() { // console.error('Error parsing lastResult data:', error); } } - console.log('currentUsageData', currentUsageData) - - - /* @ts-ignore */ - // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) - - /** - * Where temp1 is a temporary variable with the main Map of Maps (or undefined if page not yet submitted). - * - * temp1.get('heat_load_output'): Map(9) { estimated_balance_point → 61.5, other_fuel_usage → 0.2857142857142857, average_indoor_temperature → 67, difference_between_ti_and_tbp → 5.5, design_temperature → 1, whole_home_heat_loss_rate → 48001.81184312083, standard_deviation_of_heat_loss_rate → 0.08066745182677547, average_heat_load → 3048115.0520381727, maximum_heat_load → 3312125.0171753373 } - */ - /* @ts-ignore */ - // console.log("Summary Output", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('heat_load_output'): undefined) - - /** - * Where temp1 is a temporary variable with the main Map of Maps (or undefined if page not yet submitted). - * temp1.get('processed_energy_bills') - * Array(25) [ Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), Map(9), … ] - * temp1.get('processed_energy_bills')[0] - * Map(9) { period_start_date → "2020-10-02", period_end_date → "2020-11-04", usage → 29, analysis_type_override → null, inclusion_override → true, analysis_type → 0, default_inclusion → false, eliminated_as_outlier → false, whole_home_heat_loss_rate → null } - * temp1.get('processed_energy_bills')[0].get('period_start_date') - * "2020-10-02" - */ - /* @ts-ignore */ - // console.log("EnergyUseHistoryChart table data", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('processed_energy_bills'): undefined) - - /** - * Where temp1 is a temporary variable with the main Map of Maps (or undefined if page not yet submitted). - * temp1.get('balance_point_graph').get('records') - Array(23) [ Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), Map(5), … ] - temp1.get('balance_point_graph').get('records')[0] - Map(5) { balance_point → 60, heat_loss_rate → 51056.8007761249, change_in_heat_loss_rate → 0, percent_change_in_heat_loss_rate → 0, standard_deviation → 0.17628334816871494 } - temp1.get('balance_point_graph').get('records')[0].get('heat_loss_rate') - *//* @ts-ignore */ - - // console.log("HeatLoad chart", lastResult !== undefined ? JSON.parse(lastResult.data, reviver)?.get('balance_point_graph')?.get('records'): undefined) type ActionResult = | SubmissionResult | { data: string } @@ -391,28 +354,6 @@ export default function Inputs() { return result !== undefined && 'data' in result && typeof (result as any).data === 'string'; } - //////////////////////// - // TO BE DELETED - // Old way of handing results - - // let usage_data = null; - - // // Ensure we handle the result properly - // if (show_usage_data && lastResult && hasDataProperty(lastResult)) { - // try { - // // Parse the JSON string from lastResult.data - // const parsedData = JSON.parse(lastResult.data); - - // // Recursively transform any Maps in lastResult to objects - // usage_data = replacedMapToObject(parsedData); // Get the relevant part of the transformed result - // console.log('usage_data', usage_data) - - // } catch (error) { - // console.error('Error parsing lastResult data:', error); - // } - // } - //////////////////////// - type SchemaZodFromFormType = z.infer const [form, fields] = useForm({ /* removed lastResult , consider re-adding https://conform.guide/api/react/useForm#options */ diff --git a/heat-stack/app/utils/rules-engine.ts b/heat-stack/app/utils/rules-engine.ts index 06d3b703..c21add86 100644 --- a/heat-stack/app/utils/rules-engine.ts +++ b/heat-stack/app/utils/rules-engine.ts @@ -141,4 +141,25 @@ export const executeRoundtripAnalyticsFromFormJs = await pyodide.runPythonAsync( # print("py2", outputs2.processed_energy_bills[0]) return outputs2.model_dump(mode="json") executeRoundtripAnalyticsFromForm -`) \ No newline at end of file +`) + + /** + * Ask Alan, issue with list comprehension: +Traceback (most recent call last): File "", line 32, + in executeRoundtripAnalyticsFromForm TypeError: + list indices must be integers or slices, not str + */ + /* + For + 'processed_energy_bills' => [ + Map(9) { + 'period_start_date' => '2020-10-02', + 'period_end_date' => '2020-11-04', + 'usage' => 29, + 'analysis_type_override' => undefined, + 'inclusion_override' => false, + 'analysis_type' => 0, + 'default_inclusion' => false, + 'eliminated_as_outlier' => false, + 'whole_home_heat_loss_rate' => undefined + }, */ \ No newline at end of file From 63c320d9a45f3405af160422876af60d2bceff08 Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Fri, 31 Jan 2025 05:55:23 +0000 Subject: [PATCH 5/9] extracted file upload functionality Co-authored-by: Derek McIntire --- heat-stack/app/routes/_heat+/single.tsx | 28 ++------------------- heat-stack/app/utils/file-upload-handler.ts | 23 +++++++++++++++++ 2 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 heat-stack/app/utils/file-upload-handler.ts diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 623ff691..73c60bfa 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -11,6 +11,7 @@ import { type z } from 'zod' import { Button } from '#/app/components/ui/button.tsx' import { ErrorList } from '#app/components/ui/heat/CaseSummaryComponents/ErrorList.tsx' import { replacedMapToObject, replacer, reviver } from '#app/utils/data-parser.ts' +import { fileUploadHandler, uploadHandler } from '#app/utils/file-upload-handler.ts' import GeocodeUtil from '#app/utils/GeocodeUtil.ts' import { executeGetAnalyticsFromFormJs, @@ -19,8 +20,6 @@ import { } from '#app/utils/rules-engine.ts' import WeatherUtil from '#app/utils/WeatherUtil.ts' - - // THESE ARE OLD NOTES, please someone go through and clean these up :) // - [x] Server side error checking/handling // - [x] ~Save to cookie and redirect to next form~ Put everything on the same page @@ -86,29 +85,11 @@ const Schema = HomeFormSchema.and(CurrentHeatingSystemSchema) /* .and(HeatLoadAn export async function action({ request, params }: ActionFunctionArgs) { // Checks if url has a homeId parameter, throws 400 if not there // invariantResponse(params.homeId, 'homeId param is required') - console.log('action started') - const uploadHandler = createMemoryUploadHandler({ - maxPartSize: 1024 * 1024 * 5, // 5 MB - }) const formData = await parseMultipartFormData(request, uploadHandler) + const uploadedTextFile: string = await fileUploadHandler(formData) - const file = formData.get('energy_use_upload') as File // fix as File? - - async function handleFile(file: File) { - try { - const fileContent = await file.text() - return fileContent - } catch (error) { - console.error('Error reading file:', error) - return '' - } - } - - // TODO: think about the edge cases and handle the bad user input here: - const uploadedTextFile: string = file !== null ? await handleFile(file) : '' - const submission = parseWithZod(formData, { schema: Schema, }) @@ -147,11 +128,6 @@ export async function action({ request, params }: ActionFunctionArgs) { // await updateNote({ id: params.noteId, title, content }) //code snippet from - https://github.com/epicweb-dev/web-forms/blob/2c10993e4acffe3dd9ad7b9cb0cdf89ce8d46ecf/exercises/04.file-upload/01.solution.multi-part/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L180 - // const formData = await parseMultipartFormData( - // request, - // createMemoryUploadHandler({ maxPartSize: MAX_UPLOAD_SIZE }), - // ) - console.log('loading geocodeUtil/weatherUtil') const geocodeUtil = new GeocodeUtil() diff --git a/heat-stack/app/utils/file-upload-handler.ts b/heat-stack/app/utils/file-upload-handler.ts new file mode 100644 index 00000000..551ce533 --- /dev/null +++ b/heat-stack/app/utils/file-upload-handler.ts @@ -0,0 +1,23 @@ +import { createMemoryUploadHandler } from '@remix-run/server-runtime/dist/upload/memoryUploadHandler.js' + +export const uploadHandler = createMemoryUploadHandler({ + maxPartSize: 1024 * 1024 * 5, // 5 MB +}) + +async function handleFile(file: File) { + try { + const fileContent = await file.text() + return fileContent + } catch (error) { + console.error('Error reading file:', error) + return '' + } +} +export async function fileUploadHandler(formData) { + const file = formData.get('energy_use_upload') as File // fix as File? + + // TODO: think about the edge cases and handle the bad user input here: + const uploadedTextFile: string = file !== null ? await handleFile(file) : '' + + return uploadedTextFile +} \ No newline at end of file From 55bfe968899b740940e69cdf278463a12841abf0 Mon Sep 17 00:00:00 2001 From: derekvmcintire Date: Fri, 31 Jan 2025 12:33:37 -0500 Subject: [PATCH 6/9] need to figure out how to trigger recalc --- .../EnergyUseHistory.tsx | 14 +++--- .../EnergyUseHistoryChart.tsx | 12 +++++- heat-stack/app/routes/_heat+/single.tsx | 43 ++++++++++++------- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index 9d584b88..5a363f52 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx @@ -1,7 +1,7 @@ import { Upload } from 'lucide-react' import { Button } from '#/app/components/ui/button.tsx' -import { type UsageDataSchema } from '#/types/types.ts'; +import { BillingRecordsSchema, type UsageDataSchema } from '#/types/types.ts'; import { AnalysisHeader } from './AnalysisHeader.tsx' import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' @@ -11,11 +11,15 @@ import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' // import { Input } from '#/app/components/ui/input.tsx' // import { Label } from '#/app/components/ui/label.tsx' +interface EnergyUseHistoryProps { + usage_data: UsageDataSchema; + recalculateFn: (billingRecords: BillingRecordsSchema) => void; +} + export function EnergyUseHistory({ usage_data, -}: { - usage_data: UsageDataSchema -}) { + recalculateFn +}: EnergyUseHistoryProps) { const titleClass = 'text-5xl font-extrabold tracking-wide mt-10' // const subtitleClass = 'text-2xl font-semibold text-zinc-950 mt-9' @@ -44,7 +48,7 @@ export function EnergyUseHistory({ {usage_data && ( <> - + )} diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx index 4f0471b0..2d54c896 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx @@ -54,9 +54,19 @@ import NotAllowedInCalculations from './assets/NotAllowedInCalculations.svg' // naturalGasBillRecord04, // ] -export function EnergyUseHistoryChart({ usage_data }: { usage_data: UsageDataSchema }) { +interface EngergyUseHistoryChartProps { + usage_data: UsageDataSchema + recalculateFn: (billingRecords: BillingRecordsSchema) => void; +} + +export function EnergyUseHistoryChart({ usage_data, recalculateFn }: EngergyUseHistoryChartProps) { const [billingRecords, setBillingRecords] = useState([]) + useEffect(() => { + console.log('billing records changed, should trigger recalculation: ', billingRecords) + recalculateFn(billingRecords) + }, [billingRecords]) + useEffect(() => { if (usage_data?.processed_energy_bills) { // Process the billing records directly without converting from Map diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 73c60bfa..19dbd6b8 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -57,7 +57,7 @@ import WeatherUtil from '#app/utils/WeatherUtil.ts' // Ours import { HomeSchema, LocationSchema, CaseSchema /* validateNaturalGasUsageData, HeatLoadAnalysisZod */ } from '../../../types/index.ts' -import { type NaturalGasUsageDataSchema} from '../../../types/types.ts' +import { BillingRecordsSchema, UsageDataSchema, type NaturalGasUsageDataSchema} from '../../../types/types.ts' import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx' import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx' import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' @@ -233,6 +233,12 @@ export async function action({ request, params }: ActionFunctionArgs) { // return redirect(`/single`) } +/** RECALCULATE WHEN BILLING RECORDS UPDATE -- maybe this can be more generic in the future */ +const recalculateFromBillingRecordsChange = (billingRecords: BillingRecordsSchema) => { + // do something with billing records + console.log('recalculating with billing records: ', billingRecords) +} + export default function Inputs() { /* @ts-ignore */ @@ -293,10 +299,23 @@ export default function Inputs() { // - use the UsageDataSchema type here? // - use processed_energy_bills in Checkbox behavior // - let currentUsageData = { - heat_load_output: undefined, - balance_point_graph: undefined, - processed_energy_bills: undefined, + let currentUsageData; + + /** + * Builds the current usage data based on the parsed last result. + * @param parsedLastResult - The parsed last result. + * @returns The current usage data. + */ + const buildCurrentUsageData = (parsedLastResult: Map): UsageDataSchema => { + const currentUsageData = { + heat_load_output: Object.fromEntries(parsedLastResult?.get('heat_load_output')), + balance_point_graph: Object.fromEntries(parsedLastResult?.get('balance_point_graph')), + processed_energy_bills: parsedLastResult?.get('processed_energy_bills').map((map: any) => Object.fromEntries(map)), + } + + // typecasting as UsageDataSchema because the types here do not quite line up coming from parsedLastResult as Map - might need to think about how to handle typing the results from the python output more strictly + // Type '{ heat_load_output: { [k: string]: any; }; balance_point_graph: { [k: string]: any; }; processed_energy_bills: any; }' is not assignable to type '{ heat_load_output: { estimated_balance_point: number; other_fuel_usage: number; average_indoor_temperature: number; difference_between_ti_and_tbp: number; design_temperature: number; whole_home_heat_loss_rate: number; standard_deviation_of_heat_loss_rate: number; average_heat_load: number; maximum_heat_load: number...'. + return currentUsageData as UsageDataSchema; } if (show_usage_data && hasDataProperty(lastResult)) { @@ -305,14 +324,7 @@ export default function Inputs() { const parsedLastResult = JSON.parse(lastResult.data, reviver) as Map; console.log('parsedLastResult', parsedLastResult) - // TODO: Parsing without reviver to get processed_energy_bills with Objects instead of maps - // Figure out how to use parsedLastResult instead - const parsedLastResultObject = JSON.parse(lastResult.data) - - currentUsageData.heat_load_output = Object.fromEntries(parsedLastResult?.get('heat_load_output')); - currentUsageData.balance_point_graph = Object.fromEntries(parsedLastResult?.get('balance_point_graph')); - - currentUsageData.processed_energy_bills = replacedMapToObject(parsedLastResultObject).processed_energy_bills + currentUsageData = buildCurrentUsageData(parsedLastResult); } catch (error) { // console.error('Error parsing lastResult data:', error); @@ -350,6 +362,7 @@ export default function Inputs() { shouldValidate: 'onBlur', }) + // @TODO: we might need to guarantee that currentUsageData exists before rendering - currently we need to typecast an empty object in order to pass typechecking for return ( <>
{JSON.stringify(lastResult, null, 2)}
@@ -366,11 +379,11 @@ export default function Inputs() { */} - + - {show_usage_data && } + {show_usage_data && } ) } From 4158906da7fdff6070744cad00edf5b74dfbca01 Mon Sep 17 00:00:00 2001 From: derekvmcintire Date: Tue, 4 Feb 2025 21:08:03 -0500 Subject: [PATCH 7/9] Pseudo code for updating billing records --- heat-stack/app/routes/_heat+/single.tsx | 53 ++++++++++++++++++++----- heat-stack/app/utils/data-parser.ts | 2 + 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 19dbd6b8..7335c357 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -62,7 +62,7 @@ import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryCompon import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx' import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' import HeatLoadAnalysis from './heatloadanalysis.tsx' -import React from 'react' +import React, { useState } from 'react' /** Modeled off the conform example at * https://github.com/epicweb-dev/web-forms/blob/b69e441f5577b91e7df116eba415d4714daacb9d/exercises/03.schema-validation/03.solution.conform-form/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L48 */ @@ -225,22 +225,18 @@ export async function action({ request, params }: ActionFunctionArgs) { const gasBillDataWithUserAdjustments: any = executeGetAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, uploadedTextFile, state_id, county_id).toJs() const calculatedData: any = executeRoundtripAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, gasBillDataWithUserAdjustments, state_id, county_id).toJs() - + console.log('calculatedData: ', calculatedData) const str_version = JSON.stringify(calculatedData, replacer); - // Consider adding to form data - return json({data: str_version}); + // Consider adding to form data, + return json({data: str_version, parsedAndValidatedFormSchema, convertedDatesTIWD, gasBillDataWithUserAdjustments, state_id, county_id}); // return redirect(`/single`) } -/** RECALCULATE WHEN BILLING RECORDS UPDATE -- maybe this can be more generic in the future */ -const recalculateFromBillingRecordsChange = (billingRecords: BillingRecordsSchema) => { - // do something with billing records - console.log('recalculating with billing records: ', billingRecords) -} -export default function Inputs() { + +export default function SubmitAnalysis() { /* @ts-ignore */ // USAGE OF lastResult // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) @@ -299,7 +295,35 @@ export default function Inputs() { // - use the UsageDataSchema type here? // - use processed_energy_bills in Checkbox behavior // - let currentUsageData; + let currentUsageData; // maybe initialize state here instead of a variable + + const [usageData, setUsageData] = useState(undefined); + + // @TODO: left off here + /** RECALCULATE WHEN BILLING RECORDS UPDATE -- maybe this can be more generic in the future */ + const recalculateFromBillingRecordsChange = ( + parsedLastResult: Map, + billingRecords: BillingRecordsSchema + ) => { + // setUsageData(buildCurrentMapOfUsageData(parsedLastResult, billingRecords)); + + // JSON API or pyodide running on client side + // utils/rules-engine.ts executeRoundtripAnalyticsFromFormJs(parsedAndValidatedFormSchema, convertedDatesTIWD, gasBillDataWithUserAdjustments, state_id, county_id) + + // do something with billing records + console.log('recalculating with billing records: ', billingRecords) + } + + + // @TODO implement + const buildCurrentMapOfUsageData = (parsedLastResult: Map, processedEnergyBills: BillingRecordsSchema) => { + // make a copy of parsedLastResult + + // const processedEnergyBillsWithMaps = change records from objects to maps in processedEnergyBills using the same key values, and order matters (maybe) + + // parsedLastResultCopy.set("processed_energy_bills", processedEnergyBillsWithMaps) + // return parsedLastResultCopy + } /** * Builds the current usage data based on the parsed last result. @@ -383,7 +407,14 @@ export default function Inputs() { +
{show_usage_data && } + ) } diff --git a/heat-stack/app/utils/data-parser.ts b/heat-stack/app/utils/data-parser.ts index 5c5d100d..28586ab5 100644 --- a/heat-stack/app/utils/data-parser.ts +++ b/heat-stack/app/utils/data-parser.ts @@ -38,6 +38,8 @@ export function reviver(key: any, value: any) { return value; } + + /** * Translates an already replaced (see https://stackoverflow.com/a/56150320) and then parsed Map from pyodide into a plain js Object. * @param input {Map} From e6371bacff857e241f8bc1bf9d9ba4d15fde55e6 Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Tue, 4 Feb 2025 21:21:05 -0500 Subject: [PATCH 8/9] extract date and weather parsing to date-temp-util --- heat-stack/app/routes/_heat+/single.tsx | 63 ++----------------- heat-stack/app/utils/date-temp-util.ts | 82 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 heat-stack/app/utils/date-temp-util.ts diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 7335c357..6d31e779 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -63,6 +63,7 @@ import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' import HeatLoadAnalysis from './heatloadanalysis.tsx' import React, { useState } from 'react' +import getConvertedDatesTIWD from '#app/utils/date-temp-util.ts' /** Modeled off the conform example at * https://github.com/epicweb-dev/web-forms/blob/b69e441f5577b91e7df116eba415d4714daacb9d/exercises/03.schema-validation/03.solution.conform-form/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L48 */ @@ -128,16 +129,6 @@ export async function action({ request, params }: ActionFunctionArgs) { // await updateNote({ id: params.noteId, title, content }) //code snippet from - https://github.com/epicweb-dev/web-forms/blob/2c10993e4acffe3dd9ad7b9cb0cdf89ce8d46ecf/exercises/04.file-upload/01.solution.multi-part/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L180 - console.log('loading geocodeUtil/weatherUtil') - - const geocodeUtil = new GeocodeUtil() - const weatherUtil = new WeatherUtil() - - let {coordinates, state_id, county_id} = await geocodeUtil.getLL(address) - let {x, y} = coordinates ?? {x: 0, y: 0}; - - console.log('geocoded', x, y) - // CSV entrypoint parse_gas_bill(data: str, company: NaturalGasCompany) // Main form entrypoint @@ -156,12 +147,6 @@ export async function action({ request, params }: ActionFunctionArgs) { // design_temperature: 12 /* TODO: see #162 and esp. #123*/ }) - // console.log('parsedAndValidatedFormSchema', parsedAndValidatedFormSchema) - - - // console.log("uploadedTextFile", uploadedTextFile) - - /** Example: * records: [ * Map(4) { @@ -176,49 +161,9 @@ export async function action({ request, params }: ActionFunctionArgs) { */ // This assignment of the same name is a special thing. We don't remember the name right now. // It's not necessary, but it is possible. - const pyodideResultsFromTextFile: NaturalGasUsageDataSchema = executeParseGasBillPy(uploadedTextFile).toJs() - - // console.log('result', pyodideResultsFromTextFile )//, validateNaturalGasUsageData(pyodideResultsFromTextFile)) - const startDateString = pyodideResultsFromTextFile.get('overall_start_date'); - const endDateString = pyodideResultsFromTextFile.get('overall_end_date'); - - if (typeof startDateString !== 'string' || typeof endDateString !== 'string') { - throw new Error('Start date or end date is missing or invalid'); - } - - // Get today's date - const today = new Date(); - // Calculate the date 2 years ago from today - const twoYearsAgo = new Date(today); - twoYearsAgo.setFullYear(today.getFullYear() - 2); - - let start_date = new Date(startDateString); - let end_date = new Date(endDateString); - - // Use default dates if parsing fails - if (isNaN(start_date.getTime())) { - console.warn('Invalid start date, using date from 2 years ago'); - start_date = twoYearsAgo; - } - if (isNaN(end_date.getTime())) { - console.warn('Invalid end date, using today\'s date'); - end_date = today; - } - - // Function to ensure we always return a valid date string - const formatDateString = (date: Date): string => { - return date.toISOString().split('T')[0] || date.toISOString().slice(0, 10); - }; - - const weatherData = await weatherUtil.getThatWeathaData( - x, - y, - formatDateString(start_date), - formatDateString(end_date) - ); - - const datesFromTIWD = weatherData.dates.map(datestring => new Date(datestring).toISOString().split('T')[0]) - const convertedDatesTIWD = {dates: datesFromTIWD, temperatures: weatherData.temperatures} + + const {convertedDatesTIWD, state_id, county_id} = await getConvertedDatesTIWD(uploadedTextFile, address) + /** Main form entrypoint */ diff --git a/heat-stack/app/utils/date-temp-util.ts b/heat-stack/app/utils/date-temp-util.ts new file mode 100644 index 00000000..818d93a7 --- /dev/null +++ b/heat-stack/app/utils/date-temp-util.ts @@ -0,0 +1,82 @@ +/** This function takes a CSV string and an address + * and returns date and weather data, + * and geolocation information +*/ + +import { type NaturalGasUsageDataSchema } from "#types/index.ts"; +import GeocodeUtil from "./GeocodeUtil.ts"; +import { executeParseGasBillPy } from "./rules-engine.ts"; +import WeatherUtil from "./WeatherUtil.ts"; + +export default async function getConvertedDatesTIWD(uploadedTextFile: string, address: string,) { + + console.log('loading geocodeUtil/weatherUtil') + + const geocodeUtil = new GeocodeUtil() + const weatherUtil = new WeatherUtil() + + let {coordinates, state_id, county_id} = await geocodeUtil.getLL(address) + let {x, y} = coordinates ?? {x: 0, y: 0}; + + console.log('geocoded', x, y) + + /** Example: + * records: [ + * Map(4) { + * 'period_start_date' => '2022-10-04', + * 'period_end_date' => '2022-11-03', + * 'usage_therms' => 19, + * 'inclusion_override' => undefined + * } + * ], + * 'overall_start_date' => '2020-10-02', + * 'overall_end_date' => '2022-11-03' + */ + // This assignment of the same name is a special thing. We don't remember the name right now. + // It's not necessary, but it is possible. + const pyodideResultsFromTextFile: NaturalGasUsageDataSchema = executeParseGasBillPy(uploadedTextFile).toJs() + + // console.log('result', pyodideResultsFromTextFile )//, validateNaturalGasUsageData(pyodideResultsFromTextFile)) + const startDateString = pyodideResultsFromTextFile.get('overall_start_date'); + const endDateString = pyodideResultsFromTextFile.get('overall_end_date'); + + if (typeof startDateString !== 'string' || typeof endDateString !== 'string') { + throw new Error('Start date or end date is missing or invalid'); + } + + // Get today's date + const today = new Date(); + // Calculate the date 2 years ago from today + const twoYearsAgo = new Date(today); + twoYearsAgo.setFullYear(today.getFullYear() - 2); + + let start_date = new Date(startDateString); + let end_date = new Date(endDateString); + + // Use default dates if parsing fails + if (isNaN(start_date.getTime())) { + console.warn('Invalid start date, using date from 2 years ago'); + start_date = twoYearsAgo; + } + if (isNaN(end_date.getTime())) { + console.warn('Invalid end date, using today\'s date'); + end_date = today; + } + + // Function to ensure we always return a valid date string + const formatDateString = (date: Date): string => { + return date.toISOString().split('T')[0] || date.toISOString().slice(0, 10); + }; + + const weatherData = await weatherUtil.getThatWeathaData( + x, + y, + formatDateString(start_date), + formatDateString(end_date) + ); + + const datesFromTIWD = weatherData.dates.map(datestring => new Date(datestring).toISOString().split('T')[0]) + const convertedDatesTIWD = {dates: datesFromTIWD, temperatures: weatherData.temperatures} + + return {convertedDatesTIWD, state_id, county_id} +} \ No newline at end of file From f7b95f11eb6fa8beaf3a81a453907a7b7bf8d038 Mon Sep 17 00:00:00 2001 From: dwindleduck <119227220+dwindleduck@users.noreply.github.com> Date: Tue, 4 Feb 2025 21:25:58 -0500 Subject: [PATCH 9/9] fix conditional display logic for EnergyUseHistory component --- .../ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx | 6 ++++-- heat-stack/app/routes/_heat+/single.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index 5a363f52..67375b0a 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx @@ -14,11 +14,13 @@ import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' interface EnergyUseHistoryProps { usage_data: UsageDataSchema; recalculateFn: (billingRecords: BillingRecordsSchema) => void; + show_usage_data: boolean } export function EnergyUseHistory({ usage_data, - recalculateFn + recalculateFn, + show_usage_data }: EnergyUseHistoryProps) { const titleClass = 'text-5xl font-extrabold tracking-wide mt-10' // const subtitleClass = 'text-2xl font-semibold text-zinc-950 mt-9' @@ -45,7 +47,7 @@ export function EnergyUseHistory({ Get example file here - {usage_data && ( + {show_usage_data && ( <> diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 6d31e779..5c039b35 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -162,6 +162,11 @@ export async function action({ request, params }: ActionFunctionArgs) { // This assignment of the same name is a special thing. We don't remember the name right now. // It's not necessary, but it is possible. + + /** This function takes a CSV string and an address + * and returns date and weather data, + * and geolocation information + */ const {convertedDatesTIWD, state_id, county_id} = await getConvertedDatesTIWD(uploadedTextFile, address) @@ -348,7 +353,7 @@ export default function SubmitAnalysis() { */} - +