From 5868bdb156fa1c30c6301494e33888c54da56785 Mon Sep 17 00:00:00 2001 From: Brett Edwards Date: Fri, 15 Mar 2024 15:26:22 -0700 Subject: [PATCH] Morecast Draft Forecast Save (#3469) - Saves draft forecast on every edit - Add reset button to clear drafts and reset cells - Closes #2995 --- .../components/GridComponentRenderer.tsx | 29 ++----- .../components/ResetForecastButton.tsx | 52 ++++++++++++ .../moreCast2/components/TabbedDataGrid.tsx | 61 +++++++++---- .../components/resetForecastButton.test.tsx | 71 ++++++++++++++++ .../features/moreCast2/forecastDraft.test.ts | 75 ++++++++++++++++ web/src/features/moreCast2/forecastDraft.ts | 85 +++++++++++++++++++ web/src/features/moreCast2/interfaces.ts | 5 ++ .../features/moreCast2/slices/dataSlice.ts | 17 +++- web/src/features/moreCast2/util.test.ts | 39 ++++++++- web/src/features/moreCast2/util.ts | 63 ++++++++++++-- web/src/utils/date.ts | 4 + 11 files changed, 453 insertions(+), 48 deletions(-) create mode 100644 web/src/features/moreCast2/components/ResetForecastButton.tsx create mode 100644 web/src/features/moreCast2/components/resetForecastButton.test.tsx create mode 100644 web/src/features/moreCast2/forecastDraft.test.ts create mode 100644 web/src/features/moreCast2/forecastDraft.ts diff --git a/web/src/features/moreCast2/components/GridComponentRenderer.tsx b/web/src/features/moreCast2/components/GridComponentRenderer.tsx index 644836bfa..9593b2260 100644 --- a/web/src/features/moreCast2/components/GridComponentRenderer.tsx +++ b/web/src/features/moreCast2/components/GridComponentRenderer.tsx @@ -8,8 +8,7 @@ import { GridValueSetterParams } from '@mui/x-data-grid' import { ModelChoice, WeatherDeterminate } from 'api/moreCast2API' -import { createWeatherModelLabel, isPreviousToToday } from 'features/moreCast2/util' -import { MoreCast2Row } from 'features/moreCast2/interfaces' +import { createWeatherModelLabel, isBeforeToday, rowContainsActual } from 'features/moreCast2/util' import { GC_HEADER, PRECIP_HEADER, @@ -57,18 +56,6 @@ export class GridComponentRenderer { return actualField } - public rowContainsActual = (row: MoreCast2Row): boolean => { - for (const key in row) { - if (key.includes(WeatherDeterminate.ACTUAL)) { - const value = row[key as keyof MoreCast2Row] - if (typeof value === 'number' && !isNaN(value)) { - return true - } - } - } - return false - } - public valueGetter = ( params: Pick, precision: number, @@ -89,23 +76,23 @@ export class GridComponentRenderer { // The 'Actual' column will show N/R for Not Reporting, instead of N/A const noDataField = headerName === WeatherDeterminate.ACTUAL ? NOT_REPORTING : NOT_AVAILABLE - const isPreviousDate = isPreviousToToday(params.row['forDate']) + const isPreviousDate = isBeforeToday(params.row['forDate']) const isForecastColumn = this.isForecastColumn(headerName) - const rowContainsActual = this.rowContainsActual(params.row) + const containsActual = rowContainsActual(params.row) // If a cell has no value, belongs to a Forecast column, is a future forDate, and the row doesn't contain any Actuals from today, // we can leave it blank, so it's obvious that it can have a value entered into it. - if (isNaN(value) && !isPreviousDate && isForecastColumn && !rowContainsActual) { + if (isNaN(value) && !isPreviousDate && isForecastColumn && !containsActual) { return '' } else return isNaN(value) ? noDataField : Number(value).toFixed(precision) } public renderForecastCellWith = (params: Pick, field: string) => { // If a single cell in a row contains an Actual, no Forecast will be entered into the row anymore, so we can disable the whole row. - const isActual = this.rowContainsActual(params.row) + const isActual = rowContainsActual(params.row) // We can disable a cell if an Actual exists or the forDate is before today. // Both forDate and today are currently in the system's time zone - const isPreviousDate = isPreviousToToday(params.row['forDate']) + const isPreviousDate = isBeforeToday(params.row['forDate']) const isGrassField = field.includes('grass') const label = isGrassField || isPreviousDate ? '' : createWeatherModelLabel(params.row[field].choice) const formattedValue = parseFloat(params.formattedValue) @@ -150,10 +137,10 @@ export class GridComponentRenderer { public renderForecastSummaryCellWith = (params: Pick) => { // If a single cell in a row contains an Actual, no Forecast will be entered into the row anymore, so we can disable the whole row. - const isActual = this.rowContainsActual(params.row) + const isActual = rowContainsActual(params.row) // We can disable a cell if an Actual exists or the forDate is before today. // Both forDate and today are currently in the system's time zone - const isPreviousDate = isPreviousToToday(params.row['forDate']) + const isPreviousDate = isBeforeToday(params.row['forDate']) // The grass curing 'forecast' field and other weather parameter forecasts fields are rendered differently return diff --git a/web/src/features/moreCast2/components/ResetForecastButton.tsx b/web/src/features/moreCast2/components/ResetForecastButton.tsx new file mode 100644 index 000000000..cf68a739e --- /dev/null +++ b/web/src/features/moreCast2/components/ResetForecastButton.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { Button } from '@mui/material' +import { fillGrassCuringForecast } from 'features/moreCast2/util' +import { MoreCast2Row, PredictionItem } from 'features/moreCast2/interfaces' +import { ModelChoice, WeatherDeterminate } from 'api/moreCast2API' + +export interface ResetForecastButtonProps { + className?: string + enabled: boolean + label: string + onClick: () => void +} + +/** + * Reset forecast rows to their default state. Temp, RH, Wind Dir & Speed are cleared, + * Precip is set to 0, and GC is carried forward from last submitted value. + */ +export const resetForecastRows = (rows: MoreCast2Row[]) => { + const resetRows = rows.map(row => { + const rowToReset = { ...row } + Object.keys(rowToReset).forEach(key => { + if (key.includes(WeatherDeterminate.FORECAST)) { + const isPrecipField = key.includes('precip') + const field = rowToReset[key as keyof MoreCast2Row] as PredictionItem + // Submitted forecasts have a ModelChoice.FORECAST, we don't want to reset those + if (field.choice != ModelChoice.FORECAST && !isNaN(field.value)) { + field.value = isPrecipField ? 0 : NaN + field.choice = '' + } + } + }) + return rowToReset + }) + fillGrassCuringForecast(resetRows) + return resetRows +} + +const ResetForecastButton = ({ className, enabled, label, onClick }: ResetForecastButtonProps) => { + return ( + + ) +} + +export default React.memo(ResetForecastButton) diff --git a/web/src/features/moreCast2/components/TabbedDataGrid.tsx b/web/src/features/moreCast2/components/TabbedDataGrid.tsx index 6d93f151a..0d1a6cbf4 100644 --- a/web/src/features/moreCast2/components/TabbedDataGrid.tsx +++ b/web/src/features/moreCast2/components/TabbedDataGrid.tsx @@ -1,4 +1,4 @@ -import { AlertColor, List, Stack } from '@mui/material' +import { AlertColor, Grid, List, Stack, Typography } from '@mui/material' import { styled } from '@mui/material/styles' import { GridCellParams, @@ -40,7 +40,10 @@ import { AppDispatch } from 'app/store' import { deepClone } from '@mui/x-data-grid/utils/utils' import { filterAllVisibleRowsForSimulation } from 'features/moreCast2/rowFilters' import { mapForecastChoiceLabels } from 'features/moreCast2/util' -import { MoreCastParams } from 'app/theme' +import { MoreCastParams, theme } from 'app/theme' +import { MorecastDraftForecast } from 'features/moreCast2/forecastDraft' +import ResetForecastButton, { resetForecastRows } from 'features/moreCast2/components/ResetForecastButton' +import { getDateTimeNowPST } from 'utils/date' export const Root = styled('div')({ display: 'flex', @@ -48,17 +51,14 @@ export const Root = styled('div')({ flexDirection: 'column' }) -export const SaveButton = styled(SaveForecastButton)(({ theme }) => ({ - position: 'absolute', - right: theme.spacing(2) -})) - const FORECAST_ERROR_MESSAGE = 'The forecast was not saved; an unexpected error occurred.' const FORECAST_SAVED_MESSAGE = 'Forecast was successfully saved and sent to Wildfire One.' const FORECAST_WARN_MESSAGE = 'Forecast not submitted. A forecast can only contain N/A values for the Wind Direction.' const SHOW_HIDE_COLUMNS_LOCAL_STORAGE_KEY = 'showHideColumnsModel' +const storedDraftForecast = new MorecastDraftForecast(localStorage) + interface TabbedDataGridProps { morecast2Rows: MoreCast2Row[] fromTo: DateRange @@ -424,6 +424,7 @@ const TabbedDataGrid = ({ morecast2Rows, fromTo, setFromTo }: TabbedDataGridProp setSnackbarMessage(FORECAST_SAVED_MESSAGE) setSnackbarSeverity('success') setSnackbarOpen(true) + storedDraftForecast.deleteRowsFromStoredDraft(rowsToSave, getDateTimeNowPST()) } else { setSnackbarMessage(result.errorMessage ?? FORECAST_ERROR_MESSAGE) setSnackbarSeverity('error') @@ -436,6 +437,12 @@ const TabbedDataGrid = ({ morecast2Rows, fromTo, setFromTo }: TabbedDataGridProp } } + const handleResetClick = () => { + const resetRows = resetForecastRows(allRows) + dispatch(storeUserEditedRows(resetRows)) + storedDraftForecast.clearDraftForecasts() + } + // Checks if the displayed rows includes non-Actual rows const hasForecastRow = () => { return visibleRows.filter(isForecastRowPredicate).length > 0 @@ -455,17 +462,35 @@ const TabbedDataGrid = ({ morecast2Rows, fromTo, setFromTo }: TabbedDataGridProp return ( - - + + + + + + + {storedDraftForecast.getLastSavedDraftDateTime() && ( + + Draft saved {storedDraftForecast.getLastSavedDraftDateTime()} + + )} + + + + + { + it('should render the button as enabled', () => { + const { getByTestId } = render( + + undefined} /> + + ) + + const resetForecastButton = getByTestId('reset-forecast-button') + expect(resetForecastButton).toBeInTheDocument() + expect(resetForecastButton).toBeEnabled() + }) + it('should render the button as disabled', () => { + const { getByTestId } = render( + + undefined} /> + + ) + + const manageStationsButton = getByTestId('reset-forecast-button') + expect(manageStationsButton).toBeInTheDocument() + expect(manageStationsButton).toBeDisabled() + }) + it('should reset the forecast rows to their initial load state', () => { + const mockRowData = [ + buildValidForecastRow(111, TEST_DATE.plus({ days: 1 }), 'MANUAL'), + buildValidForecastRow(222, TEST_DATE.plus({ days: 1 }), 'MANUAL'), + buildValidActualRow(222, TEST_DATE.minus({ days: 1 })) + ] + + const resetRows = resetForecastRows(mockRowData) + + expect(resetRows[0].tempForecast?.value).toBe(NaN) + expect(resetRows[0].rhForecast?.value).toBe(NaN) + expect(resetRows[0].windSpeedForecast?.value).toBe(NaN) + expect(resetRows[0].precipForecast?.value).toBe(0) + }) + it('should not reset rows with submitted forecasts or actuals', () => { + const mockRowData = [ + buildValidForecastRow(111, TEST_DATE.plus({ days: 1 }), 'FORECAST'), + buildValidForecastRow(222, TEST_DATE.plus({ days: 1 }), 'FORECAST'), + buildValidActualRow(222, TEST_DATE.minus({ days: 1 })) + ] + + const resetRows = resetForecastRows(mockRowData) + + expect(resetRows).toEqual(mockRowData) + }) + it('should call the reset click handler when clicked', async () => { + const handleResetClickMock = jest.fn() + const { getByTestId } = render( + + + + ) + const resetForecastButton = getByTestId('reset-forecast-button') + userEvent.click(resetForecastButton) + await waitFor(() => expect(handleResetClickMock).toHaveBeenCalledTimes(1)) + }) +}) diff --git a/web/src/features/moreCast2/forecastDraft.test.ts b/web/src/features/moreCast2/forecastDraft.test.ts new file mode 100644 index 000000000..d27295be5 --- /dev/null +++ b/web/src/features/moreCast2/forecastDraft.test.ts @@ -0,0 +1,75 @@ +import { MorecastDraftForecast } from 'features/moreCast2/forecastDraft' +import { DraftMorecast2Rows } from 'features/moreCast2/interfaces' +import { buildValidActualRow, buildValidForecastRow } from 'features/moreCast2/rowFilters.test' +import { DateTime } from 'luxon' +import * as DateUtils from 'utils/date' + +const TEST_DATE = DateTime.fromISO('2024-01-01T00:00:00.000-08:00') + +describe('MorecastDraftForecast', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + + const localStorageMock: Storage = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + length: 0, + clear: jest.fn(), + key: jest.fn() + } + + const draftForecast = new MorecastDraftForecast(localStorageMock) + + const mockRowData = [ + buildValidForecastRow(111, TEST_DATE.plus({ days: 1 })), + buildValidForecastRow(111, TEST_DATE.plus({ days: 2 })), + buildValidForecastRow(222, TEST_DATE.plus({ days: 1 })), + buildValidForecastRow(222, TEST_DATE.plus({ days: 2 })), + buildValidActualRow(222, TEST_DATE.minus({ days: 1 })), + buildValidActualRow(222, TEST_DATE.minus({ days: 2 })), + buildValidForecastRow(111, TEST_DATE.minus({ days: 1 })) + ] + + it('should only store current forecast rows', () => { + jest.spyOn(DateUtils, 'getDateTimeNowPST').mockReturnValue(TEST_DATE) + const toBeStored: DraftMorecast2Rows = { rows: mockRowData.slice(0, 4), lastEdited: TEST_DATE } + const setSpy = jest.spyOn(localStorageMock, 'setItem') + + draftForecast.updateStoredDraftForecasts(mockRowData, TEST_DATE) + + expect(setSpy).toHaveBeenCalledWith(draftForecast.STORAGE_KEY, JSON.stringify(toBeStored)) + }) + it('should call getItem upon retrieval of a forecast', () => { + const getSpy = jest.spyOn(localStorageMock, 'getItem') + draftForecast.getStoredDraftForecasts() + + expect(getSpy).toHaveBeenCalledWith(draftForecast.STORAGE_KEY) + }) + it('should delete saved rows from storage', () => { + const storedDraft: DraftMorecast2Rows = { rows: mockRowData.slice(0, 4), lastEdited: TEST_DATE } + const toBeStored: DraftMorecast2Rows = { rows: mockRowData.slice(2, 4), lastEdited: TEST_DATE } + const savedRows = mockRowData.slice(0, 2) + + jest.spyOn(localStorageMock, 'getItem').mockReturnValue(JSON.stringify(storedDraft)) + const setSpy = jest.spyOn(localStorageMock, 'setItem') + + draftForecast.deleteRowsFromStoredDraft(savedRows, TEST_DATE) + + expect(setSpy).toHaveBeenCalledWith(draftForecast.STORAGE_KEY, JSON.stringify(toBeStored)) + }) + it('should return true if a draft forecast is stored', () => { + const storedDraft: DraftMorecast2Rows = { rows: mockRowData.slice(0, 4), lastEdited: TEST_DATE } + jest.spyOn(localStorageMock, 'getItem').mockReturnValue(JSON.stringify(storedDraft)) + + const draftStored = draftForecast.hasDraftForecastStored() + expect(draftStored).toBe(true) + }) + it('should return false if a draft forecast is not stored', () => { + jest.spyOn(localStorageMock, 'getItem').mockReturnValue('') + + const draftStored = draftForecast.hasDraftForecastStored() + expect(draftStored).toBe(false) + }) +}) diff --git a/web/src/features/moreCast2/forecastDraft.ts b/web/src/features/moreCast2/forecastDraft.ts new file mode 100644 index 000000000..d84b23a33 --- /dev/null +++ b/web/src/features/moreCast2/forecastDraft.ts @@ -0,0 +1,85 @@ +import { DraftMorecast2Rows, MoreCast2ForecastRow, MoreCast2Row } from 'features/moreCast2/interfaces' +import { getRowsMap, isForecastRow } from 'features/moreCast2/util' +import { DateTime } from 'luxon' + +export class MorecastDraftForecast { + public readonly STORAGE_KEY = 'morecastForecastDraft' + private readonly localStorage: Storage + + constructor(localStorage: Storage) { + this.localStorage = localStorage + } + + public getStoredDraftForecasts = (): DraftMorecast2Rows | undefined => { + const storedDraftString = this.localStorage.getItem(this.STORAGE_KEY) + + if (storedDraftString) { + const storedDraft = JSON.parse(storedDraftString) + storedDraft.lastEdited = storedDraft.lastEdited ? DateTime.fromISO(storedDraft.lastEdited) : undefined + return storedDraft + } + } + + private storeDraftForecasts = (forecastDraft: DraftMorecast2Rows) => { + this.localStorage.setItem( + this.STORAGE_KEY, + JSON.stringify({ rows: forecastDraft.rows, lastEdited: forecastDraft.lastEdited }) + ) + } + + public clearDraftForecasts = () => { + this.localStorage.removeItem(this.STORAGE_KEY) + } + + public createStoredForecast = (rowsToStore: MoreCast2Row[], editDateTime: DateTime): void => { + const forecastRows = rowsToStore.filter(row => isForecastRow(row)) + const forecastDraft: DraftMorecast2Rows = { rows: forecastRows, lastEdited: editDateTime } + + this.storeDraftForecasts(forecastDraft) + } + + public updateStoredDraftForecasts = (rowsToStore: MoreCast2Row[], editDateTime: DateTime) => { + const storedForecastsToUpdate = this.getStoredDraftForecasts() + if (!storedForecastsToUpdate) { + this.createStoredForecast(rowsToStore, editDateTime) + } else { + const storedRowsMap = getRowsMap(storedForecastsToUpdate.rows) + + rowsToStore.forEach(row => { + storedRowsMap.set(row.id, row) + }) + // we only need to store rows that are 'Forecast' rows + storedForecastsToUpdate.rows = Array.from(storedRowsMap.values()).filter(row => { + return isForecastRow(row) + }) + storedForecastsToUpdate.lastEdited = editDateTime + + this.storeDraftForecasts(storedForecastsToUpdate) + } + } + + public deleteRowsFromStoredDraft = (savedRows: MoreCast2ForecastRow[] | MoreCast2Row[], editDateTime: DateTime) => { + const localStoredForecast = this.getStoredDraftForecasts() + if (localStoredForecast) { + const localStoredRows = getRowsMap(localStoredForecast.rows) + savedRows.forEach(row => { + localStoredRows.delete(row.id) + }) + localStoredForecast.rows = Array.from(localStoredRows.values()) + localStoredForecast.lastEdited = editDateTime + this.storeDraftForecasts(localStoredForecast) + } + } + + public hasDraftForecastStored = (): boolean => { + const localStoredForecast = this.getStoredDraftForecasts() + return !!localStoredForecast && localStoredForecast.rows.length > 0 + } + + public getLastSavedDraftDateTime = (): string | undefined => { + const storedDraftForecast = this.getStoredDraftForecasts() + if (storedDraftForecast) { + return storedDraftForecast.lastEdited.toFormat('MMMM dd, HH:mm') + } + } +} diff --git a/web/src/features/moreCast2/interfaces.ts b/web/src/features/moreCast2/interfaces.ts index 51c7fc81a..6da7aa302 100644 --- a/web/src/features/moreCast2/interfaces.ts +++ b/web/src/features/moreCast2/interfaces.ts @@ -143,3 +143,8 @@ export interface ForecastMorecast2Row extends BaseRow { windDirection?: PredictionItem windSpeed?: PredictionItem } + +export interface DraftMorecast2Rows { + rows: MoreCast2Row[] + lastEdited: DateTime +} diff --git a/web/src/features/moreCast2/slices/dataSlice.ts b/web/src/features/moreCast2/slices/dataSlice.ts index 93b0ab4e5..f5ebd8b42 100644 --- a/web/src/features/moreCast2/slices/dataSlice.ts +++ b/web/src/features/moreCast2/slices/dataSlice.ts @@ -12,13 +12,22 @@ import { fetchCalculatedIndices } from 'api/moreCast2API' import { AppThunk } from 'app/store' -import { createDateInterval, rowIDHasher, fillGrassCuringForecast, fillGrassCuringCWFIS } from 'features/moreCast2/util' +import { + createDateInterval, + rowIDHasher, + fillGrassCuringForecast, + fillGrassCuringCWFIS, + fillForecastsFromRows +} from 'features/moreCast2/util' import { DateTime } from 'luxon' import { logError } from 'utils/error' import { MoreCast2Row } from 'features/moreCast2/interfaces' import { groupBy, isEqual, isNull, isNumber, isUndefined } from 'lodash' import { StationGroupMember } from 'api/stationAPI' +import { MorecastDraftForecast } from 'features/moreCast2/forecastDraft' +import { getDateTimeNowPST } from 'utils/date' +const morecastDraftForecast = new MorecastDraftForecast(localStorage) interface State { loading: boolean error: string | null @@ -87,6 +96,7 @@ const dataSlice = createSlice({ } } state.userEditedRows = storedRows + morecastDraftForecast.updateStoredDraftForecasts(storedRows, getDateTimeNowPST()) } } }) @@ -369,6 +379,11 @@ export const createMoreCast2Rows = ( let newRows = fillGrassCuringForecast(rows) newRows = fillGrassCuringCWFIS(newRows) + const savedDraft = morecastDraftForecast.getStoredDraftForecasts() + if (savedDraft) { + newRows = fillForecastsFromRows(newRows, savedDraft.rows) + } + return newRows } diff --git a/web/src/features/moreCast2/util.test.ts b/web/src/features/moreCast2/util.test.ts index 64b291fb6..c057ee7a7 100644 --- a/web/src/features/moreCast2/util.test.ts +++ b/web/src/features/moreCast2/util.test.ts @@ -10,9 +10,11 @@ import { parseForecastsHelper, rowIDHasher, validActualPredicate, - validForecastPredicate + validForecastPredicate, + fillForecastsFromRows } from 'features/moreCast2/util' import { buildValidActualRow, buildValidForecastRow } from 'features/moreCast2/rowFilters.test' +import { createEmptyMoreCast2Row } from 'features/moreCast2/slices/dataSlice' const TEST_DATE = '2023-02-16T20:00:00+00:00' const TEST_DATE2 = '2023-02-17T20:00:00+00:00' @@ -308,3 +310,38 @@ describe('fillStationGrassCuringForward', () => { expect(forecast2A.grassCuringForecast!.value).toBe(70) }) }) +describe('fillRowsFromSavedDraft', () => { + it('should fill forecast rows from saved drafts', () => { + const tomorrow = DateTime.now().plus({ days: 1 }) + const stationCode = 1 + const id = rowIDHasher(stationCode, tomorrow) + const savedRows = buildValidForecastRow(stationCode, tomorrow, 'MANUAL') + const rowsToFill = createEmptyMoreCast2Row(id, stationCode, 'station', tomorrow, 1, 1) + + const filledRows = fillForecastsFromRows([rowsToFill], [savedRows]) + expect(filledRows[0].tempForecast?.value).toBe(2) + expect(filledRows[0].tempForecast?.choice).toBe(ModelChoice.MANUAL) + }) + it('should not fill rows if they contain actuals', () => { + const tomorrow = DateTime.now() + const stationCode = 1 + const id = rowIDHasher(stationCode, tomorrow) + const savedRows = buildValidActualRow(stationCode, tomorrow) + const rowsToFill = createEmptyMoreCast2Row(id, stationCode, 'station', tomorrow, 1, 1) + + const filledRows = fillForecastsFromRows([rowsToFill], [savedRows]) + expect(filledRows[0].tempForecast?.value).toBe(undefined) + expect(filledRows[0].tempForecast?.choice).toBe(undefined) + }) + it('should not fill rows if they are in the past', () => { + const tomorrow = DateTime.now().minus({ days: 2 }) + const stationCode = 1 + const id = rowIDHasher(stationCode, tomorrow) + const savedRows = buildValidActualRow(stationCode, tomorrow) + const rowsToFill = createEmptyMoreCast2Row(id, stationCode, 'station', tomorrow, 1, 1) + + const filledRows = fillForecastsFromRows([rowsToFill], [savedRows]) + expect(filledRows[0].tempForecast?.value).toBe(undefined) + expect(filledRows[0].tempForecast?.choice).toBe(undefined) + }) +}) diff --git a/web/src/features/moreCast2/util.ts b/web/src/features/moreCast2/util.ts index f51a056cd..d4f3d3dcd 100644 --- a/web/src/features/moreCast2/util.ts +++ b/web/src/features/moreCast2/util.ts @@ -1,8 +1,9 @@ import { DateTime, Interval } from 'luxon' -import { ModelChoice, MoreCast2ForecastRecord } from 'api/moreCast2API' +import { ModelChoice, MoreCast2ForecastRecord, WeatherDeterminate } from 'api/moreCast2API' import { MoreCast2ForecastRow, MoreCast2Row } from 'features/moreCast2/interfaces' import { StationGroupMember } from 'api/stationAPI' import { isUndefined } from 'lodash' +import { getDateTimeNowPST } from 'utils/date' export const parseForecastsHelper = ( forecasts: MoreCast2ForecastRecord[], @@ -102,6 +103,10 @@ export const validForecastPredicate = (row: MoreCast2Row) => !isUndefined(row.windSpeedForecast) && !isNaN(row.windSpeedForecast.value) +export const isForecastRow = (row: MoreCast2Row) => { + return !rowContainsActual(row) && !isBeforeToday(row.forDate) +} + export const mapForecastChoiceLabels = (newRows: MoreCast2Row[], storedRows: MoreCast2Row[]): MoreCast2Row[] => { const storedRowChoicesMap = new Map() @@ -123,6 +128,35 @@ export const mapForecastChoiceLabels = (newRows: MoreCast2Row[], storedRows: Mor return newRows } +export const fillForecastsFromRows = ( + rowsToFill: MoreCast2Row[], + savedRows: MoreCast2Row[] | undefined +): MoreCast2Row[] => { + if (savedRows) { + const savedRowsMap = getRowsMap(savedRows) + rowsToFill + .filter(row => isForecastRow(row)) + .map(forecastRow => { + const savedRow = savedRowsMap.get(forecastRow.id) + if (savedRow) { + forecastRow.tempForecast = savedRow.tempForecast + forecastRow.rhForecast = savedRow.rhForecast + forecastRow.windDirectionForecast = savedRow.windDirectionForecast + forecastRow.windSpeedForecast = savedRow.windSpeedForecast + forecastRow.precipForecast = savedRow.precipForecast + forecastRow.grassCuringForecast = savedRow.grassCuringForecast + forecastRow.ffmcCalcForecast = savedRow.ffmcCalcForecast + forecastRow.dmcCalcForecast = savedRow.dmcCalcForecast + forecastRow.dcCalcForecast = savedRow.dcCalcForecast + forecastRow.isiCalcForecast = savedRow.isiCalcForecast + forecastRow.buiCalcForecast = savedRow.buiCalcForecast + forecastRow.fwiCalcForecast = savedRow.fwiCalcForecast + } + }) + } + return rowsToFill +} + /** * Fills all stations grass curing values with the last entered value for each station * @param rows - MoreCast2Row[] @@ -202,16 +236,17 @@ export const fillGrassCuringForecast = (rows: MoreCast2Row[]): MoreCast2Row[] => * @returns MoreCast2Row[] */ export const fillStationGrassCuringForward = (editedRow: MoreCast2Row, allRows: MoreCast2Row[]) => { - const editedStation = editedRow.stationCode + const editedStationCode = editedRow.stationCode const editedDate = editedRow.forDate const newGrassCuringValue = editedRow.grassCuringForecast!.value + const stationRows = allRows.filter(row => row.stationCode === editedStationCode) - for (const row of allRows) { - if (row.stationCode === editedStation && row.forDate > editedDate) { + for (const row of stationRows) { + if (row.forDate > editedDate) { row.grassCuringForecast!.value = newGrassCuringValue } } - return allRows + return stationRows } /** @@ -219,8 +254,22 @@ export const fillStationGrassCuringForward = (editedRow: MoreCast2Row, allRows: * @param datetime * @returns boolean */ -export const isPreviousToToday = (datetime: DateTime): boolean => { - const today = DateTime.local().startOf('day') +export const isBeforeToday = (datetime: DateTime): boolean => { + const today = getDateTimeNowPST().startOf('day') return datetime < today } + +export const rowContainsActual = (row: MoreCast2Row): boolean => { + return Object.entries(row).some( + ([key, value]) => key.includes(WeatherDeterminate.ACTUAL) && typeof value === 'number' && !isNaN(value) + ) +} + +export const getRowsMap = (morecastRows: MoreCast2Row[]): Map => { + const morecastRowMap = new Map() + morecastRows.forEach((row: MoreCast2Row) => { + morecastRowMap.set(row.id, row) + }) + return morecastRowMap +} diff --git a/web/src/utils/date.ts b/web/src/utils/date.ts index c4042b073..8ec803b45 100644 --- a/web/src/utils/date.ts +++ b/web/src/utils/date.ts @@ -55,3 +55,7 @@ export const pstFormatter = (fromDate: DateTime): string => { return !isNull(pstFormattedDate) ? pstFormattedDate : '' } + +export const getDateTimeNowPST = (): DateTime => { + return DateTime.now().setZone(`UTC${PST_UTC_OFFSET}`) +}