From 7d24a80a08e2a09cb7e173bc3c8b5fff76a54155 Mon Sep 17 00:00:00 2001 From: Tyler Adam Martinez Date: Wed, 17 Dec 2025 13:15:17 -0600 Subject: [PATCH 1/9] [ObservationsSelection] Update observation row to use property_type_id & unit_id --- frontend/src/interfaces.d.ts | 4 +- .../MeterActivityEntry/ActivityFormConfig.ts | 422 +++++++++--------- .../MeterActivityEntry/MeterActivityEntry.tsx | 78 +++- .../ObservationsSelection.tsx | 141 +++--- 4 files changed, 364 insertions(+), 281 deletions(-) diff --git a/frontend/src/interfaces.d.ts b/frontend/src/interfaces.d.ts index 482b4d2a..50ca0400 100644 --- a/frontend/src/interfaces.d.ts +++ b/frontend/src/interfaces.d.ts @@ -62,8 +62,8 @@ export interface ActivityFormControl { observations: Array<{ time: Dayjs reading: '' | number - property_type: Partial | null - unit: Partial | null + property_type_id: number | null + unit_id: number | null }>, maintenance_repair?: { service_type_ids: number[] | null, diff --git a/frontend/src/views/Activities/MeterActivityEntry/ActivityFormConfig.ts b/frontend/src/views/Activities/MeterActivityEntry/ActivityFormConfig.ts index faef3499..72a6d894 100644 --- a/frontend/src/views/Activities/MeterActivityEntry/ActivityFormConfig.ts +++ b/frontend/src/views/Activities/MeterActivityEntry/ActivityFormConfig.ts @@ -1,209 +1,213 @@ -import * as Yup from "yup" -import { ActivityForm, ActivityFormControl, MeterListDTO, ObservationForm } from '../../../interfaces.d' -import Dayjs from "dayjs" -import dayjs from "dayjs" - -// Form validation, these are applied to the current form when submitting -export const ActivityResolverSchema: Yup.ObjectSchema = Yup.object().shape({ - - activity_details: Yup.object().shape({ - selected_meter: Yup.object().shape({ - id: Yup.number().required(), - }).required("Please Select A Meter"), - - activity_type: Yup.object().shape({ - id: Yup.number().required("Please Select An Activity"), - }).required("Please Select An Activity"), - - user: Yup.object().shape({ - id: Yup.number().required("Please Select A User"), - }).required("Please Select a User"), - - date: Yup.date().required('Please Select a Date'), - start_time: Yup.date().required('Please Select a Start Time'), - end_time: Yup.date().required('Please Select an End Time') - - }).required(), - - current_installation: Yup.object().when('activity_details.activity_type.id', { - is: 1, - then: (schema) => schema.shape({ - meter: Yup.object().shape({ - id: Yup.number(), - }), - well: Yup.object().shape({ - id: Yup.number().required('Please select a well.'), - }).required('Please select a well.'), - }), - otherwise: (schema) => schema.shape({ - meter: Yup.object().shape({ - id: Yup.number(), - }), - well: Yup.object().shape({ - id: Yup.number().notRequired(), - }).notRequired(), - }) - }), - - observations: Yup.array().of(Yup.object().shape({ - time: Yup.date().required(), - reading: Yup.number().typeError('Please enter a number.').min(0, 'Please enter a non-negative value.').required('Please enter a value.'), - property_type: Yup.object().shape({ - id: Yup.number().required('Please select a property type.'), - }).required('Please select a property type.'), - unit: Yup.object().shape({ - id: Yup.number().required('Please select a unit.'), - }).required('Please select a unit.') - - })).required() - -}).required() - -// Convert the form control to the format expected by the backend -export function toSubmissionForm(activityFormControl: ActivityFormControl) { - const formData = new FormData(); - var observationForms: ObservationForm[] = [] - - activityFormControl.observations.forEach((observation: any) => { - observationForms.push({ - time: observation.time, - reading: observation.reading, - property_type_id: observation.property_type.id, - unit_id: observation.unit.id - }) - }) - - const activityForm: ActivityForm = { - activity_details: { - meter_id: activityFormControl?.activity_details?.selected_meter?.id, - activity_type_id: activityFormControl?.activity_details?.activity_type?.id, - user_id: activityFormControl?.activity_details?.user?.id, - date: activityFormControl?.activity_details?.date, - start_time: activityFormControl?.activity_details?.start_time, - end_time: activityFormControl?.activity_details?.end_time, - share_ose: activityFormControl?.activity_details?.share_ose, - work_order_id: activityFormControl?.activity_details?.work_order_id == null ? undefined : activityFormControl?.activity_details?.work_order_id - }, - current_installation: { - contact_name: activityFormControl?.current_installation?.meter?.contact_name as string, - contact_phone: activityFormControl?.current_installation?.meter?.contact_phone as string, - well_id: activityFormControl?.current_installation?.well?.id, - notes: activityFormControl?.current_installation?.meter?.notes as string, - water_users: activityFormControl?.current_installation?.meter?.water_users as string, - meter_owner: activityFormControl?.current_installation?.meter?.meter_owner as string - }, - observations: observationForms, - maintenance_repair: { - service_type_ids: activityFormControl.maintenance_repair?.service_type_ids ?? [], - description: activityFormControl.maintenance_repair?.description ?? '' - }, - notes: { - working_on_arrival_slug: activityFormControl.notes.working_on_arrival_slug, - selected_note_ids: activityFormControl.notes.selected_note_ids ?? [] - }, - part_used_ids: activityFormControl.part_used_ids ?? [] - } - - formData.append("activity", JSON.stringify(activityForm)); - - activityFormControl.photos?.forEach((file: File) => { - formData.append("photos", file); - }); - - return formData; -} - -// Provides the default values of the activity form -export function getDefaultForm(initialMeter: Partial | null, initialWorkOrderID: number | null = null): ActivityFormControl { - - //Generate start and end times using current time and end time 15min later - const start_time = Dayjs() - const end_time = Dayjs().add(15, 'minute') - - const defaultForm: ActivityFormControl = { - activity_details: { - selected_meter: initialMeter, - activity_type: null, - user: null, - date: Dayjs(), - start_time: start_time, - end_time: end_time, - share_ose: initialWorkOrderID ? true : false, - work_order_id: initialWorkOrderID - }, - - current_installation: { - meter: null, - well: null - }, - - // These should come from DB - observations: [ - { - time: dayjs.utc(), - reading: '', - property_type: { - id: 1, - units: [ - { - id: 1, name: 'Acre-feet', name_short: '...', description: '...' - }, - { - id: 2, name: 'Gallons', name_short: '...', description: '...' - } - ] - }, - unit: { id: 3 } - }, - { - time: dayjs.utc(), - reading: '', - property_type: { - id: 2, - units: [ - { - id: 3, name: 'Kilowatt hours', name_short: '...', description: '...' - }, - { - id: 4, name: 'Gas BTU', name_short: '...', description: '...' - } - ] - }, - unit: { id: 3 } - }, - { - time: dayjs.utc(), - reading: '', - property_type: { - id: 7, - units: [ - { - id: 11, name: 'Inches', name_short: '...', description: '...' - } - ] - }, - unit: { id: 7 } - }, - { - time: dayjs.utc(), - reading: '', - property_type: { - id: 3, - units: [ - { - id: 5, name: 'Percent', name_short: '...', description: '...' - } - ] - }, - unit: { id: 5 } - } - - ], - notes: { - working_on_arrival_slug: 'not-checked', - selected_note_ids: [] - } - } - - return defaultForm -} +import * as Yup from "yup"; +import { + ActivityForm, + ActivityFormControl, + MeterListDTO, + ObservationForm, +} from "../../../interfaces.d"; +import Dayjs from "dayjs"; +import dayjs from "dayjs"; + +// Form validation, these are applied to the current form when submitting +export const ActivityResolverSchema: Yup.ObjectSchema = Yup.object() + .shape({ + activity_details: Yup.object() + .shape({ + selected_meter: Yup.object() + .shape({ + id: Yup.number().required(), + }) + .required("Please Select A Meter"), + + activity_type: Yup.object() + .shape({ + id: Yup.number().required("Please Select An Activity"), + }) + .required("Please Select An Activity"), + + user: Yup.object() + .shape({ + id: Yup.number().required("Please Select A User"), + }) + .required("Please Select a User"), + + date: Yup.date().required("Please Select a Date"), + start_time: Yup.date().required("Please Select a Start Time"), + end_time: Yup.date().required("Please Select an End Time"), + }) + .required(), + + current_installation: Yup.object().when( + "activity_details.activity_type.id", + { + is: 1, + then: (schema) => + schema.shape({ + meter: Yup.object().shape({ + id: Yup.number(), + }), + well: Yup.object() + .shape({ + id: Yup.number().required("Please select a well."), + }) + .required("Please select a well."), + }), + otherwise: (schema) => + schema.shape({ + meter: Yup.object().shape({ + id: Yup.number(), + }), + well: Yup.object() + .shape({ + id: Yup.number().notRequired(), + }) + .notRequired(), + }), + }, + ), + + observations: Yup.array() + .of( + Yup.object().shape({ + time: Yup.date().required(), + reading: Yup.number() + .typeError("Please enter a number.") + .min(0, "Please enter a non-negative value.") + .required("Please enter a value."), + property_type_id: Yup.number() + .typeError("Please select a property type.") + .required("Please select a property type."), + unit_id: Yup.number() + .typeError("Please select a unit.") + .required("Please select a unit."), + }), + ) + .required(), + }) + .required(); + +// Convert the form control to the format expected by the backend +export function toSubmissionForm(activityFormControl: ActivityFormControl) { + const formData = new FormData(); + var observationForms: ObservationForm[] = []; + + activityFormControl.observations.forEach((observation: any) => { + observationForms.push({ + time: observation.time, + reading: observation.reading, + property_type_id: observation.property_type_id ?? "", + unit_id: observation.unit_id ?? "", + }); + }); + + const activityForm: ActivityForm = { + activity_details: { + meter_id: activityFormControl?.activity_details?.selected_meter?.id, + activity_type_id: + activityFormControl?.activity_details?.activity_type?.id, + user_id: activityFormControl?.activity_details?.user?.id, + date: activityFormControl?.activity_details?.date, + start_time: activityFormControl?.activity_details?.start_time, + end_time: activityFormControl?.activity_details?.end_time, + share_ose: activityFormControl?.activity_details?.share_ose, + work_order_id: + activityFormControl?.activity_details?.work_order_id == null + ? undefined + : activityFormControl?.activity_details?.work_order_id, + }, + current_installation: { + contact_name: activityFormControl?.current_installation?.meter + ?.contact_name as string, + contact_phone: activityFormControl?.current_installation?.meter + ?.contact_phone as string, + well_id: activityFormControl?.current_installation?.well?.id, + notes: activityFormControl?.current_installation?.meter?.notes as string, + water_users: activityFormControl?.current_installation?.meter + ?.water_users as string, + meter_owner: activityFormControl?.current_installation?.meter + ?.meter_owner as string, + }, + observations: observationForms, + maintenance_repair: { + service_type_ids: + activityFormControl.maintenance_repair?.service_type_ids ?? [], + description: activityFormControl.maintenance_repair?.description ?? "", + }, + notes: { + working_on_arrival_slug: + activityFormControl.notes.working_on_arrival_slug, + selected_note_ids: activityFormControl.notes.selected_note_ids ?? [], + }, + part_used_ids: activityFormControl.part_used_ids ?? [], + }; + + formData.append("activity", JSON.stringify(activityForm)); + + activityFormControl.photos?.forEach((file: File) => { + formData.append("photos", file); + }); + + return formData; +} + +// Provides the default values of the activity form +export function getDefaultForm( + initialMeter: Partial | null, + initialWorkOrderID: number | null = null, +): ActivityFormControl { + //Generate start and end times using current time and end time 15min later + const start_time = Dayjs(); + const end_time = Dayjs().add(15, "minute"); + + const defaultForm: ActivityFormControl = { + activity_details: { + selected_meter: initialMeter, + activity_type: null, + user: null, + date: Dayjs(), + start_time: start_time, + end_time: end_time, + share_ose: initialWorkOrderID ? true : false, + work_order_id: initialWorkOrderID, + }, + + current_installation: { + meter: null, + well: null, + }, + + // These should come from DB + observations: [ + { + time: dayjs.utc(), + reading: "", + property_type_id: 1, + unit_id: 1, + }, + { + time: dayjs.utc(), + reading: "", + property_type_id: 2, + unit_id: 3, + }, + { + time: dayjs.utc(), + reading: "", + property_type_id: 7, + unit_id: 11, + }, + { + time: dayjs.utc(), + reading: "", + property_type_id: 3, + unit_id: 5, + }, + ], + notes: { + working_on_arrival_slug: "not-checked", + selected_note_ids: [], + }, + }; + + return defaultForm; +} diff --git a/frontend/src/views/Activities/MeterActivityEntry/MeterActivityEntry.tsx b/frontend/src/views/Activities/MeterActivityEntry/MeterActivityEntry.tsx index 4e827e66..ae974f6e 100644 --- a/frontend/src/views/Activities/MeterActivityEntry/MeterActivityEntry.tsx +++ b/frontend/src/views/Activities/MeterActivityEntry/MeterActivityEntry.tsx @@ -37,13 +37,13 @@ export default function MeterActivityEntry() { const [isMeterAndActivitySelected, setIsMeterAndActivitySelected] = useState(false); - function onSuccessfulSubmit(activity_id: number, meter_id: number) { + const onSuccessfulSubmit = (activity_id: number, meter_id: number) => { enqueueSnackbar("Successfully Submitted Activity!", { variant: "success" }); navigate({ pathname: "/manage/meters", search: `?meter_id=${meter_id}&activity_id=${activity_id}`, }); - } + }; const createActivity = useMutation({ mutationFn: async (activityForm: FormData) => { @@ -85,7 +85,9 @@ export default function MeterActivityEntry() { onSuccess: (responseJson) => { const activity_id = responseJson.id; const meter_id = responseJson.meter_id; - enqueueSnackbar("Successfully Submitted Activity!", { variant: "success" }); + enqueueSnackbar("Successfully Submitted Activity!", { + variant: "success", + }); onSuccessfulSubmit(activity_id, meter_id); }, }); @@ -121,20 +123,19 @@ export default function MeterActivityEntry() { useEffect(() => { setHasMeterActivityConflict( - ( - meterDetails.data?.status.status_name == "Installed" && - watch("activity_details.activity_type")?.name == ActivityType.Install - ) || ( - meterDetails.data?.status.status_name != "Installed" && - watch("activity_details.activity_type")?.name == ActivityType.Uninstall - ), + (meterDetails.data?.status.status_name == "Installed" && + watch("activity_details.activity_type")?.name == + ActivityType.Install) || + (meterDetails.data?.status.status_name != "Installed" && + watch("activity_details.activity_type")?.name == + ActivityType.Uninstall), ); }, [meterDetails.data, watch("activity_details.activity_type")?.name]); useEffect(() => { setIsMeterAndActivitySelected( watch("activity_details.selected_meter") != null && - watch("activity_details.activity_type") != null, + watch("activity_details.activity_type") != null, ); }, [ watch("activity_details.selected_meter"), @@ -161,17 +162,56 @@ export default function MeterActivityEntry() { return ( - + {!hasMeterActivityConflict && isMeterAndActivitySelected ? ( - - - - - - + + + + + + {hasErrors(errors) ? ( - + Please correct any errors before submission. ) : ( diff --git a/frontend/src/views/Activities/MeterActivityEntry/ObservationsSelection.tsx b/frontend/src/views/Activities/MeterActivityEntry/ObservationsSelection.tsx index 61617959..6cf8a7c4 100644 --- a/frontend/src/views/Activities/MeterActivityEntry/ObservationsSelection.tsx +++ b/frontend/src/views/Activities/MeterActivityEntry/ObservationsSelection.tsx @@ -1,38 +1,72 @@ import { useEffect } from "react"; -import { Box, Button, Grid, Typography } from "@mui/material"; -import DeleteIcon from "@mui/icons-material/Delete"; -import IconButton from "@mui/material/IconButton"; -import { useFieldArray } from "react-hook-form"; -import dayjs from "dayjs"; +import { Box, Button, Grid, Typography, IconButton } from "@mui/material"; +import { UseQueryResult } from "react-query"; +import { Delete } from "@mui/icons-material"; +import { useFieldArray, useWatch } from "react-hook-form"; import { ObservedPropertyTypeLU } from "../../../interfaces"; import { useGetPropertyTypes } from "../../../service/ApiServiceNew"; +import { ControlledSelectNonObject } from "../../../components/RHControlled/ControlledSelect"; import ControlledTimepicker from "../../../components/RHControlled/ControlledTimepicker"; import ControlledTextbox from "../../../components/RHControlled/ControlledTextbox"; -import { ControlledSelect } from "../../../components/RHControlled/ControlledSelect"; +import dayjs from "dayjs"; -function ObservationRow({ +const ObservationRow = ({ control, - watch, errors, fieldID, index, propertyTypes, remove, setValue, -}: any) { +}: { + control: any; + setValue: any; + errors: any; + + index: number; + fieldID: string; + remove: (index: number) => void; + + propertyTypes: UseQueryResult; +}) => { + const propertyTypeId = useWatch({ + control, + name: `observations.${index}.property_type_id`, + }); + + const unitId = useWatch({ + control, + name: `observations.${index}.unit_id`, + }); + + const propertyType = propertyTypes.data?.find( + (pt) => pt.id === propertyTypeId, + ); + useEffect(() => { - setValue( - `observations.${index}.unit`, - watch(`observations.${index}.property_type`)?.units?.at(0), - ); - setValue( - `observations.${index}.time`, - watch("activity_details.start_time"), - ); //Update the Match start time - }, [ - watch(`observations.${index}.property_type`), - watch("activity_details.start_time"), - ]); // Update the selected unit to the first in the newly selected property type + if ( + !propertyType || + !propertyType?.units || + propertyType?.units?.length === 0 + ) + return; + if (unitId != null) return; + + setValue(`observations.${index}.unit_id`, propertyType?.units[0].id, { + shouldDirty: false, + }); + }, [propertyType, unitId, index, setValue]); + + const startTime = useWatch({ + control, + name: "activity_details.start_time", + }); + + useEffect(() => { + if (!startTime) return; + + setValue(`observations.${index}.time`, startTime, { shouldDirty: false }); + }, [startTime, index, setValue]); return ( @@ -46,13 +80,15 @@ function ObservationRow({ /> - p.name} - error={errors?.observations?.at(index)?.property_type?.message} + label="Reading Type" + options={propertyTypes.data?.map((pt) => pt.id) ?? []} + getOptionLabel={(id: number) => + propertyTypes.data?.find((pt) => pt.id === id)?.name ?? "" + } + error={errors?.observations?.[index]?.property_type_id?.message} /> @@ -67,54 +103,48 @@ function ObservationRow({ /> - u.id) ?? []} + getOptionLabel={(id: number) => + propertyType?.units?.find((u) => u.id === id)?.name ?? "" } - getOptionLabel={(p: ObservedPropertyTypeLU) => p.name} - error={errors?.observations?.at(index)?.unit?.message} + error={errors?.observations?.[index]?.unit_id?.message} /> - + remove(index)} > - + )} ); -} +}; export default function ObservationSelection({ control, errors, - watch, setValue, }: any) { - const propertyTypes: any = useGetPropertyTypes(); + const propertyTypes: UseQueryResult = + useGetPropertyTypes(); - // React hook formarray const { fields, append, remove } = useFieldArray({ control, name: "observations", }); - function addObservation() { - append({ - time: dayjs().utc(), - reading: "", - property_type: null, - unit: null, - }); - } - return ( @@ -125,7 +155,6 @@ export default function ObservationSelection({ return ( ); })} -