import { AppBar, Box, Button, Card, CardActionArea, Checkbox, DialogActions, DialogContent, DialogTitle, FormControlLabel, Grid2, IconButton, Typography } from "@mui/material"; import { green } from "@mui/material/colors"; import { makeStyles } from "@mui/styles"; import { CableData } from "bin/3dView/Data/BuildCableData"; import { NodeData } from "bin/3dView/Data/BuildNodeData"; import ResponsiveDialog from "common/ResponsiveDialog"; import HumidityIcon from "component/HumidityIcon"; import TemperatureIcon from "component/TemperatureIcon"; import { useComponentAPI, useMobile, useSnackbar } from "hooks"; import InteractionsOverview from "interactions/InteractionsOverview"; import { Bin, Component, Device, Interaction } from "models"; import moment from "moment"; import AddIcon from "@mui/icons-material/AddCircle"; import GraphIcon from "products/CommonIcons/graphIcon"; import { pond } from "protobuf-ts/pond"; import { useBinAPI, useGlobalState, useInteractionsAPI } from "providers"; import React from "react"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { canWrite } from "pbHelpers/Permission"; import InteractionSettings from "interactions/InteractionSettings"; const useStyles = makeStyles(() => { return ({ dialog: { maxWidth: 350 }, appBar: { position: "static", borderRadius: 30 }, readingCard: { padding: 10 }, greenButton: { color: green["600"], padding: 0 } }); }); interface Props { open: boolean onClose: () => void node: NodeData cable: CableData device: Device bin: Bin /** * the possible options for controllers when creating new interactions, these controllers must be on the same device as the cable for interactions to work */ filteredComponents?: Component[] componentMap: Map permissions: pond.Permission[] /** * callback function for updating the cable with a new top node or excluded node * @returns void */ updateComponentCallback?: () => void } export default function NodeControls(props: Props){ const {open, onClose, node, cable, device, bin, filteredComponents, permissions, updateComponentCallback, componentMap} = props const classes = useStyles() const [isTopNode, setIsTopNode] = useState(false) const [isExcluded, setIsExcluded] = useState(false) const [cableComponent, setCableComponent] = useState(Component.create()) const [cableInteractions, setCableInteractions] = useState([]) const isMobile = useMobile() const navigate = useNavigate() const [{as, user}] = useGlobalState() const interactionAPI = useInteractionsAPI() const binAPI = useBinAPI() const componentAPI = useComponentAPI() const {openSnack} = useSnackbar() const [newInteraction, setNewInteraction] = useState(false); //will need to list the interactions for the cable useEffect(() => { let component = componentMap.get(cable.grainCable.key) ?? Component.create() setCableComponent(component) interactionAPI.listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as).then(resp => { setCableInteractions(resp); }); },[cable, device, interactionAPI]) //determine if the node is excluded/the top node useEffect(()=>{ if(node.topNode !== undefined){ setIsTopNode(node.topNode) } if(cable.grainCable.excludedNodes.includes(node.nodeIndex)){ setIsExcluded(true) }else( setIsExcluded(false) ) },[node, cable]) //navigation function to go to the component page const goToComponent = () => { navigate("/devices/" + device.id() + "/components/" + cable.grainCable.key); } const close = ()=>{ onClose() } const setEMC = () => { let settings = cableComponent.settings; //make sure the mutation is not there first if(!settings.defaultMutations.includes(pond.Mutator.MUTATOR_EMC)){ settings.defaultMutations.push(pond.Mutator.MUTATOR_EMC) } settings.grainType = bin.grain(); settings.customGrain = bin.customGrain(); componentAPI .update(device.id(), settings, [bin.key()], ["bin"], as) .then(resp => { openSnack("EMC set on cable " + cableComponent.name()); }) .catch(err => {}); } const submit = () => { //should update the component and then update the bin prefs if successful componentAPI.update(device.id(), cableComponent.settings).then(resp => { let pref = pond.BinComponentPreferences.create({ type: pond.BinComponent.BIN_COMPONENT_GRAIN_CABLE, node: cableComponent.settings.grainFilledTo }); //update the bins preferences to have the new top node for the cable binAPI .updateComponentPreferences(bin.key(), cableComponent.key(), pref, as) .then(resp => { openSnack("Changes will be reflected once the bins status updates") if(updateComponentCallback){ updateComponentCallback() } }) .catch(err => {}); }).catch(err => { }) close() } const updateTopNode = (checked: boolean) => { if (isTopNode && !checked) { cableComponent.settings.grainFilledTo = 0; cableInteractions.forEach(interaction => { interaction.settings.subtype = 0; interaction.settings.nodeOne = 0; interaction.settings.nodeTwo = 0; }); } else if (checked) { cableComponent.settings.grainFilledTo = node.nodeIndex+1; cableInteractions.forEach(interaction => { interaction.settings.subtype = Interaction.upToSubtype(node.nodeIndex+1); interaction.settings.nodeOne = node.nodeIndex+1; }); } setIsTopNode(checked); } const updateNodeExclusion = (checked: boolean) => { if (checked) { cableComponent.settings.excludedNodes.push(node.nodeIndex) } else { cableComponent.settings.excludedNodes.splice(cableComponent.settings.excludedNodes.indexOf(node.nodeIndex), 1) } setIsExcluded(checked) } const displayTemp = () => { let isF = user.settings.temperatureUnit === pond.TemperatureUnit.TEMPERATURE_UNIT_FAHRENHEIT let tempFinal = node.celcius ?? 0; if (isF) { tempFinal = (tempFinal * 1.8 + 32); } return tempFinal.toFixed(2) + (isF ? " F" : "C"); }; const latestReading = () => { return ( Latest Reading {moment(cable.grainCable.lastRead).fromNow()} {displayTemp()} Temperature {node.humidity} % Humidity {/* only show the emc if the bin grain type and the cable grain type match */} {node.moisture ? ( {node.moisture} % Grain EMC ) : ( { setEMC(); }}> Set EMC )} goToComponent()}> See Graphs ); }; const nodeControl = () => { return ( Node Control { updateNodeExclusion(e.target.checked); }} name="excludedNode" /> } label={ Exclude Node If checked this will set the node to be disabled and will not be used for any calculations done for the bin } /> { updateTopNode(e.target.checked); }} name="topNode" /> } label={ Set as top node in grain If checked this will set interactions to ignore all nodes above it. Interactions can still be adjusted manually to use all nodes } /> ); }; const interactionDisplay = () => { return ( Interactions { interactionAPI .listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as) .then(resp => { setCableInteractions(resp); }); }} /> { setNewInteraction(true); }} className={classes.greenButton}> ); }; return ( {cable.grainCable.name} Node {node.nodeIndex+1} {latestReading()} {nodeControl()} {interactionDisplay()} {/* */} { setNewInteraction(false); }} refreshCallback={() => { interactionAPI.listInteractionsByComponent(device.id(), cableComponent.location(), undefined, as).then(resp => { setCableInteractions(resp); }); }} canEdit={canWrite(permissions)} /> ) }