From a2e8c338a7dca9d9ff2282788eee381028d12690 Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Mon, 9 Dec 2024 17:30:00 -0500 Subject: [PATCH 1/2] Add gas utility selector ## Links - https://app.asana.com/0/1208668890181682/1208886892124341 ## Description - Show the gas utility selector if the `/utilities` call returns a response with the `gas_utilities` key present. This is the way I came up with for the API to indicate whether the choice of gas utility matters: key is present means yes, key is absent means no. If the key is present but there are no gas utilities, we still show the selector to offer the choice of "no gas service" vs. "other", which is significant in the Mass Save case. - The selector always has three items at the bottom, after any real utilities: "Delivered propane or fuel oil", "No gas service", and "Other". The former two behave exactly the same behind the scenes, but are intended to reduce confusion: users may not know that we mean natural/methane gas pipeline service when we say "gas service" or "gas utility". - I had to add the `previousResponse` field to FetchState's loading state, so that the gas utility selector can stay visible and populated if you enter a new zip code. Otherwise, the selector would disappear while the `/utilities` call was pending, and it would look janky. As implemented, when you enter a zip code in MA and the `/utilities` call completes, the gas utility selector just pops in. This is pretty disruptive to the rest of the form in the two-column view, because all the other fields reposition --- they're in the same tabbing order, but ones that were on the left are now on the right and vice versa. It's not so bad in one-column view. I think this is tolerable for now, especially since the selector will only appear for MA zip codes. But it may be worth a rethink. ## Test Plan Add `api-host` attribute to the element on index.html to hit my local API server (so gas utilities are returned). Enter zip codes in MA to see the gas selector pop in, and make sure the options make sense. E.g. 02130 (Boston; has Eversource) and 01011 (middle of nowhere; has no gas). Enter one MA zip code, then another; make sure the gas selector stays visible throughout. Then enter a non-MA zip code; make sure the selector disappears once the new location's electric utilities are populated. Submit the form with the various gas options selected. Use network inspector to check the `gas_utility` param sent to the API. (Currently the API's response won't change based on it.) It should be `none` if "delivered fuels" or "no gas service" is selected, absent if "other" is selected, and a utility ID if a real gas utility is selected. --- src/api/calculator-types-v1.ts | 1 + src/api/fetch-state.ts | 5 ++ src/state-calculator-form.tsx | 89 ++++++++++++++++++++++++++++++++-- src/state-calculator.tsx | 3 ++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/api/calculator-types-v1.ts b/src/api/calculator-types-v1.ts index 6df4046..83dde46 100644 --- a/src/api/calculator-types-v1.ts +++ b/src/api/calculator-types-v1.ts @@ -98,6 +98,7 @@ export interface APIUtilityMap { export interface APIUtilitiesResponse { location: APILocation; utilities: APIUtilityMap; + gas_utilities?: APIUtilityMap; } export interface APIResponse { diff --git a/src/api/fetch-state.ts b/src/api/fetch-state.ts index 43225e8..4727bdf 100644 --- a/src/api/fetch-state.ts +++ b/src/api/fetch-state.ts @@ -4,6 +4,11 @@ export type FetchState = } | { state: 'loading'; + /** + * If the previous state was loading, this may or may not contain the + * response value from that state. + */ + previousResponse?: T; } | { state: 'complete'; diff --git a/src/state-calculator-form.tsx b/src/state-calculator-form.tsx index 2e59ccd..79f44b5 100644 --- a/src/state-calculator-form.tsx +++ b/src/state-calculator-form.tsx @@ -38,11 +38,15 @@ const HOUSEHOLD_SIZE_OPTIONS: ( value: count, })); +const NO_GAS_UTILITY_ID = 'no-gas'; +const DELIVERED_FUEL_UTILITY_ID = 'delivered'; const OTHER_UTILITY_ID = 'other'; -const renderUtilityField = ( +const renderUtilityFields = ( utility: string, setUtility: (newValue: string) => void, + gasUtility: string, + setGasUtility: (newValue: string) => void, utilitiesFetch: FetchState, msg: MsgFn, ) => { @@ -67,7 +71,7 @@ const renderUtilityField = ( : msg('We don’t have utility data for your area yet.') : utilitiesFetch.message; - return ( + const electricSelector = ( + ); + } + + return ( + <> + {electricSelector} + {gasSelector} + + ); }; const renderEmailField = ( @@ -144,6 +195,7 @@ export type FormValues = { householdSize: string; taxFiling: FilingStatus; utility?: string; + gasUtility?: string; email?: string; }; @@ -174,6 +226,7 @@ export const CalculatorForm: FC<{ ); const [taxFiling, setTaxFiling] = useState(initialValues.taxFiling); const [utility, setUtility] = useState(initialValues.utility ?? ''); + const [gasUtility, setGasUtility] = useState(initialValues.gasUtility ?? ''); const [email, setEmail] = useState(initialValues.email ?? ''); const [utilitiesFetchState, setUtilitiesFetchState] = useState< @@ -187,13 +240,17 @@ export const CalculatorForm: FC<{ return; } - setUtilitiesFetchState({ state: 'loading' }); + setUtilitiesFetchState(prev => ({ + state: 'loading', + previousResponse: prev.state === 'complete' ? prev.response : undefined, + })); utilityFetcher(zip) .then(response => { // If our "state" attribute is set, enforce that the entered location is // in that state. if (stateId && stateId !== response.location.state) { setUtility(''); + setGasUtility(''); // Throw to put the task into the ERROR state for rendering. const stateCodeOrName = STATES[stateId]?.name(msg) ?? stateId; @@ -214,6 +271,16 @@ export const CalculatorForm: FC<{ } else { setUtility(OTHER_UTILITY_ID); } + + const gasKeys = Object.keys(response.gas_utilities || {}); + if (gasKeys.length > 0) { + if (!gasKeys.includes(gasUtility)) { + setGasUtility(gasKeys[0]); + } + } else { + // If there are no gas utilities, choose the "no gas service" option. + setGasUtility(NO_GAS_UTILITY_ID); + } }) .catch(exc => setUtilitiesFetchState({ state: 'error', message: exc.message }), @@ -231,6 +298,13 @@ export const CalculatorForm: FC<{ householdSize, taxFiling, utility: utility !== OTHER_UTILITY_ID ? utility : '', + gasUtility: + gasUtility === OTHER_UTILITY_ID + ? '' + : gasUtility === DELIVERED_FUEL_UTILITY_ID || + gasUtility === NO_GAS_UTILITY_ID + ? 'none' + : '', email, }); }} @@ -273,7 +347,14 @@ export const CalculatorForm: FC<{ onChange={event => setZip(event.currentTarget.value)} /> - {renderUtilityField(utility, setUtility, utilitiesFetchState, msg)} + {renderUtilityFields( + utility, + setUtility, + gasUtility, + setGasUtility, + utilitiesFetchState, + msg, + )}
{ project.items.forEach(item => { query.append('items', item); From c973076e50ed5e5f9e0cd949195f5f8d5c85d03e Mon Sep 17 00:00:00 2001 From: Owen Yamauchi Date: Mon, 9 Dec 2024 18:58:17 -0500 Subject: [PATCH 2/2] extract strings, add a couple easy translations --- src/i18n/strings/es.ts | 4 ++++ translations/es.xlf | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/i18n/strings/es.ts b/src/i18n/strings/es.ts index fb9a593..2241b62 100644 --- a/src/i18n/strings/es.ts +++ b/src/i18n/strings/es.ts @@ -137,6 +137,7 @@ export const templates = { sa9e16de0ec0154b3: `aire acondicionado central`, saa36bebd89cbd670: `Alaska`, sab07cf2bfae8483f: `Política de privacidad`, + sab68e846cf3e7e02: `Empresa de gas`, sab856460d5aed19e: `equipo eléctrico para exteriores`, sabd5662f3f0a76be: `Descuento por adelantado`, sad181d4343ef967f: `Wyoming`, @@ -170,6 +171,7 @@ export const templates = { sc2e0d466583b17f8: `aislamiento del entrepiso`, sc5b20cb72269bc4f: `Los propietarios y inquilinos califican para diferentes incentivos.`, sc9266b1b6ae1aad4: `Esperado en 2024-2025`, + sc967ff75e8a58273: `Selecciona la empresa a la que paga su factura de gas.`, sc991c5ecbb3023ef: `aislamiento térmico`, sc997cfdf24ba9b58: `Aún no tenemos datos sobre las empresas de servicios eléctricos en su área.`, sc9e494c8346b7cb5: `Otra`, @@ -204,4 +206,6 @@ export const templates = { sfa7338035e1ef173: `Alquilar o poseer`, sfc7214f623fe475d: `Selecciona la empresa a la que paga su factura de electricidad.`, sfe16afc784bb9d76: `Techo solar`, + sb2d056e8d2ea5bc5: `Delivered propane or fuel oil`, + s52d768131ca697d4: `No gas service`, }; diff --git a/translations/es.xlf b/translations/es.xlf index cdb0017..4845ccd 100644 --- a/translations/es.xlf +++ b/translations/es.xlf @@ -786,6 +786,21 @@ privacy policy política de privacidad + + Delivered propane or fuel oil + + + No gas service + + + Choose the company you pay your gas bill to. + Selecciona la empresa a la que paga su factura de gas. + + + Gas Utility + Empresa de gas + as in utility company +