From 70bd91bbe749111c926e5e68e88ce200f21fc36f Mon Sep 17 00:00:00 2001 From: csawatzky Date: Thu, 20 Nov 2025 10:26:33 -0600 Subject: [PATCH] switching from having 3 dialogs to using a stepper --- src/bin/BinVisualizerV2.tsx | 271 +++++-------- src/bin/conditioning/modeChangeDialog.tsx | 451 +++++++++++++++++----- src/common/ButtonGroup.tsx | 11 +- src/common/PromiseProgress.tsx | 128 ++++++ 4 files changed, 576 insertions(+), 285 deletions(-) create mode 100644 src/common/PromiseProgress.tsx diff --git a/src/bin/BinVisualizerV2.tsx b/src/bin/BinVisualizerV2.tsx index af4184b..4f9ae41 100644 --- a/src/bin/BinVisualizerV2.tsx +++ b/src/bin/BinVisualizerV2.tsx @@ -3,6 +3,7 @@ import { Box, Button, Card, + CircularProgress, darken, DialogActions, DialogContent, @@ -241,7 +242,7 @@ export default function BinVisualizer(props: Props) { ).colour(); const pressColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_PRESSURE).colour(); const emcColour = describeMeasurement(quack.MeasurementType.MEASUREMENT_TYPE_GRAIN_EMC).colour(); - const [showInputMoisture, setShowInputMoisture] = useState(false); + // const [showInputMoisture, setShowInputMoisture] = useState(false); const [moistureInput, setMoistureInput] = useState(""); const [tempInput, setTempInput] = useState(""); const [outdoorHumidityInput, setOutdoorHumidityInput] = useState(""); @@ -295,55 +296,7 @@ export default function BinVisualizer(props: Props) { const [combinedPlenums, setCombinedPlenums] = useState([]) const [openModeChange, setOpenModeChange] = useState(false) - - // const StyledToggle = styled(ToggleButton)(({ theme }: { theme: Theme }) => ({ - // root: { - // backgroundColor: "transparent", - // overflow: "visible", - // content: "content-box", - // "&$selected": { - // backgroundColor: "gold", - // color: "black", - // borderRadius: 24, - // fontWeight: "bold" - // }, - // "&$selected:hover": { - // backgroundColor: "rgb(255, 255, 0)", - // color: "black", - // borderRadius: 24 - // } - // }, - // selected: {} - // })); - - // const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }: { theme: Theme }) => ({ - // grouped: { - // margin: theme.spacing(-0.5), - // border: "none", - // padding: theme.spacing(1), - // "&:not(:first-child):not(:last-child)": { - // borderRadius: 24, - // marginRight: theme.spacing(0.5), - // marginLeft: theme.spacing(0.5) - // }, - // "&:first-child": { - // borderRadius: 24, - // marginLeft: theme.spacing(0.25) - // }, - // "&:last-child": { - // borderRadius: 24, - // marginRight: theme.spacing(0.25) - // } - // }, - // root: { - // backgroundColor: darken( - // theme.palette.background.paper, - // getThemeType() === "light" ? 0.05 : 0.25 - // ), - // borderRadius: 24, - // content: "border-box" - // } - // })); + const [modeChangeInProgress, setModeChangeInProgress] = useState(false) useEffect(() => { setModeTime(moment(bin.status.lastModeChange)); @@ -429,20 +382,6 @@ export default function BinVisualizer(props: Props) { humids.push(...filteredNodes.humids) emcs.push(...filteredNodes.moistures) } - // let tempClone = cloneDeep(cable.temperatures).reverse(); - // let humClone = cloneDeep(cable.humidities).reverse(); - // let emcClone = cloneDeep(cable.grainMoistures).reverse(); - //add the cable data to the proper arrays - // if (cable.topNode > 0) { - // temps.push(...tempClone.splice(0, cable.topNode)); - // humids.push(...humClone.splice(0, cable.topNode)); - // emcs.push(...emcClone.splice(0, cable.topNode)); - // } else { - // //if the cable has no fill set (top node) then use all of the nodes - // temps = tempClone; - // humids = humClone; - // emcs = emcClone; - // } //add the trend data to the proper arrays if the top node is set let cableTrendData = binTrend.cableTrend[cable.key()]; @@ -1796,7 +1735,11 @@ export default function BinVisualizer(props: Props) { Bin Mode + {modeChangeInProgress && + + } { - setShowInputMoisture(true) + // setShowInputMoisture(true) + setModeConditioning() //will set it to either drying or hydrating based on the initial and target moisture } } ]} toggledButtons={determineToggle(mode)} toggle /> - {/* - - Storage - - - Cooldown - - {bin.settings.inventory && - bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture ? ( - setShowInputMoisture(true)} - value={pond.BinMode.BIN_MODE_HYDRATING} - aria-label="Hydrating Mode"> - Hydrating - - ) : ( - setShowInputMoisture(true)} - value={pond.BinMode.BIN_MODE_DRYING} - aria-label="Drying Mode"> - Drying - - )} - */} ); }; @@ -2037,7 +1946,7 @@ export default function BinVisualizer(props: Props) { setOpenModeChange(true) }; - const setModeDrying = () => { + const setModeConditioning = () => { if ( bin.settings.inventory && bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture @@ -2051,82 +1960,82 @@ export default function BinVisualizer(props: Props) { setOpenModeChange(true) }; - const closeMoistureDialog = () => { - // setNewPreset(0); - setNewBinMode(bin.settings.mode); - setShowInputMoisture(false); - }; + // const closeMoistureDialog = () => { + // // setNewPreset(0); + // setNewBinMode(bin.settings.mode); + // setShowInputMoisture(false); + // }; - const moistureDialog = () => { - return ( - - Input new grain moisture - - - Updating the current grain moisture will calibrate the estimate for more accurate - results. The outdoor temperature will have some effect on the estimate as well; consider - updating with drastic changes in the weather or using a predicted average. - - setMoistureInput(event.target.value)} - InputProps={{ - endAdornment: % - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - - Weather - - setTempInput(event.target.value)} - InputProps={{ - endAdornment: ( - - {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT - ? "°F" - : "℃"} - - ) - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - setOutdoorHumidityInput(event.target.value)} - InputProps={{ - endAdornment: % - }} - style={{ marginTop: theme.spacing(2) }} - fullWidth - variant="outlined" - /> - - - - - - - ); - }; + // const moistureDialog = () => { + // return ( + // + // Input new grain moisture + // + // + // Updating the current grain moisture will calibrate the estimate for more accurate + // results. The outdoor temperature will have some effect on the estimate as well; consider + // updating with drastic changes in the weather or using a predicted average. + // + // setMoistureInput(event.target.value)} + // InputProps={{ + // endAdornment: % + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // + // Weather + // + // setTempInput(event.target.value)} + // InputProps={{ + // endAdornment: ( + // + // {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + // ? "°F" + // : "℃"} + // + // ) + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // setOutdoorHumidityInput(event.target.value)} + // InputProps={{ + // endAdornment: % + // }} + // style={{ marginTop: theme.spacing(2) }} + // fullWidth + // variant="outlined" + // /> + // + // + // + // + // + // + // ); + // }; if (loading) { return ; @@ -2171,7 +2080,7 @@ export default function BinVisualizer(props: Props) { return ( - {moistureDialog()} + {/* {moistureDialog()} */} {cfmLowDialog()} {cfmHighDialog()} {changeGrain()} @@ -2201,11 +2110,15 @@ export default function BinVisualizer(props: Props) { compDevMap={componentDevices} preferences={preferences} onClose={(refresh) => { - setShowInputMoisture(false) setOpenModeChange(false) if(refresh) updateBin(); }} - + startChange={() => { + setModeChangeInProgress(true) + }} + changeComplete={() => { + setModeChangeInProgress(false) + }} /> {binMode()} diff --git a/src/bin/conditioning/modeChangeDialog.tsx b/src/bin/conditioning/modeChangeDialog.tsx index 06e1a8f..4d420e2 100644 --- a/src/bin/conditioning/modeChangeDialog.tsx +++ b/src/bin/conditioning/modeChangeDialog.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField } from "@mui/material"; +import { Autocomplete, Box, Button, DialogActions, DialogContent, DialogContentText, DialogTitle, TextField, Stepper, Step, StepLabel, InputAdornment, Typography } from "@mui/material"; import ResponsiveDialog from "common/ResponsiveDialog"; import { useComponentAPI, useInteractionsAPI } from "hooks"; import { Component, Device, Interaction } from "models"; @@ -21,6 +21,7 @@ import { lowerCase } from "lodash"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { getTemperatureUnit } from "utils"; import { GetGrainExtensionMap } from "grain"; +import { PromiseProgress, Stage, Step as PromiseStep } from "common/PromiseProgress"; interface Props { open: boolean @@ -33,6 +34,13 @@ interface Props { binCables?: GrainCable[]; compDevMap?: Map; presets?: DevicePreset[]; + startChange: () => void; + changeComplete: () => void; + defaultTargetMoisture?: number; + changeTargetMoisture?: (newTarget: number) => {} + defaultOutdoorTemp?: number; + changeOutdoorTemp?: (newTemp: number) => {} + defaultOutdoorHumidity?: number; } interface Option { @@ -41,6 +49,11 @@ interface Option { icon?: string; } +interface DialogStep { + label: string; + completed?: boolean; +} + export default function ModeChangeDialog(props: Props){ const { open, @@ -51,10 +64,13 @@ export default function ModeChangeDialog(props: Props){ preferences, binCables, compDevMap, - grain + grain, + startChange, + changeComplete, + defaultTargetMoisture } = props const [{as}] = useGlobalState() - // const [componentSets, setComponentSets] = useState([]) + const [componentSets, setComponentSets] = useState([]) const interactionAPI = useInteractionsAPI() const componentAPI = useComponentAPI(); const [selectedDevice, setSelectedDevice] = useState() @@ -73,6 +89,16 @@ export default function ModeChangeDialog(props: Props){ const grainExtensionMap = GetGrainExtensionMap(); const [conflictingSetInteractions, setConflictingSetInteractions] = useState([]) const [interactionsToAdd, setInteractionsToAdd] = useState() + const [promiseStages, setPromiseStages] = useState([]) + const [dialogSteps, setDialogSteps] = useState([]) + const [currentStep, setCurrentStep] = useState(0) + + //the state variables for the moisture + const [moistureInput, setMoistureInput] = useState("") + + useEffect(()=>{ + setMoistureInput(defaultTargetMoisture?.toFixed(2) ?? "0") + },[defaultTargetMoisture]) //get the interactions and components for the device when it is selected from the dropdown useEffect(()=>{ @@ -153,6 +179,39 @@ if (!selectedDevice) return; setOptions(o); }, [devices, setOptions]); + + //use effect here to determine the steps based on the bin mode + useEffect(() => { + let steps: DialogStep[] = [] + switch(binMode){ + case pond.BinMode.BIN_MODE_STORAGE: + steps.push({ + label: "Storage Settings" + }) + break; + case pond.BinMode.BIN_MODE_COOLDOWN: + steps.push({ + label: "Set Components" + }) + steps.push({ + label: "Progress" + }) + break; + case pond.BinMode.BIN_MODE_DRYING: + case pond.BinMode.BIN_MODE_HYDRATING: + steps.push({ + label: "Weather Conditions" + }) + steps.push({ + label: "Set Components" + }) + steps.push({ + label: "Progress" + }) + } + setDialogSteps(steps) + },[binMode]) + const buildHeaterInteraction = ( sensor: Plenum | Ambient, heater: Controller @@ -235,7 +294,8 @@ if (!selectedDevice) return; }); conditions.push(tempCondition); interaction.conditions = conditions; - + //the the heater output to auto so that when the components are update it uses the new mode + heater.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO return interaction; }; @@ -333,40 +393,72 @@ if (!selectedDevice) return; conditions.push(fanConditionTwo); interaction.conditions = conditions; + //set the output mode to auto in the fan so that when the components are updated it uses the new mode + fan.settings.defaultOutputState = quack.OutputMode.OUTPUT_MODE_AUTO return interaction; }; //this is what will use the conflicting interactions - const execute = () => { - if(!selectedDevice || !interactionsToAdd) return //if there is no device selected or there are no interactions to add, do nothing - //create an array of promises to delete the conflicting interactions - let conflictingInteractionsPromise = conflictingSetInteractions.map(i => { - return interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as); + const buildPromises = () => { + //if there is no device selected or there are no interactions to add, do nothing + if(!selectedDevice || !interactionsToAdd) return + + //build the stages to pass into the PromiseProgress + //stage one is to remove conflicting interactions + let stages: Stage[] = [] + let stage1: Stage = { + title: "Remove Conflicting Interactions", + steps: [] + } + conflictingSetInteractions.forEach(i => { + let newStep: PromiseStep = { + title: "Removing Interaction", + promise: interactionAPI.removeInteraction(selectedDevice.id(), i.key(), as) + } + stage1.steps.push(newStep); }); - Promise.all([...conflictingInteractionsPromise]).then(() => { - //the conflicting interactions were removed now add the new ones - interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) - }).catch(err => { - //there was a problem removing any conflicting interactions + stages.push(stage1) + //stage two is to add the new interactions + let stage2: Stage = { + title: "Add New Interactions", + steps: [ + { + title: "Adding Interactions", + promise: interactionAPI.addMultiInteractions(selectedDevice.id(), interactionsToAdd) + } + ] + } + stages.push(stage2) + + //stage three is to update the controller components + let stage3: Stage = { + title: "Update Controllers", + steps: [] + } + componentSets.forEach(set => { + set.controllers.forEach(controller => { + console.log(controller) + let newStep: PromiseStep = { + title: "Updating " + controller.name(), + promise: componentAPI.update(selectedDevice.id(), controller.settings) + } + stage3.steps.push(newStep) + }) }) + stages.push(stage3) + //set those stages to a state variable + setPromiseStages(stages) } - /** - * this function takes in the sets that are passed in and returns the promises needed for removing conflicting interactions and adding the new ones - * IT DOES NOT EXECUTE THEM ONLY CREATES THE PROMISES - * returns an object containing two values, - * one is an array of promises for conflicting interactions to remove and the other is the promise for adding the new interactions - * @param sets - * @returns - */ + //this function just takes in the sets as they change and creates the list of conflicting interactions and the settings to create the new ones and returns them const createInteractions = (sets: ComponentSet[]) => { if(!selectedDevice) return undefined //if there is no device that was selected, do nothing let multiInteractionSettings: pond.MultiInteractionSettings = pond.MultiInteractionSettings.create() let conflictingInteractions: Interaction[] = [] let linkedComponents = deviceComponents.get(selectedDevice.id().toString()); //loop through the sets - sets.forEach(async (set) => { + sets.forEach((set) => { //loop through the controllers set.controllers.forEach(controller => { //filter the conflicting interactions for that controller @@ -419,6 +511,7 @@ if (!selectedDevice) return; } }) }) + setComponentSets([...sets]) return { conflicting: conflictingInteractions, toAdd: multiInteractionSettings @@ -460,102 +553,250 @@ if (!selectedDevice) return; const close = (refresh: boolean) => { + //clear state data + setCurrentStep(0) + setInteractionsToAdd(undefined) + setPromiseStages([]) onClose(refresh) } - const content = () => { + //this step will only be used for drying and hydrating + const moistureStep = () => { + return ( + + + Updating the current grain moisture will calibrate the estimate for more accurate + results. The outdoor temperature will have some effect on the estimate as well; consider + updating with drastic changes in the weather or using a predicted average. + + setMoistureInput(event.target.value)} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + + Weather + + setTempInput(event.target.value)} + InputProps={{ + endAdornment: ( + + {getTemperatureUnit() === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT + ? "°F" + : "℃"} + + ) + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + setOutdoorHumidityInput(event.target.value)} + InputProps={{ + endAdornment: % + }} + style={{ marginTop: theme.spacing(2) }} + fullWidth + variant="outlined" + /> + + ) + }; + + const selectDeviceStep = () => { switch(binMode){ case pond.BinMode.BIN_MODE_STORAGE: return ( - - Entering Storage Mode - - {binCables && binCables?.length > 0 && compDevMap && ( - - - - )} - - Select device to turn off controllers and end conditioning. Any interactions will be - left alone. Heaters/Fans will be turned off. - - {deviceSelector()} - - - - {/* Confirm - button to enter storage and end conditioning */} - - - - ) - case pond.BinMode.BIN_MODE_COOLDOWN: + + {binCables && binCables?.length > 0 && compDevMap && ( + + + + )} + + Select device to turn off controllers and end conditioning. Any interactions will be + left alone. Heaters/Fans will be turned off. + + {deviceSelector()} + + ) case pond.BinMode.BIN_MODE_DRYING: + case pond.BinMode.BIN_MODE_COOLDOWN: case pond.BinMode.BIN_MODE_HYDRATING: return ( - - Choose Device for {modeDescription()} Method - - - {modeDescription()} control is handled by interactions between multiple components on the - same device. Please choose which device and components will handle this bin's{" "} - {modeDescription()}. - - {deviceSelector()} - { - //update the component sets that will be used for the interactions - //when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list - //then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones - let i = createInteractions(sets) - if(i !== undefined){ - console.log(i) - setConflictingSetInteractions(i.conflicting) - setInteractionsToAdd(i.toAdd) - } - }}/> - - - - - - - ) + + + {modeDescription()} control is handled by interactions between multiple components on the + same device. Please choose which device and components will handle this bin's{" "} + {modeDescription()}. + + {deviceSelector()} + { + //update the component sets that will be used for the interactions + //when the sets change it will create the interactions and any conflicting ones that need to be removed will be put into a list + //then when the submit button gets clicked it will use the conflicting list and toAdd to remove conflicting interactions and add the new ones + let i = createInteractions(sets) + if(i !== undefined){ + setConflictingSetInteractions(i.conflicting) + setInteractionsToAdd(i.toAdd) + } + }} + /> + + ) } } + const progressStep = () => { + return ( + + { + console.log("COMPLETED PROMISES") + setCurrentStep(0)//reset the stepper + changeComplete()//change the boolean preventing more mode changes + }} + /> + + ) + } + + const stepper = () => { + return ( + + {dialogSteps.map(dialogStep => { + const labelProps: { + optional?: React.ReactNode; + } = {}; + return ( + + {dialogStep.label} + + ); + })} + + ); + } + + const actions = () => { + let buttons: JSX.Element[] = [] + //this button should be on every step of all of them + buttons.push( + + ) + // this button should appear if the step they are on is not 0 and not the last step + if(currentStep !== 0 && currentStep !== dialogSteps.length-1){ + buttons.push( + + ) + } + // this button should appear if the step they are on is less than the last step -1 + if(currentStep < dialogSteps.length-2){ + buttons.push( + + ) + } + if((binMode === pond.BinMode.BIN_MODE_STORAGE || binMode === pond.BinMode.BIN_MODE_COOLDOWN && currentStep === 0) || + binMode === pond.BinMode.BIN_MODE_DRYING || binMode === pond.BinMode.BIN_MODE_HYDRATING && currentStep === 1){ + buttons.push( + + ) + } + return buttons + } + + const stepperContent = (step: number) => { + switch(binMode){ + case pond.BinMode.BIN_MODE_STORAGE: + return selectDeviceStep(); + case pond.BinMode.BIN_MODE_COOLDOWN: + switch (step) { + case 1: + return progressStep(); + default: + return selectDeviceStep(); + } + case pond.BinMode.BIN_MODE_DRYING: + case pond.BinMode.BIN_MODE_HYDRATING: + switch (step) { + case 1: + return selectDeviceStep(); + case 2: + return progressStep(); + default: + return moistureStep(); + } + } + }; + return ( + + {/* { + setOpenProgress(false) + close(refresh) + }} /> */} {close(false)}}> - {content()} + + {binMode === pond.BinMode.BIN_MODE_STORAGE ? "Entering Storage Mode" : "Choose Device for " + modeDescription() + " Method"} + + + {stepper()} + {stepperContent(currentStep)} + + + {actions()} + + ) } \ No newline at end of file diff --git a/src/common/ButtonGroup.tsx b/src/common/ButtonGroup.tsx index dfadd58..3c2c64c 100644 --- a/src/common/ButtonGroup.tsx +++ b/src/common/ButtonGroup.tsx @@ -19,6 +19,10 @@ export interface ButtonData { * The function to run when the button is clicked or toggled on when toggle is true */ function: () => void + /** + * whether the button is disabled or not + */ + disabled?: boolean } interface Props { @@ -49,6 +53,10 @@ interface Props { * Numerical value for the size of the text for buttons that dont use the icon */ textSize?: number + /** + * When true will disable all of the buttons in the group, will be overridded by individual buttons disabled state in the button data + */ + disableAll?: boolean } const useStyles = makeStyles((theme: Theme) => { @@ -88,7 +96,7 @@ const useStyles = makeStyles((theme: Theme) => { }) export default function ButtonGroup(props: Props){ - const { buttons, toggle, toggledButtons, multiToggle, textSize } = props + const { buttons, toggle, toggledButtons, multiToggle, textSize, disableAll } = props const classes = useStyles() const [currentToggle, setCurrentToggle] = useState([]) @@ -135,6 +143,7 @@ export default function ButtonGroup(props: Props){ {buttons.map((b, i) => ( + + + ) + +} \ No newline at end of file