From 75e4f98ed986439ce0f31524dc785f6bf351bac0 Mon Sep 17 00:00:00 2001 From: vyvyvyThao Date: Sat, 16 Aug 2025 14:23:59 -0400 Subject: [PATCH 1/9] Push incomplete updates --- .../baseline/BaselineViewComponent.tsx | 75 +++++++++++++++++++ .../baseline/EditBaselineModalComponent.ts | 54 +++++++++++++ src/client/app/redux/api/baselineApi.ts | 58 ++++++++++++++ src/client/app/types/redux/baselines.ts | 12 +++ src/server/routes/baseline.js | 35 +++++++++ 5 files changed, 234 insertions(+) create mode 100644 src/client/app/components/baseline/BaselineViewComponent.tsx create mode 100644 src/client/app/components/baseline/EditBaselineModalComponent.ts create mode 100644 src/client/app/redux/api/baselineApi.ts create mode 100644 src/client/app/types/redux/baselines.ts diff --git a/src/client/app/components/baseline/BaselineViewComponent.tsx b/src/client/app/components/baseline/BaselineViewComponent.tsx new file mode 100644 index 0000000000..9e20408583 --- /dev/null +++ b/src/client/app/components/baseline/BaselineViewComponent.tsx @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button } from 'reactstrap'; +import { Baseline } from '../../types/redux/baselines'; +import { useAppSelector } from '../../redux/reduxHooks'; +import '../../styles/card-page.css'; +import { useTranslate } from '../../redux/componentHooks'; +import EditBaselineModalComponent from './EditBaselineModalComponent'; + + + +interface BaselineViewComponentProps { + baseline: Baseline; +} + +/** + * Defines the baseline info card + * @param props defined above + * @returns Baseline element + */ +export default function BaselineViewComponent(props: BaselineViewComponentProps) { + const translate = useTranslate(); + // Don't check if admin since only an admin is allow to route to this page. + + // Edit Modal Show + const [showEditModal, setShowEditModal] = useState(false); + + const handleShow = () => { + setShowEditModal(true); + }; + + const handleClose = () => { + setShowEditModal(false); + }; + + // // Create header from sourceId, destinationId identifiers + // const conversionIdentifier = String(unitDataById[props.conversion.sourceId]?.identifier + conversionArrow(props.conversion.bidirectional) + + // unitDataById[props.conversion.destinationId]?.identifier); + + + return ( +
+ {/*
+ {conversionIdentifier} +
*/} +
+ {props.baseline.baselineValue} +
+
+ {translate(`TrueFalseType.${props.baseline.isActive.toString()}`)} +
+
+ {/* Only show first 30 characters so card does not get too big. Should limit to one line */} + {props.baseline.note.slice(0, 29)} +
+
+ + {/* Creates a child ConversionModalEditComponent */} + +
+
+ ); +} \ No newline at end of file diff --git a/src/client/app/components/baseline/EditBaselineModalComponent.ts b/src/client/app/components/baseline/EditBaselineModalComponent.ts new file mode 100644 index 0000000000..870ef160e6 --- /dev/null +++ b/src/client/app/components/baseline/EditBaselineModalComponent.ts @@ -0,0 +1,54 @@ +import * as React from 'react'; +// Realize that * is already imported from react +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormGroup, FormFeedback, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { baselineApi, selectBaselinesDetails } from '../../redux/api/baselineApi'; +import { selectMeterDataById } from '../../redux/api/metersApi'; +// import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/reduxHooks'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { TrueFalseType } from '../../types/items'; +import { Baseline } from '../../types/redux/baselines'; +import { useTranslate } from '../../redux/componentHooks'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + +interface EditBaselineModalComponentProps { + show: boolean; + baseline: Baseline; + baselineIdentifier: string; + // passed in to handle opening the modal + handleShow: () => void; + // passed in to handle closing the modal + handleClose: () => void; +} + +/** + * Defines the edit baselin modal form + * @param props Props for the component + * @returns Baseline edit element + */ +export default function EditBaselineModalComponent(props: EditBaselineModalComponentProps) { + const translate = useTranslate(); + const [editBaseline] = baselineApi.useEditBaselineMutation(); + const [deleteBaseline] = baselineApi.useDeleteBaselineMutation(); + const meterDataById = useAppSelector(selectMeterDataById); + + // Set existing baseline values + const values = { ...props.baseline }; + + /* State */ + // Handlers for each type of input change + const [state, setState] = useState(values); + + const handleStringChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: e.target.value }); + }; + + const handleBooleanChange = (e: React.ChangeEvent) => { + setState({...state, [e.target.name]: JSON.parse(e.target.value) }); + }; +} \ No newline at end of file diff --git a/src/client/app/redux/api/baselineApi.ts b/src/client/app/redux/api/baselineApi.ts new file mode 100644 index 0000000000..9519538b70 --- /dev/null +++ b/src/client/app/redux/api/baselineApi.ts @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { createSelector, EntityState, createEntityAdapter } from '@reduxjs/toolkit'; +import { Baseline } from '../../types/redux/baselines'; +import { baseApi } from './baseApi'; +// export const unitsAdapter = createEntityAdapter({ +// selectId +// // sortComparer: (unitA, unitB) => unitA.identifier?.localeCompare(unitB.identifier, undefined, { sensitivity: 'accent' }) +// }); +// export const unitsInitialState = unitsAdapter.getInitialState(); +// export type UnitDataState = EntityState; + +export const baselineApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getBaselinesDetails: builder.query({ + query: () => 'api/baseline', + providesTags: ['Baselines'] + }), + addBaseline: builder.mutation({ + query: baseline => ({ + url: 'api/baseline/new', + method: 'POST', + body: baseline + }), + invalidatesTags: ['Baselines'] + }), + editBaseline: builder.mutation({ + query: baseline => ({ + url: 'api/baseline/edit', + method: 'POST', + body: baseline + }), + invalidatesTags: ['Baselines'] + }), + deleteBaseline: builder.mutation({ + query: unitId => ({ + url: 'api/baseline/delete', + method: 'POST', + body: { id: unitId } + }), + // You should not be able to delete a unit that is used in a meter or conversion + // so no invalidation for those. + invalidatesTags: ['Baselines'] + }) + }) +}); + +export const selectBaselinesQueryState = baselineApi.endpoints.getBaselinesDetails.select(); +export const selectBaselinesDetails= createSelector( + selectBaselinesQueryState, + ({ data: baselineData = [] }) => { + return baselineData; + } +); + +export const { selectEntities: selectBaselineById } = diff --git a/src/client/app/types/redux/baselines.ts b/src/client/app/types/redux/baselines.ts new file mode 100644 index 0000000000..688096773e --- /dev/null +++ b/src/client/app/types/redux/baselines.ts @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// export interface BaselineDataById extends Record { } <-- don't know yet if needed + +export interface Baseline { + baselineValue: number; + meterId: number; + isActive: boolean; // whether there's a baseline applied or not + note: string; +} diff --git a/src/server/routes/baseline.js b/src/server/routes/baseline.js index 07d85fe8f2..94e130c969 100644 --- a/src/server/routes/baseline.js +++ b/src/server/routes/baseline.js @@ -34,4 +34,39 @@ router.post('/new', async (req, res) => { log(`Error while adding baseline: ${err}`, 'error'); } }); +router.post('/edit', adminAuthMiddleware('edit baselines'), async (req, res) => { + const validConversion = { + type: 'object', + required: ['baselineValue', 'isActive'], + properties: { + baselineValue: { + type: 'number', + // Do not allow negatives for now + minimum: 0 + }, + isActive: { + type: 'boolean' + } + } + }; + + // not edited + const validatorResult = validate(req.body, validConversion); + if (!validatorResult.valid) { + log.warn(`Got request to edit conversions with invalid conversion data, errors: ${validatorResult.errors}`); + failure(res, 400, `Got request to edit conversions with invalid conversion data, errors: ${validatorResult.errors}`); + } else { + const conn = getConnection(); + try { + const updatedConversion = new Conversion(req.body.sourceId, req.body.destinationId, req.body.bidirectional, + req.body.slope, req.body.intercept, req.body.note); + await updatedConversion.update(conn); + } catch (err) { + log.error(`Error while editing conversion with error(s): ${err}`); + failure(res, 500, `Error while editing conversion with error(s): ${err}`); + } + success(res); + } +}); + module.exports = router; From 9050d7e1fb8edc8f668a5c9bde0f5360ca08eb17 Mon Sep 17 00:00:00 2001 From: Nhat Anh Nguyen Date: Tue, 26 Aug 2025 22:41:15 -0400 Subject: [PATCH 2/9] Upload new components and APIs --- .../baseline/BaselineViewComponent.tsx | 7 +- .../baseline/CreateBaselineModalComponent.tsx | 0 .../DeleteBaselineSegmentModalComponent.tsx | 85 +++++ .../baseline/EditBaselineModalComponent.ts | 54 ---- .../baseline/EditBaselineModalComponent.tsx | 295 ++++++++++++++++++ .../EditBaselineSegmentModalComponent.tsx | 201 ++++++++++++ .../SplitBaselineSegmentComponent.tsx | 199 ++++++++++++ src/client/app/redux/api/baseApi.ts | 4 +- src/client/app/redux/api/baselineApi.ts | 113 ++++++- .../app/redux/selectors/adminSelectors.ts | 45 ++- src/client/app/types/redux/baselines.ts | 22 ++ 11 files changed, 957 insertions(+), 68 deletions(-) create mode 100644 src/client/app/components/baseline/CreateBaselineModalComponent.tsx create mode 100644 src/client/app/components/baseline/DeleteBaselineSegmentModalComponent.tsx delete mode 100644 src/client/app/components/baseline/EditBaselineModalComponent.ts create mode 100644 src/client/app/components/baseline/EditBaselineModalComponent.tsx create mode 100644 src/client/app/components/baseline/EditBaselineSegmentModalComponent.tsx create mode 100644 src/client/app/components/baseline/SplitBaselineSegmentComponent.tsx diff --git a/src/client/app/components/baseline/BaselineViewComponent.tsx b/src/client/app/components/baseline/BaselineViewComponent.tsx index 9e20408583..133cabca69 100644 --- a/src/client/app/components/baseline/BaselineViewComponent.tsx +++ b/src/client/app/components/baseline/BaselineViewComponent.tsx @@ -7,7 +7,6 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import { Baseline } from '../../types/redux/baselines'; -import { useAppSelector } from '../../redux/reduxHooks'; import '../../styles/card-page.css'; import { useTranslate } from '../../redux/componentHooks'; import EditBaselineModalComponent from './EditBaselineModalComponent'; @@ -66,9 +65,9 @@ export default function BaselineViewComponent(props: BaselineViewComponentProps) + handleClose={handleClose} + baselineIdentifier={props.baseline.meterId.toString()} + handleShow={handleShow} /> ); diff --git a/src/client/app/components/baseline/CreateBaselineModalComponent.tsx b/src/client/app/components/baseline/CreateBaselineModalComponent.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/client/app/components/baseline/DeleteBaselineSegmentModalComponent.tsx b/src/client/app/components/baseline/DeleteBaselineSegmentModalComponent.tsx new file mode 100644 index 0000000000..f238cd4bd3 --- /dev/null +++ b/src/client/app/components/baseline/DeleteBaselineSegmentModalComponent.tsx @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { Button } from 'reactstrap'; +import { baselineSegmentsApi } from '../../redux/api/baselineApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { BaselineSegment } from '../../types/redux/baselines'; +import { showErrorNotification } from '../../utils/notifications'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; + +interface DeleteBaselineSegmentComponentProps { + /** + * The direction to delete the segment + */ + direction: 'earlier' | 'later'; + + /** + * The baseline segment to delete + */ + baselineSegment: BaselineSegment; +} +/** + * Defines a button that opens a modal to delete a baseline segment. + * The deletion can be done earlier or later based on the direction prop. + * @param props The properties for the component + * @returns A button element + */ +export default function DeleteBaselineSegmentComponent(props: DeleteBaselineSegmentComponentProps): React.ReactElement { + const translate = useTranslate(); + const [showDeleteModal, setShowDeleteModal] = React.useState(false); + + const handleShowDeleteModal = () => setShowDeleteModal(true); + const handleHideDeleteModal = () => setShowDeleteModal(false); + + const [deleteBaselineSegmentEarlierMutation] = baselineSegmentsApi.useDeleteBaselineSegmentEarlierMutation(); + const [deleteBaselineSegmentLaterMutation] = baselineSegmentsApi.useDeleteBaselineSegmentLaterMutation(); + + const handleDeleteBaselineSegment = async () => { + try { + if (props.direction === 'earlier') { + await deleteBaselineSegmentEarlierMutation(props.baselineSegment).unwrap(); + } else if (props.direction === 'later') { + await deleteBaselineSegmentLaterMutation(props.baselineSegment).unwrap(); + } else { + throw new Error( + translate('baseline.segments.delete.invalid.direction').replace('{direction}', props.direction) + ); + } + handleHideDeleteModal(); + } catch (error) { + showErrorNotification(error); + } + }; + + const deleteButtonText = props.direction === 'earlier' + ? translate('delete.earlier') : translate('delete.later'); + + const deleteConfirmationMessage = props.direction === 'earlier' + ? translate('baseline.segments.delete.confirm.earlier') + : translate('baseline.segments.delete.confirm.later'); + + return ( + <> + + + {/* Delete baseline segment confirmation modal */} + + + ); +} \ No newline at end of file diff --git a/src/client/app/components/baseline/EditBaselineModalComponent.ts b/src/client/app/components/baseline/EditBaselineModalComponent.ts deleted file mode 100644 index 870ef160e6..0000000000 --- a/src/client/app/components/baseline/EditBaselineModalComponent.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react'; -// Realize that * is already imported from react -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Container, FormGroup, FormFeedback, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../TooltipHelpComponent'; -import { baselineApi, selectBaselinesDetails } from '../../redux/api/baselineApi'; -import { selectMeterDataById } from '../../redux/api/metersApi'; -// import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/reduxHooks'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { TrueFalseType } from '../../types/items'; -import { Baseline } from '../../types/redux/baselines'; -import { useTranslate } from '../../redux/componentHooks'; -import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; - -interface EditBaselineModalComponentProps { - show: boolean; - baseline: Baseline; - baselineIdentifier: string; - // passed in to handle opening the modal - handleShow: () => void; - // passed in to handle closing the modal - handleClose: () => void; -} - -/** - * Defines the edit baselin modal form - * @param props Props for the component - * @returns Baseline edit element - */ -export default function EditBaselineModalComponent(props: EditBaselineModalComponentProps) { - const translate = useTranslate(); - const [editBaseline] = baselineApi.useEditBaselineMutation(); - const [deleteBaseline] = baselineApi.useDeleteBaselineMutation(); - const meterDataById = useAppSelector(selectMeterDataById); - - // Set existing baseline values - const values = { ...props.baseline }; - - /* State */ - // Handlers for each type of input change - const [state, setState] = useState(values); - - const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); - }; - - const handleBooleanChange = (e: React.ChangeEvent) => { - setState({...state, [e.target.name]: JSON.parse(e.target.value) }); - }; -} \ No newline at end of file diff --git a/src/client/app/components/baseline/EditBaselineModalComponent.tsx b/src/client/app/components/baseline/EditBaselineModalComponent.tsx new file mode 100644 index 0000000000..1b85a8c613 --- /dev/null +++ b/src/client/app/components/baseline/EditBaselineModalComponent.tsx @@ -0,0 +1,295 @@ +import * as React from 'react'; +// Realize that * is already imported from react +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { + Button, + Container, + FormGroup, + Input, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + Table, + Pagination, + PaginationItem, + PaginationLink +} +from 'reactstrap'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { baselineApi, baselineSegmentsApi } from '../../redux/api/baselineApi'; +// import { selectMeterDataById } from '../../redux/api/metersApi'; +// import { useAppSelector } from '../../redux/reduxHooks'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import { Baseline, BaselineSegment } from '../../types/redux/baselines'; +import { useTranslate } from '../../redux/componentHooks'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +// import MetersDetailComponent from 'components/meters/MetersDetailComponent'; +import SplitBaselineSegmentComponent from './SplitBaselineSegmentComponent'; +import DeleteBaselineSegmentComponent from './DeleteBaselineSegmentModalComponent'; +import EditBaselineSegmentModalComponent from './EditBaselineSegmentModalComponent'; +import '../../styles/modal.css'; + +interface EditBaselineModalComponentProps { + show: boolean; + baseline: Baseline; + baselineIdentifier: string; + // passed in to handle opening the modalq2 + handleShow: () => void; + // passed in to handle closing the modal + handleClose: () => void; +} + +/** + * Defines the edit baselin modal form + * @param props Props for the component + * @returns Baseline edit element + */ +export default function EditBaselineModalComponent(props: EditBaselineModalComponentProps) { + const PER_PAGE = 10; + const translate = useTranslate(); + // const meterDataById = useAppSelector(selectMeterDataById); + + // Set existing baseline values + const values = { ...props.baseline }; + + /* State */ + // Handlers for each type of input change + const [state, setState] = useState(values); + + const handleStringChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: e.target.value }); + }; + + const handleBooleanChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); + }; + + // const handleNumberChange = (e: React.ChangeEvent) => { + // setState({ ...state, [e.target.name]: Number(e.target.value) }); + // }; + /* End State */ + + // Performs checks to warn the admin if the baseline is inactive + const checkState = () => { + if (!state.isActive) { + alert(translate('baseline.inactiveWarning')); + } + }; + + const { data: baselineSegments = [] } = baselineSegmentsApi.useGetBaselineSegmentsByMeterIdQuery(props.baseline.meterId); + const [ editBaselineMutation, { isLoading: isSaving }] = baselineApi.useEditBaselineMutation(); + const handleSubmit = () => { + props.handleClose(); + editBaselineMutation(state).unwrap() + .then(() => { + showSuccessNotification(translate('baseline.edit.success')); + }).catch(error => { + showErrorNotification(`${translate('baseline.edit.error')} ${error}`); + }); + }; + + // Pagination + const [page, setPage] = React.useState(1); + const totalPages = Math.ceil(baselineSegments.length / PER_PAGE); + const paged = baselineSegments.slice((page - 1) * PER_PAGE, page * PER_PAGE); + + const isUnchanged = React.useMemo(() => { + return props.baseline.isActive === state.isActive && props.baseline.note === state.note; + }, [props.baseline, state]); + + // State to hold the modal for editing a baseline segment + const [showEditSegmentModal, setShowEditSegmentModal] = React.useState(false); + const [editSegment, setEditSegment] = React.useState(null); + + // Function to open the edit segment modal + const handleShowEditSegmentModal = (segment: BaselineSegment) => { + setEditSegment(segment); + setShowEditSegmentModal(true); + }; + // Function to close the edit segment modal + const handleCloseEditSegmentModal = () => { + setShowEditSegmentModal(false); + setEditSegment(null); + }; + + // State to hold the segment for showing segment notes in a modal + const [noteSegment, setNoteSegment] = React.useState(null); + // The note modal is shown when a segment note is clicked + const showNoteModal = Boolean(noteSegment); + const handleShowNoteModal = (segment: BaselineSegment) => { + setNoteSegment(segment); + }; + const handleCloseNoteModal = () => { + setNoteSegment(null); + }; + + // Delete baseline confirmation modal + const [showDeleteModal, setShowDeleteModal] = React.useState(false); + const [deleteBaselineMutation, { isLoading: isDeleting }] = baselineApi.useDeleteBaselineMutation(); + + // Function to handle deleting the baseline + const handleDeleteBaseline = () => { + setShowDeleteModal(false); + deleteBaselineMutation(state.meterId).unwrap() + .then(() => { + showSuccessNotification(translate('baseline.delete.success')); + props.handleClose(); + }) + .catch(error => { + showErrorNotification(`${translate('baseline.delete.error')} ${error}`); + }); + }; + + return ( + <> + + + + +
+ +
+
+ + + + {/* Name */} + + + + + + + + {/* Note */} + + + + + +
+ {/* Table */} +
+ + + + + + + + + + + + + + + {paged?.map(seg => ( + + + + + + + + + + + ))} + +
{seg.startHour} - {seg.endHour}{seg.baselineValue} handleShowNoteModal(seg)} + aria-label={seg.note} + > + {(seg.note ?? '').length > 30 ? `${seg.note?.slice(0, 30)} ...` : seg.note || ''} + + + + {/* only show split buttons if segment is longer than 1 hour */} + {seg.endHour - seg.startHour > 1 && } + + {seg.endHour - seg.startHour > 1 && } + + {/* first segment cannot delete earlier */} + {seg.startHour > 0 && + + } + + {/* last segment cannot delete later */} + {seg.endHour < 24 && + + } +
+ + {/* Pagination */} + {baselineSegments && baselineSegments.length > PER_PAGE && ( + + + setPage(1)} /> + + + setPage(p => p - 1)} /> + + {Array.from({ length: totalPages }, (_, i) => ( + + setPage(i + 1)}>{i + 1} + + ))} + + setPage(p => p + 1)} /> + + + setPage(totalPages)} /> + + + )} +
+
+ + + {/* Delete baseline */} + + + + + + {/* Delete confirmation modal */} + setShowDeleteModal(false)} + actionConfirmText={translate('baseline.delete.button')} + actionRejectText={translate('cancel')} + /> + + {/* Segment note modal */} + + + {noteSegment?.startHour} - {noteSegment?.endHour} + + + {noteSegment?.note} + + +
+ + {/* Edit Baseline Segment modal */} + {editSegment && } + + ); +} \ No newline at end of file diff --git a/src/client/app/components/baseline/EditBaselineSegmentModalComponent.tsx b/src/client/app/components/baseline/EditBaselineSegmentModalComponent.tsx new file mode 100644 index 0000000000..f6be5804ac --- /dev/null +++ b/src/client/app/components/baseline/EditBaselineSegmentModalComponent.tsx @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { baselineSegmentsApi } from '../../redux/api/baselineApi'; +import { BaselineSegment, UpdateBaselineSegmentPayload } from '../../types/redux/baselines'; +import { showErrorNotification } from '../../utils/notifications'; + +interface EditBaselineSegmentModalComponentProps { + show: boolean; + baselineSegment: BaselineSegment; + /** + * Function to run when edit modal closes + */ + handleClose: () => void; +} +/** + * /** + * Defines a modal that allows editing of an existing Baseline Segment. + * @param props - The properties for the component + * @returns Baseline segment edit element + */ +export default function EditBaselineSegmentModalComponent(props: EditBaselineSegmentModalComponentProps): React.ReactElement { + + const [baselineSegment, setBaselineSegment] = React.useState( + { ...props.baselineSegment, originalStartHour: props.baselineSegment.startHour, originalEndHour: props.baselineSegment.endHour } + ); + + // Fetch baseline segments by meter ID to validate start and end hours + const { data: baselineSegments = [] } = baselineSegmentsApi.useGetBaselineSegmentsByMeterIdQuery(props.baselineSegment.meterId); + const [editBaselineSegmentMutation, { isLoading: isSaving }] = baselineSegmentsApi.useEditBaselineSegmentMutation(); + const handleStringChange = (e: React.ChangeEvent) => { + setBaselineSegment({ ...baselineSegment, [e.target.name]: e.target.value }); + }; + const handleNumberChange = (e: React.ChangeEvent) => { + setBaselineSegment({ ...baselineSegment, [e.target.name]: Number(e.target.value) }); + }; + const handleSubmit = () => { + props.handleClose(); + editBaselineSegmentMutation(baselineSegment).unwrap() + .then(() => { + }) + .catch(error => { + showErrorNotification(error); + }); + }; + // The segment immediately before the current segment, if it exists + const earlierSegment = React.useMemo(() => { + const segmentIndex = baselineSegments.findIndex(s => s.meterId === props.baselineSegment.meterId); + if (segmentIndex > 0) { + return baselineSegments[segmentIndex - 1]; + } + return null; + }, [baselineSegments, props.baselineSegment.meterId]); + // The segment immediately after the current segment, if it exists + const laterSegment = React.useMemo(() => { + const segmentIndex = baselineSegments.findIndex(s => s.meterId === props.baselineSegment.meterId); + if (segmentIndex < baselineSegments.length - 1) { + return baselineSegments[segmentIndex + 1]; + } + return null; + }, [baselineSegments, props.baselineSegment.meterId]); + // Validate start hour + const isStartHourValid = React.useMemo(() => { + if (!Number.isInteger(baselineSegment.startHour) || baselineSegment.startHour < 0 || baselineSegment.startHour >= baselineSegment.endHour) { + return false; + } + // Check if the start hour does not conflict with existing segments + if (earlierSegment && baselineSegment.startHour <= earlierSegment.startHour) { + return false; + } + + return true; + }, [baselineSegment.startHour, baselineSegment.endHour, earlierSegment]); + // Validate end hour + const isEndHourValid = React.useMemo(() => { + if (!Number.isInteger(baselineSegment.endHour) || baselineSegment.endHour <= baselineSegment.startHour || baselineSegment.endHour > 24) { + return false; + } + // Check if the end hour does not conflict with existing segments + if (laterSegment && baselineSegment.endHour >= laterSegment.endHour) { + return false; + } + + return true; + }, [baselineSegment.startHour, baselineSegment.endHour, laterSegment]); + + // Validate the segment as a whole + // It should have valid start and end hours + const isSegmentValid = React.useMemo(() => { + return isStartHourValid && isEndHourValid; + }, [baselineSegment.startHour, baselineSegment.endHour]); + + const isSegmentUnchanged = React.useMemo(() => { + return props.baselineSegment.baselineValue === baselineSegment.baselineValue && + props.baselineSegment.startHour === baselineSegment.startHour && + props.baselineSegment.endHour === baselineSegment.endHour && + props.baselineSegment.note === baselineSegment.note; + }, [props.baselineSegment, baselineSegment]); + + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Discard changes */} + + {/* Save changes */} + + + + ) +}; \ No newline at end of file diff --git a/src/client/app/components/baseline/SplitBaselineSegmentComponent.tsx b/src/client/app/components/baseline/SplitBaselineSegmentComponent.tsx new file mode 100644 index 0000000000..46e1532cd2 --- /dev/null +++ b/src/client/app/components/baseline/SplitBaselineSegmentComponent.tsx @@ -0,0 +1,199 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; +import { baselineSegmentsApi } from '../../redux/api/baselineApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { selectDefaultSplitBaselineSegmentValues } from '../../redux/selectors/adminSelectors'; +import { BaselineSegment, SplitBaselineSegmentPayload } from '../../types/redux/baselines'; +import { showErrorNotification } from '../../utils/notifications'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; + +interface SplitBaselineSegmentComponentProps { + /** + * The direction to split the segment + */ + direction: 'earlier' | 'later'; + + /** + * The baseline segment to split + */ + baselineSegment: BaselineSegment; +} + +/** + * Defines a button that opens a modal to split a baseline segment into two segments. + * The split can be done earlier or later based on the direction prop. + * @param props The properties for the component + * @returns A button element + */ +export default function SplitBaselineSegmentComponent(props: SplitBaselineSegmentComponentProps): React.ReactElement { + const translate = useTranslate(); + + const [splitHour, setSplitHour] = React.useState(-999); + + const defaultSegmentValues = selectDefaultSplitBaselineSegmentValues(props.baselineSegment); + + // New segment to be created after the split + const [newSegment, setNewSegment] = React.useState(defaultSegmentValues); + + const [showSplitModal, setShowSplitModal] = React.useState(false); + + // State for the warning modal + const [showWarningModal, setShowWarningModal] = React.useState(false); + const [warningMessage, setWarningMessage] = React.useState(''); + + const [splitEarlierMutation] = baselineSegmentsApi.useSplitEarlierMutation(); + const [splitLaterMutation] = baselineSegmentsApi.useSplitLaterMutation(); + + const handleShowSplitModal = () => setShowSplitModal(true); + const handleHideSplitModal = () => setShowSplitModal(false); + + const handleSplitInputChange = (e: React.ChangeEvent) => { + setSplitHour(Number(e.target.value)); + }; + + const handleNumberChange = (e: React.ChangeEvent) => { + setNewSegment(prev => ({ + ...prev, + [e.target.name]: Number(e.target.value) + })); + }; + const handleStringChange = (e: React.ChangeEvent) => { + setNewSegment(prev => ({ + ...prev, + [e.target.name]: e.target.value + })); + }; + + // Validate the split hour + // It should be greater than the start hour and less than the end hour + const isSplitValid = React.useMemo(() => { + if (!Number.isInteger(splitHour)) { + return false; + } + return splitHour > props.baselineSegment.startHour && splitHour < props.baselineSegment.endHour; + + }, [splitHour, props.baselineSegment, props.direction]); + + const doMutation = () => { + const mutation = props.direction === 'earlier' + ? splitEarlierMutation + : splitLaterMutation; + + handleHideSplitModal(); + mutation({ ...newSegment, id: props.baselineSegment.id, splitTime: splitHour }).unwrap() + .then(() => { + }) + .catch(error => { + showErrorNotification(error); + }); + }; + + // Handle the split operation + const handleSubmit = () => { + // Show warning modal if baseline value is 0 + if (newSegment.newBaselineValue === 0) { + setWarningMessage(translate('baseline.baseline.zero')); + setShowWarningModal(true); + return; + } + doMutation(); + }; + + const handleWarningCancel = () => { + setShowWarningModal(false); + }; + + const handleWarningConfirm = () => { + setShowWarningModal(false); + doMutation(); + }; + + return ( + <> + + + + + + + + {/* split hour */} + + + + + {!isSplitValid && translate('split.hour.invalid') + .replace('{start}', String(props.baselineSegment.startHour + 1)) + .replace('{end}', String(props.baselineSegment.endHour - 1))} + + + + + + + + + + + + + + + + + {/* Warning modal if baseline value is zero */} + + + + ); +} diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 58d58b5e14..6b57a8a0cb 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -35,7 +35,9 @@ export const baseApi = createApi({ 'ConversionDetails', 'Units', 'Cik', - 'Readings' + 'Readings', + 'Baselines', + 'BaselineSegments' ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) diff --git a/src/client/app/redux/api/baselineApi.ts b/src/client/app/redux/api/baselineApi.ts index 9519538b70..70c27e5421 100644 --- a/src/client/app/redux/api/baselineApi.ts +++ b/src/client/app/redux/api/baselineApi.ts @@ -2,8 +2,13 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { createSelector, EntityState, createEntityAdapter } from '@reduxjs/toolkit'; -import { Baseline } from '../../types/redux/baselines'; +import { createSelector } from '@reduxjs/toolkit'; +import { + Baseline, + BaselineSegment, + SplitBaselineSegmentPayload, + UpdateBaselineSegmentPayload +} from '../../types/redux/baselines'; import { baseApi } from './baseApi'; // export const unitsAdapter = createEntityAdapter({ // selectId @@ -18,6 +23,10 @@ export const baselineApi = baseApi.injectEndpoints({ query: () => 'api/baseline', providesTags: ['Baselines'] }), + getBaselineByMeterId: builder.query({ + query: meterId => `api/baseline/${meterId}`, + providesTags: (result, error, id) => [{ type: 'Baselines', id }] + }), addBaseline: builder.mutation({ query: baseline => ({ url: 'api/baseline/new', @@ -32,21 +41,96 @@ export const baselineApi = baseApi.injectEndpoints({ method: 'POST', body: baseline }), - invalidatesTags: ['Baselines'] + // invalidatesTags: ['Baselines'] + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'Baselines', id: arg.meterId }] }), deleteBaseline: builder.mutation({ - query: unitId => ({ + query: meterId => ({ url: 'api/baseline/delete', method: 'POST', - body: { id: unitId } + body: { id: meterId } }), - // You should not be able to delete a unit that is used in a meter or conversion - // so no invalidation for those. invalidatesTags: ['Baselines'] }) }) }); +export const baselineSegmentsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getBaselineSegmentsByMeterId: builder.query({ + query: meterId => `api/baselineSegments/${meterId}`, + providesTags: (result, error, id) => [{ type: 'BaselineSegments', id }] + }), + addBaselineSegment: builder.mutation>({ + query: baselineSegment => ({ + url: 'api/baselineSegments/addBaselineSegment', + method: 'POST', + body: baselineSegment + }), + transformErrorResponse: res => res.data, + invalidatesTags: ['BaselineSegments'] + }), + // TO-DO: split functions + splitEarlier: builder.mutation>({ + query: ({ id, newBaselineValue, newNote, splitTime }) => ({ + url: 'api/baselineSegments/splitEarlier', + method: 'POST', + body: { id, newBaselineValue, newNote, splitTime } + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }), + splitLater: builder.mutation>({ + query: ({ id, newBaselineValue, newNote, splitTime }) => ({ + url: 'api/baselineSegments/splitLater', + method: 'POST', + body: { id, newBaselineValue, newNote, splitTime } + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }), + editBaselineSegment: builder.mutation({ + query: baselineSegment => ({ + url: 'api/baselineSegments/edit', + method: 'POST', + body: baselineSegment + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }), + deleteBaselineSegment: builder.mutation({ + query: ({ meterId, startHour, endHour }) => ({ + url: 'api/baselineSegments/delete', + method: 'POST', + body: { meterId, startHour, endHour } + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }), + // Deletes the provided day segment and updates the end hour of the previous segment + deleteBaselineSegmentEarlier: builder.mutation({ + query: ({ meterId, startHour, endHour }) => ({ + url: 'api/baselineSegments/deleteEarlier', + method: 'POST', + body: { meterId, startHour, endHour } + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }), + // Deletes the provided day segment and updates the start hour of the next segment + deleteBaselineSegmentLater: builder.mutation({ + query: ({ meterId, startHour, endHour }) => ({ + url: 'api/baselineSegments/deleteLater', + method: 'POST', + body: { meterId, startHour, endHour } + }), + transformErrorResponse: res => res.data, + invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] + }) + }) +}); + export const selectBaselinesQueryState = baselineApi.endpoints.getBaselinesDetails.select(); export const selectBaselinesDetails= createSelector( selectBaselinesQueryState, @@ -55,4 +139,17 @@ export const selectBaselinesDetails= createSelector( } ); -export const { selectEntities: selectBaselineById } = +export const { + useGetBaselinesDetailsQuery, + useGetBaselineByMeterIdQuery, + useAddBaselineMutation, + useEditBaselineMutation, + useDeleteBaselineMutation +} = baselineApi; + +export const { + useGetBaselineSegmentsByMeterIdQuery, + useAddBaselineSegmentMutation, + useEditBaselineSegmentMutation, + useDeleteBaselineSegmentMutation +} = baselineSegmentsApi; \ No newline at end of file diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index a17436adb7..62d1f10589 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -2,10 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { createSelector } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { selectCik, selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectAllGroups } from '../../redux/api/groupsApi'; -import { selectAllMeters, selectMeterById } from '../../redux/api/metersApi'; +import { selectAllMeters, selectMeterById, selectMeterDataById } from '../../redux/api/metersApi'; import { selectAdminPreferences } from '../../redux/slices/adminSlice'; import { ConversionData } from '../../types/redux/conversions'; import { MeterData, MeterTimeSortType } from '../../types/redux/meters'; @@ -20,6 +21,8 @@ import { createAppSelector } from './selectors'; import { selectSelectedLanguage } from '../../redux/slices/appStateSlice'; import { DisableChecksType } from '../../types/redux/units'; import { MAX_VAL, MIN_VAL } from '../../utils/input'; +import { Baseline, BaselineSegment, SplitBaselineSegmentPayload } from 'types/redux/baselines'; +import { selectBaselinesDetails } from '../api/baselineApi'; export const MIN_DATE_MOMENT = moment(0).utc(); export const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); @@ -339,6 +342,46 @@ export const selectDefaultCreateConversionValues = createAppSelector( } ); +const selectBaselineSegmentById = (baselineSegment: BaselineSegment) => baselineSegment.id; +export const selectDefaultSplitBaselineSegmentValues = createSelector( + [selectBaselineSegmentById], + (id: number) => ({ + id, + newBaselineValue: 0, + newNote: '', + splitTime: -999 + } as SplitBaselineSegmentPayload) +); + +// Baseline selector to be reviewed +export const selectBaselineExists = createAppSelector( + [ + selectMeterDataById, + selectBaselinesDetails, + (_state, baselineState) => baselineState + ], + (meterDataById, baselines, baselineState): [boolean, string] => { + // Create Baseline Validation: + const baselineValue = baselineState.overallBaseline.baselineValue; + // const isActive = baselineState.overallBaseline.isActive; + + // Baseline value not set or valid + if (baselineValue < 0) { + return [false, translate('baseline.create.baseline.value.not')]; + } + + // Baseline already exists + // TO-DO: compare meter state ID with the baseline's meterID + if ((baselineState.findIndex((baseline: Baseline) => ( + baseline.meterId === meterDataById[baseline.meterId].id + )) !== -1)) { + console.log("Baseline exists"); + return [true, 'Baseline exists']; + } + return [false, 'Baseline does not exist']; + } +); + /* Create Meter Validation: Name cannot be blank Area must be positive or zero diff --git a/src/client/app/types/redux/baselines.ts b/src/client/app/types/redux/baselines.ts index 688096773e..571102dc95 100644 --- a/src/client/app/types/redux/baselines.ts +++ b/src/client/app/types/redux/baselines.ts @@ -10,3 +10,25 @@ export interface Baseline { isActive: boolean; // whether there's a baseline applied or not note: string; } + +export interface BaselineSegment { + id: number; + meterId: number; + startHour: number; + endHour: number; + baselineValue: number; + note?: string; +} + +export interface UpdateBaselineSegmentPayload extends BaselineSegment { + originalStartHour: number; + originalEndHour: number; +} + +export interface SplitBaselineSegmentPayload { + id: number; + meterId: number; + newBaselineValue: number; + newNote?: string; + splitTime: number; +} From 381d29017cf99b5a0f18b1e32c70b19bf2770f46 Mon Sep 17 00:00:00 2001 From: Nhat Anh Nguyen Date: Mon, 1 Sep 2025 10:59:44 -0400 Subject: [PATCH 3/9] Minor fix --- src/server/routes/baseline.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/routes/baseline.js b/src/server/routes/baseline.js index 94e130c969..2bae9e1f37 100644 --- a/src/server/routes/baseline.js +++ b/src/server/routes/baseline.js @@ -7,6 +7,7 @@ const { getConnection } = require('../db'); const express = require('express'); const Baseline = require('../models/Baseline'); const log = require('../log'); +const { adminAuthMiddleware } = require('./authenticator'); const router = express.Router(); router.get('/', async (req, res) => { const conn = getConnection(); From 940122631c87dde5980dfde1224fa72528a4136c Mon Sep 17 00:00:00 2001 From: Nhat Anh Nguyen Date: Sat, 6 Sep 2025 13:35:01 -0400 Subject: [PATCH 4/9] Baseline create and edit --- .../app/components/TooltipHelpComponent.tsx | 1 + .../baseline/CreateBaselineModalComponent.tsx | 264 ++++++++++++++++++ .../baseline/EditBaselineModalComponent.tsx | 21 +- .../meters/EditMeterModalComponent.tsx | 38 ++- src/client/app/redux/api/baselineApi.ts | 21 +- .../app/redux/selectors/adminSelectors.ts | 53 ++-- src/client/app/styles/modal.css | 4 + src/client/app/translations/data.ts | 45 +++ src/client/app/types/redux/baselines.ts | 22 +- 9 files changed, 424 insertions(+), 45 deletions(-) diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index f48de775ea..2265abde09 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -29,6 +29,7 @@ export default function TooltipHelpComponent(props: TooltipHelpProps) { const helpUrl = useAppSelector(selectHelpUrl); const helpLinks: Record> = { + 'help.admin.baselinecreate': { link: `${helpUrl}/adminBaselineCreating/` }, 'help.admin.conversioncreate': { link: `${helpUrl}/adminConversionCreating/` }, 'help.admin.conversionedit': { link: `${helpUrl}/adminConversionEditing/` }, 'help.admin.conversionview': { link: `${helpUrl}/adminConversionViewing/` }, diff --git a/src/client/app/components/baseline/CreateBaselineModalComponent.tsx b/src/client/app/components/baseline/CreateBaselineModalComponent.tsx index e69de29bb2..da7c256c9d 100644 --- a/src/client/app/components/baseline/CreateBaselineModalComponent.tsx +++ b/src/client/app/components/baseline/CreateBaselineModalComponent.tsx @@ -0,0 +1,264 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { baselineApi } from '../../redux/api/baselineApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { useAppSelector } from '../../redux/reduxHooks'; +import { selectDefaultCreateBaselineValues } from '../../redux/selectors/adminSelectors'; +import '../../styles/modal.css'; +import { TrueFalseType } from '../../types/items'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import { isAction } from '@reduxjs/toolkit'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + +/** + * Defines the create baseline modal form + * @returns Baseline create element + */ +export default function CreateBaselineModalComponent({ currentMeterId }: { currentMeterId: number }) { + // /* Hooks & Selectors */ + const translate = useTranslate(); + const [addBaselineMutation] = baselineApi.useAddBaselineMutation(); + + + + const defaultValues = useAppSelector(state => selectDefaultCreateBaselineValues(state, currentMeterId)); + // /* End Hooks & Selectors */ + + /* Utility Functions */ + // Utility to get the initial pattern state + const getInitialPatternState = () => ({ + Baseline: { + meterId: currentMeterId, + isActive: defaultValues.isActive, + note: defaultValues.note + }, + initialSegment: { + baselineValue: defaultValues.baselineValue, + startTime: defaultValues.startTime, + endTime: defaultValues.endTime, + segmentNote: defaultValues.segmentNote + } + }); + + // States + const [showModal, setShowModal] = useState(false); + const [showWarningModal, setShowWarningModal] = useState(false); + const [warningMessage, setWarningMessage] = useState(''); + const [patternState, setPatternState] = useState(getInitialPatternState()); + + // Reset the state to default values + const resetState = () => { + setPatternState(getInitialPatternState()); + }; + + const submitBaseline = () => { + // Close modal first to avoid repeat clicks + setShowModal(false); + addBaselineMutation({ + meterId: patternState.Baseline.meterId, + isActive: patternState.Baseline.isActive, + note: patternState.Baseline.note, + baselineValue: patternState.initialSegment.baselineValue, + segmentNote: patternState.initialSegment.segmentNote + }).unwrap() + .then(() => { + showSuccessNotification(translate('day.create.success')); + }) + .catch(error => { + showErrorNotification(translate('day.create.failure') + error); + }); + resetState(); + }; + /* End Utility Functions */ + + // /* Handlers */ + const handleInitialSegmentNoteChange = (e: React.ChangeEvent) => { + setPatternState(prev => ({ + ...prev, + initialSegment: { + ...prev.initialSegment, + segmentNote: e.target.value + } + })); + }; + + const handleStringChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setPatternState(prev => ({ + ...prev, + Baseline: { + ...prev.Baseline, + [name]: value + } + })); + }; + + const handleNumberChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + const newValue = Number(value); + setPatternState(prev => ({ + ...prev, + initialSegment: { + ...prev.initialSegment, + [name]: newValue + } + })); + }; + + const handleBooleanChange = (e: React.ChangeEvent) => { + setPatternState({ ...patternState, [e.target.name]: JSON.parse(e.target.value) }); + }; + + const handleClose = () => { + setShowModal(false); + // resetState(); + }; + + const handleShow = () => setShowModal(true); + + const handleWarningConfirm = () => { + //Close the warning modal + setShowWarningModal(false); + // submitDay(); + }; + + const handleWarningCancel = () => { + //Close the warning modal + setShowWarningModal(false); + }; + + // Submit + const handleSubmit = () => { + // Show warning modal if baseline value is 0 + if (patternState.initialSegment.baselineValue === 0) { + setWarningMessage(translate('baseline.value.zero')); + setShowWarningModal(true); + } else { + submitBaseline(); + } + }; + /* End Handlers */ + + const tooltipStyle = { + ...tooltipBaseStyle, + tooltipCreateBaselineView: 'help.admin.baselinecreate' + }; + + return ( + <> + + + + + + +
+ +
+
+ + + {/* Active status input for overall baseline*/} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return (); + })} + + + {/* Note input for overall baseline*/} + + + handleStringChange(e)} + value={patternState.Baseline.note} /> + + {/*Initial baseline segment*/} +
+ +
+ + + {/* Baseline value input*/} + + + handleNumberChange(e)} /> + + + + + + {/* Start time input*/} + + + + + + + {/* End time input*/} + + + + + + + {/* Note input for initial pattern*/} + + + handleInitialSegmentNoteChange(e)} + value={patternState.initialSegment.segmentNote} /> + +
+
+
+ + + ); +} \ No newline at end of file diff --git a/src/client/app/components/baseline/EditBaselineModalComponent.tsx b/src/client/app/components/baseline/EditBaselineModalComponent.tsx index 1b85a8c613..1a4b7522b9 100644 --- a/src/client/app/components/baseline/EditBaselineModalComponent.tsx +++ b/src/client/app/components/baseline/EditBaselineModalComponent.tsx @@ -37,7 +37,7 @@ import '../../styles/modal.css'; interface EditBaselineModalComponentProps { show: boolean; baseline: Baseline; - baselineIdentifier: string; + baselineIdentifier: string; // think about whether to get rid of this // passed in to handle opening the modalq2 handleShow: () => void; // passed in to handle closing the modal @@ -75,9 +75,10 @@ export default function EditBaselineModalComponent(props: EditBaselineModalCompo /* End State */ // Performs checks to warn the admin if the baseline is inactive + // TODO: Call in submit const checkState = () => { if (!state.isActive) { - alert(translate('baseline.inactiveWarning')); + alert(translate('baseline.inactiveWarning')); // make a notification } }; @@ -147,6 +148,7 @@ export default function EditBaselineModalComponent(props: EditBaselineModalCompo return ( <> +

Edit baseline

@@ -191,7 +193,7 @@ export default function EditBaselineModalComponent(props: EditBaselineModalCompo {paged?.map(seg => ( - {seg.startHour} - {seg.endHour} + {seg.startTime} - {seg.endTime} {seg.baselineValue} + // This needs to have the same logic as conversions {/* only show split buttons if segment is longer than 1 hour */} - {seg.endHour - seg.startHour > 1 && } + {seg.endTime - seg.startTime > 1 && } - {seg.endHour - seg.startHour > 1 && } + {seg.endTime - seg.startTime > 1 && } {/* first segment cannot delete earlier */} - {seg.startHour > 0 && + {seg.startTime > 0 && } {/* last segment cannot delete later */} - {seg.endHour < 24 && + {seg.endTime < 24 && } @@ -262,7 +265,7 @@ export default function EditBaselineModalComponent(props: EditBaselineModalCompo - @@ -280,7 +283,7 @@ export default function EditBaselineModalComponent(props: EditBaselineModalCompo {/* Segment note modal */} - {noteSegment?.startHour} - {noteSegment?.endHour} + {noteSegment?.startTime} - {noteSegment?.endTime} {noteSegment?.note} diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 01a23d61cd..51e34d0637 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -13,7 +13,7 @@ import { metersApi, selectMeterById, selectMeterDataById } from '../../redux/api import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/reduxHooks'; import { - MAX_DATE, MAX_DATE_MOMENT, MAX_ERRORS, MIN_DATE, MIN_DATE_MOMENT, selectGraphicUnitCompatibility + MAX_DATE, MAX_DATE_MOMENT, MAX_ERRORS, MIN_DATE, MIN_DATE_MOMENT, selectBaselineExists, selectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; @@ -28,6 +28,9 @@ import { useTranslate } from '../../redux/componentHooks'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipHelpComponent from '../TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateBaselineModalComponent from '../baseline/CreateBaselineModalComponent'; +import EditBaselineModalComponent from '../baseline/EditBaselineModalComponent'; +import { useGetBaselineByMeterIdQuery } from 'redux/api/baselineApi'; interface EditMeterModalComponentProps { show: boolean; @@ -57,6 +60,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr const groupDataByID = useAppSelector(selectGroupDataById); // TODO should this state be used for the meterState above or would that cause issues? const meterDataByID = useAppSelector(selectMeterDataById); + const [ baselineExists, description, currentBaseline ] = useAppSelector(state => selectBaselineExists(state, props.meter.id)); useEffect(() => { setLocalMeterEdits(cloneDeep(meterState)); }, [meterState]); /* State */ @@ -234,6 +238,31 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr } }; + const [showModal, setShowModal] = useState(false); + const handleShowEditBaselineModal = () => setShowModal(true); + const handleCloseEditBaselineModal = () => setShowModal(false); + + const handleCreateEditBaseline = () => { + // Show create modal if baseline does not exist for this meter + if (!baselineExists) { + console.log(description); + console.log("Creating baseline..."); + return ; + } else { + // Retrieve current baseline data using meter ID + // const { data: currentBaseline } = useGetBaselineByMeterIdQuery(props.meter.id); + if (currentBaseline) { + return ; + } + } + }; + const handleNumberChange = (e: React.ChangeEvent) => { setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: Number(e.target.value) }); }; @@ -473,6 +502,13 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr value={nullToEmptyString(localMeterEdits.note)} placeholder='Note' /> + + +
+ {handleCreateEditBaseline()} +
+ + {/* cumulative input */} diff --git a/src/client/app/redux/api/baselineApi.ts b/src/client/app/redux/api/baselineApi.ts index 70c27e5421..55c09ce05c 100644 --- a/src/client/app/redux/api/baselineApi.ts +++ b/src/client/app/redux/api/baselineApi.ts @@ -6,6 +6,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { Baseline, BaselineSegment, + CreateBaselinePayload, SplitBaselineSegmentPayload, UpdateBaselineSegmentPayload } from '../../types/redux/baselines'; @@ -27,12 +28,13 @@ export const baselineApi = baseApi.injectEndpoints({ query: meterId => `api/baseline/${meterId}`, providesTags: (result, error, id) => [{ type: 'Baselines', id }] }), - addBaseline: builder.mutation({ + addBaseline: builder.mutation({ query: baseline => ({ url: 'api/baseline/new', method: 'POST', body: baseline }), + transformErrorResponse: res => res.data, invalidatesTags: ['Baselines'] }), editBaseline: builder.mutation({ @@ -100,30 +102,30 @@ export const baselineSegmentsApi = baseApi.injectEndpoints({ invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] }), deleteBaselineSegment: builder.mutation({ - query: ({ meterId, startHour, endHour }) => ({ + query: ({ meterId, startTime, endTime }) => ({ url: 'api/baselineSegments/delete', method: 'POST', - body: { meterId, startHour, endHour } + body: { meterId, startTime, endTime } }), transformErrorResponse: res => res.data, invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] }), - // Deletes the provided day segment and updates the end hour of the previous segment + // Deletes the provided day segment and updates the end time of the previous segment deleteBaselineSegmentEarlier: builder.mutation({ - query: ({ meterId, startHour, endHour }) => ({ + query: ({ meterId, startTime, endTime }) => ({ url: 'api/baselineSegments/deleteEarlier', method: 'POST', - body: { meterId, startHour, endHour } + body: { meterId, startTime, endTime } }), transformErrorResponse: res => res.data, invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] }), - // Deletes the provided day segment and updates the start hour of the next segment + // Deletes the provided day segment and updates the start time of the next segment deleteBaselineSegmentLater: builder.mutation({ - query: ({ meterId, startHour, endHour }) => ({ + query: ({ meterId, startTime, endTime }) => ({ url: 'api/baselineSegments/deleteLater', method: 'POST', - body: { meterId, startHour, endHour } + body: { meterId, startTime, endTime } }), transformErrorResponse: res => res.data, invalidatesTags: (result, error, arg) => [{ type: 'BaselineSegments', meterId: arg.meterId }] @@ -147,6 +149,7 @@ export const { useDeleteBaselineMutation } = baselineApi; + export const { useGetBaselineSegmentsByMeterIdQuery, useAddBaselineSegmentMutation, diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 62d1f10589..41af53bcae 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -23,6 +23,7 @@ import { DisableChecksType } from '../../types/redux/units'; import { MAX_VAL, MIN_VAL } from '../../utils/input'; import { Baseline, BaselineSegment, SplitBaselineSegmentPayload } from 'types/redux/baselines'; import { selectBaselinesDetails } from '../api/baselineApi'; +import { create } from 'lodash'; export const MIN_DATE_MOMENT = moment(0).utc(); export const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); @@ -353,32 +354,46 @@ export const selectDefaultSplitBaselineSegmentValues = createSelector( } as SplitBaselineSegmentPayload) ); -// Baseline selector to be reviewed +export const selectDefaultCreateBaselineValues = createAppSelector( + [ selectMeterById ], + (meter) => { + const defaultValues = { + isActive: true, + // Baseline note + note: '', + baselineValue: 0, + // First segment note + segmentNote: '', + startTime: -Infinity, + endTime: Infinity + }; + return defaultValues; + } +); + export const selectBaselineExists = createAppSelector( [ selectMeterDataById, selectBaselinesDetails, - (_state, baselineState) => baselineState + (_state, meterId: number) => meterId + // (_state, baselineState: Baseline) => baselineState.baselineValue, + // (_state, baselineState: Baseline) => baselineState.isActive ], - (meterDataById, baselines, baselineState): [boolean, string] => { - // Create Baseline Validation: - const baselineValue = baselineState.overallBaseline.baselineValue; - // const isActive = baselineState.overallBaseline.isActive; + (meterDataById, baselines): [boolean, string, Baseline] => { + // Find baseline with the matching meter ID + const baselineIndex = baselines.findIndex((baseline: Baseline) => (baseline.meterId === meterDataById[baseline.meterId].id)); + if (baselineIndex !== -1) { + const baseline = baselines[baselineIndex]; - // Baseline value not set or valid - if (baselineValue < 0) { - return [false, translate('baseline.create.baseline.value.not')]; - } - - // Baseline already exists - // TO-DO: compare meter state ID with the baseline's meterID - if ((baselineState.findIndex((baseline: Baseline) => ( - baseline.meterId === meterDataById[baseline.meterId].id - )) !== -1)) { - console.log("Baseline exists"); - return [true, 'Baseline exists']; + // TODO: internationalize + if (baseline.isActive) { + return [true, 'Baseline exists and is active', baseline]; + } else { + return [true, 'Baseline exists but is not active', baseline]; + } } - return [false, 'Baseline does not exist']; + // Return empty baseline if not exists + return [false, 'Baseline does not exist', {} as Baseline]; } ); diff --git a/src/client/app/styles/modal.css b/src/client/app/styles/modal.css index 64d8c4813c..29405c6c18 100644 --- a/src/client/app/styles/modal.css +++ b/src/client/app/styles/modal.css @@ -21,4 +21,8 @@ select option[value="true"] { .confirmation-message { white-space: pre-wrap; +} + +.baseline-btn { + text-decoration: underline; } \ No newline at end of file diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 60490fb536..94da3bc7a0 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -40,6 +40,22 @@ const LocaleTranslationData = { "bar.interval": "Bar Interval", "bar.raw": "Cannot create bar graph on raw units such as temperature", "bar.stacking": "Bar Stacking", + "baseline.active": "Baseline is active", + "baseline.create": "Create Baseline", + "baseline.create.inactive": "Baseline is inactive", + "baseline.create.exists": "Baseline already exists", + "baseline.delete.success": "Baseline deleted successfully", + "baseline.delete.error": "Error deleting baseline", + "baseline.delete.button": "Delete Baseline", + "baseline.delete.confirm": "Are you sure you want to delete this baseline?", + "baseline.edit.success": "Baseline edited successfully", + "baseline.edit.error": "Error editing baseline", + "baseline.initial.segment": "Baseline Segment", + "baseline.segment.note": "Segment note:", + "baseline.value": "Baseline value:", + "baseline.value.zero": "Baseline value is currently zero", + "baseline.start.time": "Start Time:", + "baseline.end.time": "End Time:", "BooleanMeterTypes.false": "no", "BooleanMeterTypes.meter": "meter or default value", "BooleanMeterTypes.true": "yes", @@ -610,6 +626,22 @@ const LocaleTranslationData = { "bar.interval": "Intervalle du Diagramme à Bandes", "bar.raw": "Cannot create bar graph on raw units such as temperature\u{26A1}", "bar.stacking": "Empilage de Bandes", + "baseline.active": "Baseline is active\u{26A1}", + "baseline.create": "Create Baseline\u{26A1}", + "baseline.create.inactive": "Baseline is inactive\u{26A1}", + "baseline.create.exists": "Baseline already exists\u{26A1}", + "baseline.delete.success": "Baseline deleted successfully\u{26A1}", + "baseline.delete.error": "Error deleting baseline\u{26A1}", + "baseline.delete.button": "Delete Baseline\u{26A1}", + "baseline.delete.confirm": "Are you sure you want to delete this baseline?\u{26A1}", + "baseline.edit.success": "Baseline edited successfully\u{26A1}", + "baseline.edit.error": "Error editing baseline\u{26A1}", + "baseline.initial.segment": "Baseline Segment\u{26A1}", + "baseline.segment.note": "Segment note:\u{26A1}", + "baseline.start.time": "Start Time:\u{26A1}", + "baseline.end.time": "End Time:\u{26A1}", + "baseline.value": "Baseline value:\u{26A1}", + "baseline.value.zero": "Baseline value is currently zero\u{26A1}", "BooleanMeterTypes.false": "yes\u{26A1}", "BooleanMeterTypes.meter": "valeur du compteur ou valeur par défaut", "BooleanMeterTypes.true": "no\u{26A1}", @@ -1180,6 +1212,19 @@ const LocaleTranslationData = { "bar.interval": "Intervalo de barra", "bar.raw": "No se puede crear un gráfico de barras con unidades crudas como la temperatura", "bar.stacking": "Apilamiento de barras", + "baseline.active": "Baseline is active\u{26A1}", + "baseline.create": "Create Baseline\u{26A1}", + "baseline.create.inactive": "Baseline is inactive\u{26A1}", + "baseline.create.exists": "Baseline already exists\u{26A1}", + "baseline.delete.success": "Baseline deleted successfully\u{26A1}", + "baseline.delete.error": "Error deleting baseline\u{26A1}", + "baseline.delete.button": "Delete Baseline\u{26A1}", + "baseline.delete.confirm": "Are you sure you want to delete this baseline?\u{26A1}", + "baseline.edit.success": "Baseline edited successfully\u{26A1}", + "baseline.edit.error": "Error editing baseline\u{26A1}", + "baseline.start.time": "Start Time:\u{26A1}", + "baseline.end.time": "End Time:\u{26A1}", + "baseline.value.zero": "Baseline value is currently zero\u{26A1}", "BooleanMeterTypes.false": "no", "BooleanMeterTypes.meter": "medidor o valor predeterminado", "BooleanMeterTypes.true": "sí", diff --git a/src/client/app/types/redux/baselines.ts b/src/client/app/types/redux/baselines.ts index 571102dc95..715146017d 100644 --- a/src/client/app/types/redux/baselines.ts +++ b/src/client/app/types/redux/baselines.ts @@ -2,20 +2,18 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// export interface BaselineDataById extends Record { } <-- don't know yet if needed - export interface Baseline { - baselineValue: number; meterId: number; - isActive: boolean; // whether there's a baseline applied or not - note: string; + // Whether there's a baseline applied or not + isActive: boolean; + note?: string; } export interface BaselineSegment { id: number; meterId: number; - startHour: number; - endHour: number; + startTime: number; + endTime: number; baselineValue: number; note?: string; } @@ -25,6 +23,16 @@ export interface UpdateBaselineSegmentPayload extends BaselineSegment { originalEndHour: number; } +export interface CreateBaselinePayload { + meterId: number; + isActive: boolean; + // Baseline note + note?: string; + baselineValue: number; + // First segment note + segmentNote?: string; +} + export interface SplitBaselineSegmentPayload { id: number; meterId: number; From fd7a1f2c9e76802c4cb289fd00b7ac4db5ca3783 Mon Sep 17 00:00:00 2001 From: Bao Mai Date: Sun, 7 Sep 2025 23:04:16 -0400 Subject: [PATCH 5/9] For review --- src/server/data/mockBaselines.js | 36 ++++++++++++++++++++++++++++++++ src/server/routes/baseline.js | 14 +++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/server/data/mockBaselines.js diff --git a/src/server/data/mockBaselines.js b/src/server/data/mockBaselines.js new file mode 100644 index 0000000000..3bd6bbc3c1 --- /dev/null +++ b/src/server/data/mockBaselines.js @@ -0,0 +1,36 @@ +const mockBaselines = [ + { + id: 1, + meterId: 101, + startTime: 0, + endTime: 6, + baselineValue: 12.5, + note: "Night segment" + }, + { + id: 2, + meterId: 101, + startTime: 6, + endTime: 12, + baselineValue: 15.2, + note: "Morning segment" + }, + { + id: 3, + meterId: 101, + startTime: 12, + endTime: 18, + baselineValue: 18.0, + note: "Afternoon segment" + }, + { + id: 4, + meterId: 101, + startTime: 18, + endTime: 24, + baselineValue: 14.3, + note: "Evening segment" + } +]; + +module.exports = mockBaselines; \ No newline at end of file diff --git a/src/server/routes/baseline.js b/src/server/routes/baseline.js index 2bae9e1f37..5292dfb52c 100644 --- a/src/server/routes/baseline.js +++ b/src/server/routes/baseline.js @@ -9,6 +9,9 @@ const Baseline = require('../models/Baseline'); const log = require('../log'); const { adminAuthMiddleware } = require('./authenticator'); const router = express.Router(); + +const mockBaselines = require("../data/mockBaselines") + router.get('/', async (req, res) => { const conn = getConnection(); try { @@ -35,6 +38,17 @@ router.post('/new', async (req, res) => { log(`Error while adding baseline: ${err}`, 'error'); } }); + +router.get("/", (req, res) => { + res.json(mockBaselines); +}); + +router.get("/:meterId", (req, res) => { + const { meterId } = req.params; + const segments = mockBaselines.filter(s => s.meterId == meterId); + res.json(segments); +}); + router.post('/edit', adminAuthMiddleware('edit baselines'), async (req, res) => { const validConversion = { type: 'object', From 6a51ce8d520f666d7213dc1e9fd22b59615de365 Mon Sep 17 00:00:00 2001 From: vyvyvyThao Date: Tue, 9 Sep 2025 23:09:05 -0400 Subject: [PATCH 6/9] New baseline and baseline segment models and routes --- src/client/app/types/redux/baselines.ts | 6 +- src/server/app.js | 2 + src/server/models/Baseline.js | 9 +++ src/server/models/BaselineSegment.js | 78 +++++++++++++++++++ src/server/routes/baseline.js | 43 ++++------ src/server/routes/baselineSegments.js | 74 ++++++++++++++++++ src/server/sql/baseline/update_baseline.sql | 11 +++ .../create_baseline_segments_table.sql | 0 8 files changed, 192 insertions(+), 31 deletions(-) create mode 100644 src/server/models/BaselineSegment.js create mode 100644 src/server/routes/baselineSegments.js create mode 100644 src/server/sql/baseline/update_baseline.sql create mode 100644 src/server/sql/baselineSegment/create_baseline_segments_table.sql diff --git a/src/client/app/types/redux/baselines.ts b/src/client/app/types/redux/baselines.ts index 715146017d..f9d75cea85 100644 --- a/src/client/app/types/redux/baselines.ts +++ b/src/client/app/types/redux/baselines.ts @@ -5,13 +5,15 @@ export interface Baseline { meterId: number; // Whether there's a baseline applied or not - isActive: boolean; + isActive: boolean; note?: string; } export interface BaselineSegment { - id: number; meterId: number; + // Calc range can be null if the value is manually enter + calcStart?: number; + calcEnd?: number; startTime: number; endTime: number; baselineValue: number; diff --git a/src/server/app.js b/src/server/app.js index 79385ade76..1d8e538b6b 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -24,6 +24,7 @@ const version = require('./routes/version'); const createRouterForReadings = require('./routes/unitReadings').createRouter; const createRouterForCompareReadings = require('./routes/compareReadings').createRouter; const baseline = require('./routes/baseline'); +const baselineSegments = require('./routes/baselineSegments'); const maps = require('./routes/maps'); const logs = require('./routes/logs'); const obvius = require('./routes/obvius'); @@ -132,6 +133,7 @@ app.use('/api/version', version); app.use('/api/unitReadings', createRouterForReadings()); app.use('/api/compareReadings', createRouterForCompareReadings()); app.use('/api/baselines', baseline); +app.use('/api/baselineSegments', baselineSegments); app.use('/api/maps', maps); app.use('/api/logs', logs); app.use('/api/obvius', obvius); diff --git a/src/server/models/Baseline.js b/src/server/models/Baseline.js index de4de54b01..e2d208463a 100644 --- a/src/server/models/Baseline.js +++ b/src/server/models/Baseline.js @@ -70,6 +70,15 @@ class Baseline { const rows = await conn.any(sqlFile('baseline/get_all_baselines.sql')); return rows.map(row => Baseline.mapRow(row)); } + + /** + * Updates an existed baseline in the database. + * @param {*} conn The connection to use. + */ + async update(conn) { + const baseline = this; + await conn.none(sqlFile('baseline/update_baseline.sql'), baseline); + } } module.exports = Baseline; diff --git a/src/server/models/BaselineSegment.js b/src/server/models/BaselineSegment.js new file mode 100644 index 0000000000..6a244dfcd6 --- /dev/null +++ b/src/server/models/BaselineSegment.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +const database = require('./database'); +const { TimeInterval } = require('../../common/TimeInterval'); +const { momentToIsoOrInfinity } = require('../util/handleTimestampValues'); +const sqlFile = database.sqlFile; + +class BaselineSegment { + /** + * @param {*} meterId The foreign key to the corresponding baseline/meter. + * @param {*} baselineValue The baseline value within the segment. + * @param {*} startTime The moment the segment starts. + * @param {*} endTime The moment the segment ends. + * @param {*} calcStart The moment the segment starts being calculated. + * @param {*} calcEnd The moment the segment stops being calculated. + * @param {*} note Comments by the admin or OED inserted. + */ + constructor(meterId, baselineValue, startTime, endTime, calcStart, calcEnd, note) { + this.meterId = meterId; + this.baselineValue = baselineValue; + this.startTime = momentToIsoOrInfinity(startTime); + this.endTime = momentToIsoOrInfinity(endTime); + this.calcStart = momentToIsoOrInfinity(calcStart); + this.calcEnd = momentToIsoOrInfinity(calcEnd); + this.note = note; + } + + /** + * Returns a promise to create the baseline_segments table. + * @param {*} conn The connection to use. + * @returns {Promise.<>} + */ + static createTable(conn) { + // TODO: baseline segment table in db + return conn.none(sqlFile('baselineSegment/create_baseline_segments_table.sql')); + } + + /** + * Updates an existed baseline segment in the database. + * Note: This function only supports updates where the new start and/or end time extends into the immediately adjacent segments. + * @param {*} originalStartTime The original start time of the segment being updated. + * @param {*} originalEndTime The original end time of the segment being updated. + * @param {*} conn The connection to use. + */ + async update(originalStartTime, originalEndTime, conn) { + const baselineSegment = { + ...this, + originalStartTime, + originalEndTime + }; + const startChanged = this.startTime !== originalStartTime + const endChanged = this.endTime !== originalEndTime; + + // check that -infinity and infinity aren't being updated + if ((startChanged && (originalStartTime === '-infinity')) || endChanged && (originalEndTime === 'infinity')) { + const errMsg = `Cannot update starting time of -infinity or ending time of infinity`; + log.error(errMsg); + throw new Error(errMsg); + } + + return conn.tx(async t => { + // update the previous segment's end time to the updated start time + if (startChanged) { + await t.none(sqlFile('baselineSegment/update_prev_seg_end_to_new_start.sql'), BaselineSegment); + } + + // update the next segment's start time to the updated end time + if (endChanged) { + await t.none(sqlFile('baselineSegment/update_next_seg_start_to_new_end.sql'), baselineSegment); + } + + // Update the current segment + await t.none(sqlFile('baselineSegment/update_baseline_segment.sql'), baselineSegment); + }); + } +} \ No newline at end of file diff --git a/src/server/routes/baseline.js b/src/server/routes/baseline.js index 5292dfb52c..e9686203ac 100644 --- a/src/server/routes/baseline.js +++ b/src/server/routes/baseline.js @@ -10,8 +10,6 @@ const log = require('../log'); const { adminAuthMiddleware } = require('./authenticator'); const router = express.Router(); -const mockBaselines = require("../data/mockBaselines") - router.get('/', async (req, res) => { const conn = getConnection(); try { @@ -38,44 +36,31 @@ router.post('/new', async (req, res) => { log(`Error while adding baseline: ${err}`, 'error'); } }); - -router.get("/", (req, res) => { - res.json(mockBaselines); -}); - -router.get("/:meterId", (req, res) => { - const { meterId } = req.params; - const segments = mockBaselines.filter(s => s.meterId == meterId); - res.json(segments); -}); - router.post('/edit', adminAuthMiddleware('edit baselines'), async (req, res) => { - const validConversion = { + const validBaseline = { type: 'object', - required: ['baselineValue', 'isActive'], + required: ['meterId', 'isActive', 'note'], properties: { - baselineValue: { - type: 'number', - // Do not allow negatives for now - minimum: 0 - }, - isActive: { - type: 'boolean' + meterId: { type: 'number' }, + isActive: { type: 'boolean' }, + note: { + oneOf: [ + { type: 'string' }, + { type: 'null' } + ] } } }; - // not edited - const validatorResult = validate(req.body, validConversion); + const validatorResult = validate(req.body, validBaseline); if (!validatorResult.valid) { - log.warn(`Got request to edit conversions with invalid conversion data, errors: ${validatorResult.errors}`); - failure(res, 400, `Got request to edit conversions with invalid conversion data, errors: ${validatorResult.errors}`); + log.warn(`Got request to edit baselines with invalid baseline data, errors: ${validatorResult.errors}`); + failure(res, 400, `Got request to edit baselines with invalid baseline data, errors: ${validatorResult.errors}`); } else { const conn = getConnection(); try { - const updatedConversion = new Conversion(req.body.sourceId, req.body.destinationId, req.body.bidirectional, - req.body.slope, req.body.intercept, req.body.note); - await updatedConversion.update(conn); + const updatedBaseline = new Baseline(req.body.meterId, req.body.isActive, req.body.note); + await updatedBaseline.update(conn); } catch (err) { log.error(`Error while editing conversion with error(s): ${err}`); failure(res, 500, `Error while editing conversion with error(s): ${err}`); diff --git a/src/server/routes/baselineSegments.js b/src/server/routes/baselineSegments.js new file mode 100644 index 0000000000..b20b8c6282 --- /dev/null +++ b/src/server/routes/baselineSegments.js @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +const { getConnection } = require('../db'); +const express = require('express'); +const BaselineSegment = require('../models/BaselineSegment'); +const log = require('../log'); +const { adminAuthMiddleware } = require('./authenticator'); +const router = express.Router(); +const mockBaselines = require("../data/mockBaselines") + +router.get("/allSegments", (req, res) => { + res.json(mockBaselines); +}); +router.get("/segment/:meterId", (req, res) => { + const { meterId } = req.params; + const segments = mockBaselines.filter(s => s.meterId == meterId); + res.json(segments); +}); +router.get("/segment/:meterId", (req, res) => { + const { meterId } = req.params; + const segments = mockBaselines.filter(s => s.meterId == meterId); + res.json(segments); +}); +router.post('/edit', adminAuthMiddleware('edit baseline segments'), async (req, res) => { + const validBaselineSegment = { + type: 'object', + required: ['meterId', 'baselineValue', 'startTime', 'endTime', 'calcStart', 'calcEnd', 'note'], + properties: { + meterId: { type: 'number' }, + baselineValue: { + type: 'number', + minimum: 0 + }, + startTime: { type: 'number' }, + endTime: { type: 'number' }, + calcStart: { + oneOf: [ + { type: 'number' }, + { type: 'null' } + ] + }, + calcEnd: { + oneOf: [ + { type: 'number' }, + { type: 'null' } + ] + }, + note: { + oneOf: [ + { type: 'string' }, + { type: 'null' } + ] + } + } + }; + const validatorResult = validate(req.body, validBaselineSegment); + if (!validatorResult.valid) { + log.warn(`Got request to edit baselines with invalid baseline segment data, errors: ${validatorResult.errors}`); + failure(res, 400, `Got request to edit baselines with invalid baseline segment data, errors: ${validatorResult.errors}`); + } else { + const conn = getConnection(); + try { + const updatedBaselineSegment = new BaselineSegment(req.body.meterId, req.body.isActive, req.body.note); + await updatedBaseline.update(conn); + } catch (err) { + log.error(`Error while editing conversion with error(s): ${err}`); + failure(res, 500, `Error while editing conversion with error(s): ${err}`); + } + success(res); + } +}); \ No newline at end of file diff --git a/src/server/sql/baseline/update_baseline.sql b/src/server/sql/baseline/update_baseline.sql new file mode 100644 index 0000000000..ce2c8a246a --- /dev/null +++ b/src/server/sql/baseline/update_baseline.sql @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +-- Update overall baseline info using meter ID +UPDATE baselines + SET + is_active = ${isActive}, + note = ${note} + WHERE meter_id = ${meterId}; \ No newline at end of file diff --git a/src/server/sql/baselineSegment/create_baseline_segments_table.sql b/src/server/sql/baselineSegment/create_baseline_segments_table.sql new file mode 100644 index 0000000000..e69de29bb2 From 2b783c4b1d66691b91bcf0a65257e4487c098448 Mon Sep 17 00:00:00 2001 From: vyvyvyThao Date: Wed, 10 Sep 2025 14:10:06 -0400 Subject: [PATCH 7/9] Add split segment functions and routes --- src/server/models/BaselineSegment.js | 77 ++++++++ src/server/routes/baselineSegments.js | 187 +++++++++++++++--- .../create_baseline_segments_table.sql | 6 + .../get_by_meter_id_start_end.sql | 5 + .../insert_new_baseline_segment.sql | 6 + .../update_baseline_segment.sql | 3 + 6 files changed, 256 insertions(+), 28 deletions(-) create mode 100644 src/server/sql/baselineSegment/get_by_meter_id_start_end.sql create mode 100644 src/server/sql/baselineSegment/insert_new_baseline_segment.sql create mode 100644 src/server/sql/baselineSegment/update_baseline_segment.sql diff --git a/src/server/models/BaselineSegment.js b/src/server/models/BaselineSegment.js index 6a244dfcd6..a3ca6a995e 100644 --- a/src/server/models/BaselineSegment.js +++ b/src/server/models/BaselineSegment.js @@ -75,4 +75,81 @@ class BaselineSegment { await t.none(sqlFile('baselineSegment/update_baseline_segment.sql'), baselineSegment); }); } + + static async splitEarlier(meterId, newBaselineValue, newNote, originalStartTime, originalEndTime, splitTime, conn) { + return conn.tx(async t => { + // get all data for the original segment + const originalSegment = await t.one(sqlFile('baselineSegment/get_by_meter_id_start_end.sql'), { + meterId: meterId, + startTime: originalStartTime, + endTime: originalEndTime + }); + + // earlier segment - insert new + const earlierSegment = { + meterId: originalSegment.meterId, + baselineValue: newBaselineValue, + startTime: originalSegment.start_time, + endTime: splitTime, + note: newNote + } + await t.none(sqlFile('baselineSegment/insert_new_baseline_segment.sql'), earlierSegment); + + // later segment - update start time + await t.none(sqlFile('baselineSegment/update_baseline_segment.sql'), { + meterId: originalSegment.meterId, + baselineValue: originalSegment.baselineValue, + startTime: splitTime, + endTime: originalSegment.end_time, + note: originalSegment.note, + originalStartTime: originalSegment.start_time, + originalEndTime: originalSegment.end_time + }); + }); + } + + /** + * Split a segment in two, the later segment uses the new baselineValue/note + * @param {*} meterId The meter's id. + * @param {*} newBaselineValue The baseline value for the new segment. + * @param {*} newNote The note for the new segment. + * @param {*} startTime When the current segment starts. + * @param {*} endTime When the current segment ends. + * @param {*} splitTime The time to split the segment at. + * @param {*} conn The connection to use + * @returns {Promise.} + */ + static async splitLater(meterId, newBaselineValue, newNote, originalStartTime, originalEndTime, splitTime, conn) { + return conn.tx(async t => { + // get all data for the original segment + const originalSegment = await t.one(sqlFile('baselineSegment/get_by_meter_id_start_end.sql'), { + meterId: meterId, + startTime: originalStartTime, + endTime: originalEndTime + }); + + // earlier segment - update end time + await t.none(sqlFile('baselineSegment/update_baseline_segment.sql'), { + meterId: originalSegment.meterId, + baselineValue: originalSegment.baselineValue, + startTime: originalSegment.startTime, + endTime: splitTime, + note: originalSegment.note, + originalStartTime: originalSegment.startTime, + originalEndTime: originalSegment.endTime + }); + + // later segment - insert new + const earlierSegment = { + meterId: originalSegment.meterId, + baselineValue: newBaselineValue, + startTime: splitTime, + endTime: originalSegment.endTime, + calcStart: null, + calcEnd: null, + note: newNote + } + await t.none(sqlFile('baselineSegment/insert_new_baseline_segment.sql'), earlierSegment); + }); + } } \ No newline at end of file diff --git a/src/server/routes/baselineSegments.js b/src/server/routes/baselineSegments.js index b20b8c6282..343f85c70d 100644 --- a/src/server/routes/baselineSegments.js +++ b/src/server/routes/baselineSegments.js @@ -12,50 +12,50 @@ const router = express.Router(); const mockBaselines = require("../data/mockBaselines") router.get("/allSegments", (req, res) => { - res.json(mockBaselines); + res.json(mockBaselines); }); router.get("/segment/:meterId", (req, res) => { - const { meterId } = req.params; - const segments = mockBaselines.filter(s => s.meterId == meterId); - res.json(segments); + const { meterId } = req.params; + const segments = mockBaselines.filter(s => s.meterId == meterId); + res.json(segments); }); router.get("/segment/:meterId", (req, res) => { - const { meterId } = req.params; - const segments = mockBaselines.filter(s => s.meterId == meterId); - res.json(segments); + const { meterId } = req.params; + const segments = mockBaselines.filter(s => s.meterId == meterId); + res.json(segments); }); router.post('/edit', adminAuthMiddleware('edit baseline segments'), async (req, res) => { const validBaselineSegment = { - type: 'object', - required: ['meterId', 'baselineValue', 'startTime', 'endTime', 'calcStart', 'calcEnd', 'note'], - properties: { - meterId: { type: 'number' }, - baselineValue: { + type: 'object', + required: ['meterId', 'baselineValue', 'startTime', 'endTime', 'calcStart', 'calcEnd', 'note'], + properties: { + meterId: { type: 'number' }, + baselineValue: { type: 'number', - minimum: 0 + minimum: 0 }, startTime: { type: 'number' }, - endTime: { type: 'number' }, + endTime: { type: 'number' }, calcStart: { oneOf: [ - { type: 'number' }, - { type: 'null' } - ] + { type: 'number' }, + { type: 'null' } + ] }, calcEnd: { oneOf: [ - { type: 'number' }, - { type: 'null' } - ] + { type: 'number' }, + { type: 'null' } + ] }, - note: { - oneOf: [ - { type: 'string' }, - { type: 'null' } - ] - } - } - }; + note: { + oneOf: [ + { type: 'string' }, + { type: 'null' } + ] + } + } + }; const validatorResult = validate(req.body, validBaselineSegment); if (!validatorResult.valid) { log.warn(`Got request to edit baselines with invalid baseline segment data, errors: ${validatorResult.errors}`); @@ -71,4 +71,135 @@ router.post('/edit', adminAuthMiddleware('edit baseline segments'), async (req, } success(res); } +}); +/** + * POST split a segment in two, the earlier segment uses the new baselineValue/note. + * @param {int} meterId The meter's id. + * @param {string} startTime The start time of the baseline segment. + * @param {string} endTime The end time of the baseline segment. + * @param {number} newBaselineValue The baseline value for the new baseline segment. + * @param {string} newNote Notes added by the admin for the new baseline segment. + * @param {string} splitTime The time to split the segment at. + */ +router.post('/splitEarlier', adminAuthMiddleware('split earlier baseline segment'), async (req, res) => { + const validBaselineSegment = { + type: 'object', + maxProperties: 9, + required: ['meterId', 'startTime', 'endTime', 'newBaselineValue', 'newNote', 'splitTime'], + properties: { + meterId: { + type: 'integer', + minimum: 0 + }, + startTime: { + type: 'string' + }, + endTime: { + type: 'string' + }, + newBaselineValue: { + type: 'number' + }, + newNote: { + oneOf: [ + { type: 'string' }, + { type: 'null' } + ] + }, + splitTime: { + type: 'string' + } + } + }; + + const validatorResult = validate(req.body, validBaselineSegment); + if (!validatorResult.valid) { + const errMsg = `Got request to split a baseline segment earlier with invalid baseline segment data, error(s): ${validatorResult.errors}`; + log.warn(errMsg); + failure(res, 400, errMsg); + } else { + const conn = getConnection(); + try { + await BaselineSegment.splitEarlier( + req.body.meterId, + req.body.newBaselineValue, + req.body.newNote, + momentToIsoOrInfinity(req.body.startTime), + momentToIsoOrInfinity(req.body.endTime), + momentToIsoOrInfinity(req.body.splitTime), + conn + ); + success(res, `Successfully split baseline segment earlier`); + } catch (err) { + const errMsg = `Error splitting baseline segment earlier with error(s): ${err}` + log.error(errMsg); + failure(res, 500, errMsg); + } + } +}); + +/** + * POST split a segment in two, the later segment uses the new baselineValue/note. + * @param {int} meterId The meter's id. + * @param {string} startTime The start time of the baseline segment. + * @param {string} endTime The end time of the baseline segment. + * @param {number} newBaselineValue The baseline value for the new baseline segment. + * @param {string} newNote Notes added by the admin for the new baseline segment. + * @param {string} splitTime The time to split the segment at. + */ +router.post('/splitLater', adminAuthMiddleware('split later baseline segment'), async (req, res) => { + const validBaselineSegment = { + type: 'object', + maxProperties: 9, + required: ['meterId', 'startTime', 'endTime', 'newBaselineValue', 'newNote', 'splitTime'], + properties: { + meterId: { + type: 'integer', + minimum: 0 + }, + startTime: { + type: 'string' + }, + endTime: { + type: 'string' + }, + newBaselineValue: { + type: 'number' + }, + newNote: { + oneOf: [ + { type: 'string' }, + { type: 'null' } + ] + }, + splitTime: { + type: 'string' + } + } + }; + + const validatorResult = validate(req.body, validBaselineSegment); + if (!validatorResult.valid) { + const errMsg = `Got request to split a baseline segment later with invalid baseline segment data, error(s): ${validatorResult.errors}`; + log.warn(errMsg); + failure(res, 400, errMsg); + } else { + const conn = getConnection(); + try { + await BaselineSegment.splitLater( + req.body.meterId, + req.body.newBaselineValue, + req.body.newNote, + momentToIsoOrInfinity(req.body.startTime), + momentToIsoOrInfinity(req.body.endTime), + momentToIsoOrInfinity(req.body.splitTime), + conn + ); + success(res, `Successfully split baseline segment later`); + } catch (err) { + const errMsg = `Error splitting baseline segment later with error(s): ${err}` + log.error(errMsg); + failure(res, 500, errMsg); + } + } }); \ No newline at end of file diff --git a/src/server/sql/baselineSegment/create_baseline_segments_table.sql b/src/server/sql/baselineSegment/create_baseline_segments_table.sql index e69de29bb2..7d9f11730e 100644 --- a/src/server/sql/baselineSegment/create_baseline_segments_table.sql +++ b/src/server/sql/baselineSegment/create_baseline_segments_table.sql @@ -0,0 +1,6 @@ + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + -- TODO: Implement create baseline_segments table with columns: meter_id, baseline_value, start_time, end_time, calc_start, calc_end, note \ No newline at end of file diff --git a/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql b/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql new file mode 100644 index 0000000000..d34036c412 --- /dev/null +++ b/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + -- TODO: Implement retrieving data segment with meter_id, start_time and end_time \ No newline at end of file diff --git a/src/server/sql/baselineSegment/insert_new_baseline_segment.sql b/src/server/sql/baselineSegment/insert_new_baseline_segment.sql new file mode 100644 index 0000000000..b6bf82a807 --- /dev/null +++ b/src/server/sql/baselineSegment/insert_new_baseline_segment.sql @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +INSERT INTO baseline_segments(meter_id, baseline_value, start_time, end_time, calc_start, calc_end, note) +VALUES (${meterId}, ${baselineValue}, ${startTime}, ${endTime}, ${calcStart}, ${calcEnd}, ${note}); \ No newline at end of file diff --git a/src/server/sql/baselineSegment/update_baseline_segment.sql b/src/server/sql/baselineSegment/update_baseline_segment.sql new file mode 100644 index 0000000000..d5649c6111 --- /dev/null +++ b/src/server/sql/baselineSegment/update_baseline_segment.sql @@ -0,0 +1,3 @@ +-- TODO: Implement baseline_segments table updates with parameter: +-- meterId, baselineValue, startTime, endTime, note, originalStartTime, originalEndTime +-- on row with corresponding meter_id, start_time and end_time \ No newline at end of file From cef73dd282eaf134afe8bd1deaa98f8097e3cd87 Mon Sep 17 00:00:00 2001 From: vyvyvyThao Date: Tue, 16 Sep 2025 11:37:40 -0400 Subject: [PATCH 8/9] Add baseline sql codes --- .../create_baseline_segments_table.sql | 12 +++++++++++- .../get_by_meter_id_start_end.sql | 15 ++++++++++++++- .../baselineSegment/update_baseline_segment.sql | 17 ++++++++++++++--- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/server/sql/baselineSegment/create_baseline_segments_table.sql b/src/server/sql/baselineSegment/create_baseline_segments_table.sql index 7d9f11730e..5d87025824 100644 --- a/src/server/sql/baselineSegment/create_baseline_segments_table.sql +++ b/src/server/sql/baselineSegment/create_baseline_segments_table.sql @@ -3,4 +3,14 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -- TODO: Implement create baseline_segments table with columns: meter_id, baseline_value, start_time, end_time, calc_start, calc_end, note \ No newline at end of file +CREATE TABLE IF NOT EXISTS baseline_segments ( + meter_id INTEGER NOT NULL REFERENCES meters(id), + baseline_value FLOAT, + start_time TIMESTAMP NOT NULL DEFAULT '-infinity', + end_time TIMESTAMP NOT NULL DEFAULT 'infinity', + calc_start TIMESTAMP, + calc_end TIMESTAMP, + note TEXT, + PRIMARY KEY (meter_id, start_time, end_time), + CHECK (start_time < end_time) +); \ No newline at end of file diff --git a/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql b/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql index d34036c412..be5734536d 100644 --- a/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql +++ b/src/server/sql/baselineSegment/get_by_meter_id_start_end.sql @@ -2,4 +2,17 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -- TODO: Implement retrieving data segment with meter_id, start_time and end_time \ No newline at end of file +-- Select the baseline segment with corresponding meter ID, start time and end time +SELECT + meter_id, + baseline_value, + -- Cast to text to preserve '-infinity' and 'infinity values, otherwise, JavaScript will convert these values to null + start_time::TEXT AS start_time, + end_time::TEXT AS end_time, + calc_start, + calc_end, + note +FROM baseline_segments +WHERE meter_id = ${meterId} + AND start_time = ${startTime} + AND end_time = ${endTime}; \ No newline at end of file diff --git a/src/server/sql/baselineSegment/update_baseline_segment.sql b/src/server/sql/baselineSegment/update_baseline_segment.sql index d5649c6111..9c3c54217e 100644 --- a/src/server/sql/baselineSegment/update_baseline_segment.sql +++ b/src/server/sql/baselineSegment/update_baseline_segment.sql @@ -1,3 +1,14 @@ --- TODO: Implement baseline_segments table updates with parameter: --- meterId, baselineValue, startTime, endTime, note, originalStartTime, originalEndTime --- on row with corresponding meter_id, start_time and end_time \ No newline at end of file +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +UPDATE baseline_segments + SET baseline_value = ${baselineValue}, + start_time = ${startTime}, + end_time = ${endTime}, + calc_start = ${calcStart}, + calc_end = ${calcEnd}, + note = ${note} + WHERE meter_id = ${meterId} + AND start_time = ${originalStartTime}::TIMESTAMP + AND end_time = ${originalEndTime}::TIMESTAMP; \ No newline at end of file From 7e614edb35f4ea740fc2733ec13f9b08c229ae0c Mon Sep 17 00:00:00 2001 From: vyvyvyThao Date: Sun, 21 Sep 2025 23:05:35 -0400 Subject: [PATCH 9/9] Clean UI components and push changes for debugging --- .../baseline/BaselineViewComponent.tsx | 5 +- .../baseline/CreateBaselineModalComponent.tsx | 10 +- .../DeleteBaselineSegmentModalComponent.tsx | 4 +- .../EditBaselineSegmentModalComponent.tsx | 36 +++--- .../SplitBaselineSegmentComponent.tsx | 14 +-- .../meters/EditMeterModalComponent.tsx | 1 - src/client/app/index.tsx | 2 + src/client/app/types/redux/baselines.ts | 3 + src/server/models/BaselineSegment.js | 35 +++++- src/server/routes/baseline.js | 2 +- src/server/routes/baselineSegments.js | 113 ++++++++++++++++-- src/server/routes/conversionSegments.js | 2 + .../baselineSegment/get_all_by_meter_id.sql | 16 +++ 13 files changed, 191 insertions(+), 52 deletions(-) create mode 100644 src/server/sql/baselineSegment/get_all_by_meter_id.sql diff --git a/src/client/app/components/baseline/BaselineViewComponent.tsx b/src/client/app/components/baseline/BaselineViewComponent.tsx index 133cabca69..963b1576e1 100644 --- a/src/client/app/components/baseline/BaselineViewComponent.tsx +++ b/src/client/app/components/baseline/BaselineViewComponent.tsx @@ -47,15 +47,12 @@ export default function BaselineViewComponent(props: BaselineViewComponentProps) {/*
{conversionIdentifier}
*/} -
- {props.baseline.baselineValue} -
{translate(`TrueFalseType.${props.baseline.isActive.toString()}`)}
{/* Only show first 30 characters so card does not get too big. Should limit to one line */} - {props.baseline.note.slice(0, 29)} + {props.baseline.note?.slice(0, 29)}