import { Button, Card, CardActions, CardContent, CardHeader, darken, Grid2 as Grid, IconButton, Slider, Theme, ToggleButton, ToggleButtonGroup, Tooltip, Typography } from "@mui/material"; import { Settings } from "@mui/icons-material"; import PendingChangesIndicator from "common/PendingChangesIndicator"; import { useInteractionsAPI, usePrevious, useSnackbar } from "hooks"; import { cloneDeep } from "lodash"; import { Component, Device, Interaction } from "models"; import moment from "moment"; import { componentIDToString } from "pbHelpers/Component"; import { abbreviatedWeekdays, friendlyTimeOfDayDescriptor, interactionConditionText, interactionResultText, isSource, timeOfDayDescriptor } from "pbHelpers/Interaction"; import { describeMeasurement } from "pbHelpers/MeasurementDescriber"; import { canWrite } from "pbHelpers/Permission"; import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; import React, { useEffect, useState } from "react"; import InteractionSettings from "./InteractionSettings"; import { makeStyles } from "@mui/styles"; import { useGlobalState } from "providers"; const useStyles = makeStyles((theme: Theme) => { return ({ card: { backgroundColor: darken(theme.palette.background.paper, 0.05) }, header: { padding: theme.spacing(1), paddingBottom: 0 }, content: { padding: `${theme.spacing(1)}px !important` }, actions: { marginTop: "auto", display: "flex", flexDirection: "row", justifyContent: "flex-end", padding: theme.spacing(1), paddingTop: 0 } }) }); interface Props { device: Device; component: Component; components: Component[]; interactions: Interaction[]; permissions: pond.Permission[]; refreshCallback: () => void; } export default function InteractionsOverview(props: Props) { const classes = useStyles(); const [{as, user}] = useGlobalState(); const interactionsAPI = useInteractionsAPI(); const { success, error } = useSnackbar(); const { device, component, components, permissions, refreshCallback } = props; const prevComponents = usePrevious(components); const prevInteractions = usePrevious(props.interactions); const [interactions, setInteractions] = useState(props.interactions); const [dirtyInteractions, setDirtyInteractions] = useState>(new Map()); const [mappedComponents, setMappedComponents] = useState>(new Map()); const [acceptedInteractions, setAcceptedInteractions] = useState>(new Set()); const [renderToggle, setRenderToggle] = useState(false); const [selectedInteraction, setSelectedInteraction] = useState( undefined ); useEffect(() => { if (components !== prevComponents) { let initMappedComponents: Map = new Map(); components.forEach((component: Component) => { initMappedComponents.set(component.locationString(), component); }); setMappedComponents(initMappedComponents); } if (props.interactions !== prevInteractions) { setAcceptedInteractions(prev => { const updated = new Set(prev); props.interactions.forEach((interaction: Interaction) => { const key = interaction.key(); const prevInteraction = prevInteractions?.find(p => p.key() === key); if (!interaction.status.synced) { updated.delete(key); } else if (prevInteraction && !prevInteraction.status.synced) { updated.add(key); } }); return updated; }); setInteractions(cloneDeep(props.interactions)); } }, [components, prevComponents, props.interactions, prevInteractions]); const openInteractionSettings = (interaction: Interaction) => { setSelectedInteraction(interaction); }; const closeInteractionSettings = () => { setSelectedInteraction(undefined); }; const interactionConditions = ( source: Component | undefined, conditions: pond.IInteractionCondition[] | null | undefined, interactionIndex: number ) => { let sourceType = source && source.settings.type; let sourceSubtype = source && source.settings.subtype; let items: any[] = []; if (!conditions) return items; conditions.forEach((condition: pond.IInteractionCondition, conditionIndex: number) => { let measurementType = condition.measurementType; let measurement = describeMeasurement(condition.measurementType, sourceType, sourceSubtype, undefined, user); items.push( {interactionConditionText(source, condition, conditionIndex > 0, user)} {measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BOOLEAN ? ( updateConditionValue( source, interactionIndex, conditionIndex, parseInt(value) === 1 ? 1 : 0 ) }> {measurement.convertWithUnits(0)} {measurement.convertWithUnits(1)} ) : ( updateConditionValue(source, interactionIndex, conditionIndex, value) } /> )} ); }); return items; }; const updateConditionValue = ( source: Component | undefined, interactionIndex: number, conditionIndex: number, value: number | number[] ) => { if (interactions.length > interactionIndex) { let updatedDirtyInteractions = cloneDeep(dirtyInteractions); let updatedInteractions = cloneDeep(interactions); let interaction = updatedInteractions[interactionIndex]; let conditions = interaction.settings.conditions; if (conditions.length > conditionIndex) { if (typeof value === "number") { let condition = conditions[conditionIndex]; let sourceType = source && source.settings.type; let sourceSubtype = source && source.settings.subtype; let describer = describeMeasurement(condition.measurementType, sourceType, sourceSubtype, undefined, user); condition.value = describer.toStored(value); updatedDirtyInteractions.set(interactionIndex, true); } setInteractions(updatedInteractions); setDirtyInteractions(updatedDirtyInteractions); setRenderToggle(!renderToggle); } } }; const submitInteraction = ( deviceID: number, index: number, settings: pond.IInteractionSettings ) => { interactionsAPI .updateInteraction(Number(deviceID), settings, as) .then((_response: any) => { let updatedDirtyInteractions = cloneDeep(dirtyInteractions); updatedDirtyInteractions.set(index, false); setDirtyInteractions(updatedDirtyInteractions); setInteractions(prev => { const updated = cloneDeep(prev); if (updated[index]) { updated[index].status.synced = false; updated[index].status.lastUpdate = moment().toISOString(); } return updated; }); const interactionKey = settings.key ?? ""; if (interactionKey) { setAcceptedInteractions(prev => { const updated = new Set(prev); updated.delete(interactionKey); return updated; }); } success("Successfully updated the interaction for " + component.name()); }) .catch((_err: any) => { error("Error occurred while updating the interaction for " + component.name()); }); }; const cancel = (index: number) => { let updatedDirtyInteractions = cloneDeep(dirtyInteractions); updatedDirtyInteractions.set(index, false); setDirtyInteractions(updatedDirtyInteractions); setInteractions(cloneDeep(props.interactions)); }; const interactionItem = (interaction: Interaction, index: number) => { let key = interaction.key(); let source = mappedComponents.get(componentIDToString(interaction.settings.source)); let sink = mappedComponents.get(componentIDToString(interaction.settings.sink)); const isAccepted = acceptedInteractions.has(interaction.key()); const isPending = !interaction.status.synced; let statusText = isAccepted ? "Pending change accepted" : isPending ? "Pending " + moment(interaction.status.lastUpdate).fromNow() : ""; let schedule = pond.InteractionSchedule.create( interaction.settings.schedule !== null ? interaction.settings.schedule : undefined ); let timezone = schedule && schedule.timezone ? schedule.timezone : moment.tz.guess(); let todStart = (schedule.timeOfDayStart ? schedule.timeOfDayStart : "00:00").split(":"); let todEnd = (schedule.timeOfDayEnd ? schedule.timeOfDayEnd : "23:59").split(":"); let start = moment .tz(timezone) .startOf("day") .add(todStart[0], "hours") .add(todStart[1], "minutes") .local(); let end = moment .tz(timezone) .startOf("day") .add(todEnd[0], "hours") .add(todEnd[1], "minutes") .local(); let descriptor: "allDay" | "before" | "after" | "from" = timeOfDayDescriptor(schedule); let showStart = descriptor === "after" || descriptor === "from"; let showEnd = descriptor === "before" || descriptor === "from"; let showBoth = showStart && showEnd; let everyDay = schedule.weekdays && schedule.weekdays.length === 7; let action = ; if (canWrite(permissions)) { action = ( openInteractionSettings(interaction)}> ); } return ( ) : ""} subheaderTypographyProps={{ variant: "caption" }} action={action} /> {interaction.settings.conditions && ( {interactionConditions(source, interaction.settings.conditions, index)} )} {(descriptor !== "allDay" || !everyDay) && ( {!everyDay ? abbreviatedWeekdays(schedule) : "Operational"}{" "} {descriptor !== "allDay" && friendlyTimeOfDayDescriptor(descriptor)}{" "} {showStart && start.format("h:mma")} {showBoth && "-"}{" "} {showEnd && end.format("h:mma")} )} {dirtyInteractions.get(index) && ( )} ); }; const interactionList = (component: Component, interactions: Interaction[]) => { let items: any = []; interactions.forEach((interaction: Interaction, index: number) => { if (isSource(component.location(), interaction)) { items.push(interactionItem(interaction, index)); } }); if (items.length <= 0) { return null; } return items; }; return ( {interactionList(component, interactions)} closeInteractionSettings()} refreshCallback={refreshCallback} canEdit={canWrite(permissions)} /> ); }