From 1ab7eda504461cb8db843d60956791a3d7c0e8ef Mon Sep 17 00:00:00 2001 From: TBardini Date: Mon, 30 Sep 2024 13:30:56 -0400 Subject: [PATCH] Refactor single.tsx to transform usage_data from Map to object for better readability and validation. Updated components (AnalysisHeader, EnergyUseHistory, EnergyUseHistoryChart) to handle usage_data as an object instead of Map. Introduced mapToObject function in single.tsx to initiate this change and propagate it to other components. Enhanced Zod type definitions across the components for stronger type validation and clarity. These changes improve the organization and readability of the code related to types in the heat-stack/types directory. --- .../CaseSummaryComponents/AnalysisHeader.tsx | 64 +++++----- .../EnergyUseHistory.tsx | 56 +++++---- .../EnergyUseHistoryChart.tsx | 117 +++++++++--------- heat-stack/app/routes/_heat+/single.tsx | 48 ++++++- heat-stack/types/index.ts | 51 ++++++++ heat-stack/types/types.ts | 16 +++ 6 files changed, 239 insertions(+), 113 deletions(-) create mode 100644 heat-stack/types/types.ts diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx index 62f8f592..227d366f 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx @@ -1,9 +1,13 @@ -import { type z } from 'zod' -import { type HeatLoadAnalysisZod } from '#types/index' +import { type z } from 'zod'; +import { usageDataSchema, BillingRecordsSchema, SummaryOutputSchema } from '#types/index' -type HeatLoadAnalysisZod = z.infer -export function AnalysisHeader(props: { usage_data: any }) { +// type HeatLoadAnalysisZod = z.infer +type usageDataZod = z.infer +type BillingRecordsZod = z.infer +type SummaryOutputZod = z.infer + +export function AnalysisHeader({ usage_data }: { usage_data: usageDataZod}) { // Example usage_data // new Map([[ // "estimated_balance_point", @@ -34,27 +38,29 @@ export function AnalysisHeader(props: { usage_data: any }) { // 3312125.0171753373 // ]]) - const summaryOutputs = props.usage_data?.get('summary_output') + // Extract the summary_output from usage_data + const summaryOutputs: SummaryOutputZod | undefined = usage_data?.summary_output; // Calculate the number of billing periods included in Heating calculations - const heatingAnalysisTypeRecords = props.usage_data - ?.get('billing_records') - ?.filter((billingRecord: any) => billingRecord.get('analysis_type') == 1) + const heatingAnalysisTypeRecords = usage_data?.billing_records?.filter( + (billingRecord: BillingRecordsZod) => billingRecord.analysis_type === 1, + ); const recordsIncludedByDefault = heatingAnalysisTypeRecords?.filter( - (billingRecord: any) => - billingRecord.get('default_inclusion_by_calculation') == true && - billingRecord.get('inclusion_override') == false, - ).length + (billingRecord:BillingRecordsZod) => + billingRecord.default_inclusion_by_calculation === true && + billingRecord.inclusion_override === false, + ).length; const recordsIncludedByOverride = heatingAnalysisTypeRecords?.filter( - (billingRecord: any) => - billingRecord.get('default_inclusion_by_calculation') == false && - billingRecord.get('inclusion_override') == true, - ).length + (billingRecord: BillingRecordsZod) => + billingRecord.default_inclusion_by_calculation === false && + billingRecord.inclusion_override === true, + ).length; const numRecordsForHeatingCalculations = - recordsIncludedByDefault + recordsIncludedByOverride + (recordsIncludedByDefault || 0) + (recordsIncludedByOverride || 0); + return (
@@ -64,14 +70,14 @@ export function AnalysisHeader(props: { usage_data: any }) {
Average Indoor Temperature
- {summaryOutputs?.get('average_indoor_temperature')} °F -
{' '} + {summaryOutputs?.average_indoor_temperature} °F +

Balance Point Temperature
- {summaryOutputs?.get('estimated_balance_point')} °F -
{' '} + {summaryOutputs?.estimated_balance_point} °F +

@@ -83,8 +89,8 @@ export function AnalysisHeader(props: { usage_data: any }) { Daily non-heating Usage
{/* Rounding to two decimal places */} - {summaryOutputs?.get('other_fuel_usage').toFixed(2)} therms -
{' '} + {summaryOutputs?.other_fuel_usage?.toFixed(2)} therms +
@@ -93,19 +99,17 @@ export function AnalysisHeader(props: { usage_data: any }) {
{/* Rounding to two decimal places */} {( - summaryOutputs?.get('standard_deviation_of_heat_loss_rate') * - 100 - ).toFixed(2)}{' '} + summaryOutputs?.standard_deviation_of_heat_loss_rate * 100 + )?.toFixed(2)}{' '} % -
{' '} +

Whole-home UA
{/* Rounding to zero decimal places */} - {summaryOutputs?.get('whole_home_heat_loss_rate').toFixed(0)}{' '} - BTU/h-°F -
{' '} + {summaryOutputs?.whole_home_heat_loss_rate?.toFixed(0)} BTU/h-°F +
diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index ffd6781e..e9ccc417 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 { Suspense, /* lazy */ } from 'react' import { Button } from '#/app/components/ui/button.tsx' +import { type usageDataSchema } from '#/types/types.ts' import { AnalysisHeader } from './AnalysisHeader.tsx' import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' @@ -11,40 +11,46 @@ import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' // import { Input } from '#/app/components/ui/input.tsx' // import { Label } from '#/app/components/ui/label.tsx' - -// export function EnergyUseHistory(props: EnergyUseProps) { -export function EnergyUseHistory(props: { usage_data: any }) { - // console.log(`EnergyUseHistory:`, props.usage_data); +export function EnergyUseHistory({ + usage_data, +}: { + usage_data: usageDataSchema +}) { const titleClass = 'text-5xl font-extrabold tracking-wide mt-10' // const subtitleClass = 'text-2xl font-semibold text-zinc-950 mt-9' return ( -

Energy Use History

-
- Blah
'}> - - - - (Get example file here) - -
- {props.usage_data && } - {props.usage_data && } + + + + + + Get example file here + + + {usage_data && ( + <> + + + + )} ) } - - // const file = event.target.files?.[0] // if (file) { diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx index 830ce6b7..4b825e62 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect } from "react" +import { useState, useEffect } from 'react' import { type z } from 'zod' +import { type usageDataSchema, BillingRecordsSchema } from '#/types/types.ts' import { NaturalGasUsageData, type NaturalGasBillRecord as NaturalGasBillRecordZod, @@ -57,31 +58,35 @@ import { tr } from '@faker-js/faker' // naturalGasBillRecord04, // ] -export function EnergyUseHistoryChart(props: { usage_data: Map }) { - const [billingRecords, setBillingRecords] = useState[]>([]) +export function EnergyUseHistoryChart({ usage_data }: { usage_data: usageDataSchema }) { + const [billingRecords, setBillingRecords] = useState([]) useEffect(() => { - const records = props.usage_data?.get('billing_records') || [] - setBillingRecords(records as Map[]) - }, [props.usage_data]) - - const handleOverrideCheckboxChange = (index: number) => { - setBillingRecords((prevRecords) => { - const newRecords = [...prevRecords] - const periodMap = newRecords[index] - - if (periodMap instanceof Map) { - const currentOverride = periodMap.get('inclusion_override') - periodMap.set('inclusion_override', !currentOverride) - newRecords[index] = periodMap - } - - return newRecords - }) - } + if (usage_data?.billing_records) { + // Process the billing records directly without converting from Map + setBillingRecords(usage_data.billing_records) + } + }, [usage_data]) + + const handleOverrideCheckboxChange = (index: number) => { + setBillingRecords((prevRecords) => { + const newRecords = [...prevRecords] + const period = newRecords[index] + + if (period) { + const currentOverride = period.inclusion_override + // Toggle 'inclusion_override' + period.inclusion_override = !currentOverride + + newRecords[index] = { ...period } + } + + return newRecords + }) + } return ( - +
# @@ -97,16 +102,16 @@ export function EnergyUseHistoryChart(props: { usage_data: Map }) { - {billingRecords.map((period: Map, index: number) => { - const startDate = new Date(period.get('period_start_date')) - const endDate = new Date(period.get('period_end_date')) + {billingRecords.map((period, index) => { + const startDate = new Date(period.period_start_date) + const endDate = new Date(period.period_end_date) // Calculate days in period const timeInPeriod = endDate.getTime() - startDate.getTime() const daysInPeriod = Math.round(timeInPeriod / (1000 * 3600 * 24)) // Set Analysis Type image and checkbox setting - const analysisType = period.get('analysis_type') + const analysisType = period.analysis_type let analysisType_Image = undefined let overrideCheckboxDisabled = false @@ -125,39 +130,39 @@ export function EnergyUseHistoryChart(props: { usage_data: Map }) { } // Adjust inclusion for user input - let calculatedInclusion = period.get('default_inclusion_by_calculation') - if (period.get('inclusion_override')) { + let calculatedInclusion = period.default_inclusion_by_calculation + if (period.inclusion_override) { calculatedInclusion = !calculatedInclusion } const variant = calculatedInclusion ? 'included' : 'excluded' return ( - - {index + 1} - - Analysis Type - - {startDate.toLocaleDateString()} - {endDate.toLocaleDateString()} - {daysInPeriod} - {period.get('usage')} - - {period.get('whole_home_heat_loss_rate') - ? period.get('whole_home_heat_loss_rate').toFixed(0) - : '-'} - - - handleOverrideCheckboxChange(index)} - /> - - - ) - })} - -
- ) -} \ No newline at end of file + + {index + 1} + + Analysis Type + + {startDate.toLocaleDateString()} + {endDate.toLocaleDateString()} + {daysInPeriod} + {period.usage} + + {period.whole_home_heat_loss_rate + ? period.whole_home_heat_loss_rate.toFixed(0) + : '-'} + + + handleOverrideCheckboxChange(index)} + /> + + + ) + })} + + + ) +} diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index ce9fc1ca..24625cb5 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -453,10 +453,38 @@ function reviver(key: any, value: any) { return value; } +function mapToObject(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] = mapToObject(value); // Recursively process nested Maps + } + return obj; + } + + // Handle case where input is an array + if (Array.isArray(input)) { + return input.map(mapToObject); // 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(); // console.log(`location:`, location); // `.state` is `null` const lastResult = useActionData() + /* @ts-ignore */ // console.log("lastResult (all Rules Engine data)", lastResult !== undefined ? JSON.parse(lastResult.data, reviver): undefined) @@ -503,12 +531,28 @@ export default function Inputs() { } let usage_data = null; + let modifiedLastResult = null; let show_usage_data = lastResult !== undefined; + console.log('lastResult', lastResult) - if ( show_usage_data && hasDataProperty(lastResult)) { - usage_data = JSON.parse(lastResult?.data, reviver); + + // 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 + modifiedLastResult = mapToObject(parsedData); + usage_data = modifiedLastResult; // Get the relevant part of the transformed result + + } catch (error) { + console.error('Error parsing lastResult data:', error); + } } + console.log('usage_data', usage_data); + 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/types/index.ts b/heat-stack/types/index.ts index 220cc6a1..8202222d 100644 --- a/heat-stack/types/index.ts +++ b/heat-stack/types/index.ts @@ -104,3 +104,54 @@ export const OilPropaneBillRecord = z.object({ gallons: z.number(), inclusionOverride: z.enum(['Include', 'Do not include', 'Include in other analysis']), }); + +// Define the schema for balance records +export const balancePointGraphRecordSchema = z.object({ + balance_point: z.number(), + heat_loss_rate: z.number(), + change_in_heat_loss_rate: z.number(), + percent_change_in_heat_loss_rate: z.number(), + standard_deviation: z.number(), +}) + +// Define the schema for the balance point graph +export const balancePointGraphSchema = z.object({ + records: z.array(balancePointGraphRecordSchema), +}) + +// Define the schema for the 'summary_output' key +export const summaryOutputSchema = z.object({ + estimated_balance_point: z.number().optional().default(0), + other_fuel_usage: z.number().optional().default(0), + average_indoor_temperature: z.number().optional().default(0), + difference_between_ti_and_tbp: z.number().optional().default(0), + design_temperature: z.number().optional().default(0), + whole_home_heat_loss_rate: z.number().optional().default(0), + standard_deviation_of_heat_loss_rate: z.number().optional().default(0), + average_heat_load: z.number().optional().default(0), + maximum_heat_load: z.number().optional().default(0), +}); + + +// Define the schema for billing records +export const billingRecordSchema = z.object({ + period_start_date: z.string().default(''), + period_end_date: z.string().default(''), + usage: z.number().default(0), + analysis_type_override: z.any().nullable(), + inclusion_override: z.boolean().default(false), + analysis_type: z.number().default(0), + default_inclusion_by_calculation: z.boolean().default(false), + eliminated_as_outlier: z.boolean().default(false), + whole_home_heat_loss_rate: z.number().optional().default(0), +}); + +// Define the schema for 'billing_records' key +export const billingRecordsSchema = z.array(billingRecordSchema); + +// Define the schema for the 'usage_data' key +export const usageDataSchema = z.object({ + summary_output: summaryOutputSchema, + balance_point_graph: balancePointGraphSchema, + billing_records: billingRecordsSchema, +}) diff --git a/heat-stack/types/types.ts b/heat-stack/types/types.ts new file mode 100644 index 00000000..baa10abc --- /dev/null +++ b/heat-stack/types/types.ts @@ -0,0 +1,16 @@ +import { type z } from 'zod' +import { + type balancePointGraphRecordSchema, + type balancePointGraphSchema, + type summaryOutputSchema, + type billingRecordSchema, + type billingRecordsSchema, + type usageDataSchema, +} from './index.ts' + +export type BalancePointGraphRecordSchema = z.infer; +export type BalancePointGraphSchema = z.infer; +export type SummaryOutputSchema = z.infer; +export type BillingRecordSchema = z.infer; +export type BillingRecordsSchema = z.infer; +export type usageDataSchema = z.infer; \ No newline at end of file