Merge branch 'staging_environment'

This commit is contained in:
Carter 2025-10-22 10:36:43 -06:00
commit dd50fdc723
42 changed files with 1696 additions and 400 deletions

View file

@ -810,7 +810,8 @@ export default function Bin(props: Props) {
</Grid>
<Grid size={{ xs: 12, sm: 12, md: 12, lg: 2.6 }}>
{(bin.settings.mode === pond.BinMode.BIN_MODE_DRYING ||
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING) && (
bin.settings.mode === pond.BinMode.BIN_MODE_HYDRATING ||
bin.settings.mode === pond.BinMode.BIN_MODE_COOLDOWN) && (
<BinConditioningCard
mode={bin.settings.mode}
interactions={interactions}

View file

@ -345,7 +345,6 @@ export default function Bins(props: Props) {
let b = resp.data.bins.map(b => Bin.any(b));
b.forEach(bin => {
if (bin.empty()) {
console.log(bin.name())
empty.push(bin);
} else if (bin.storage() === pond.BinStorage.BIN_STORAGE_FERTILIZER) {
fert.push(bin);

View file

@ -208,6 +208,118 @@ export default function Devices() {
})
}, [tab])
function compareTimestamps(timestamp1: string, timestamp2: string): number {
const date1 = new Date(timestamp1);
const date2 = new Date(timestamp2);
// Check if dates are valid
if (isNaN(date1.getTime()) || isNaN(date2.getTime())) {
throw new Error("Invalid RFC3339 timestamp format");
}
// Compare using getTime() for millisecond precision
return date1.getTime() - date2.getTime();
}
const doesDeviceHavePlenum = (device: pond.Device) => {
if (device.status?.plenum?.temperature && device.status?.plenum?.timestamp) {
if (device.status.plenum?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.plenum?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveSen5x = (device: pond.Device) => {
if (device.status?.sen5x?.temperature && device.status?.sen5x?.timestamp) {
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.sen5x?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
// if (device.status?.o2) setHasO2(true)
// if (device.status?.co) setHasCo(true)
// if (device.status?.co2) setHasCo2(true)
// if (device.status?.no2) setHasNo2(true)
const doesDeviceHaveO2 = (device: pond.Device) => {
if (device.status?.o2?.ppm && device.status?.o2?.timestamp) {
if (device.status.sen5x?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.o2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveCo = (device: pond.Device) => {
if (device.status?.co?.ppm && device.status?.co?.timestamp) {
if (device.status.co?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.co?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveCo2 = (device: pond.Device) => {
if (device.status?.co2?.ppm && device.status?.co2?.timestamp) {
if (device.status.co2?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.co2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
const doesDeviceHaveNo2 = (device: pond.Device) => {
if (device.status?.no2?.ppm && device.status?.no2?.timestamp) {
if (device.status.no2?.timestamp) {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (compareTimestamps(device.status.no2?.timestamp, oneDayAgo.toISOString()) < 0) {
return true
}
} else {
return false
}
}
return false
}
useEffect(() => {
console.log(hasPlenums)
}, [hasPlenums])
const openProvisionDialog = () => {
setIsProvisionDialogOpen(true);
};
@ -319,12 +431,13 @@ export default function Devices() {
).then(resp => {
let newDevices: Device[] = []
resp.data.devices.forEach(device => {
if (device.status?.plenum?.temperature) setHasPlenums(true)
if (device.status?.sen5x?.temperature) setHasSen5x(true)
if (device.status?.o2) setHasO2(true)
if (device.status?.co) setHasCo(true)
if (device.status?.co2) setHasCo2(true)
if (device.status?.no2) setHasNo2(true)
setHasPlenums(doesDeviceHavePlenum(device))
setHasPlenums(true)
setHasSen5x(doesDeviceHaveSen5x(device))
setHasO2(doesDeviceHaveO2(device))
setHasCo(doesDeviceHaveCo(device))
setHasCo2(doesDeviceHaveCo2(device))
setHasNo2(doesDeviceHaveNo2(device))
newDevices.push(Device.create(device))
})
setDevices(newDevices)
@ -809,12 +922,12 @@ export default function Devices() {
).then(resp => {
let newDevices: Device[] = []
resp.data.devices.forEach(device => {
if (device.status?.plenum?.temperature) setHasPlenums(true)
if (device.status?.sen5x?.temperature) setHasSen5x(true)
if (device.status?.o2) setHasO2(true)
if (device.status?.co) setHasCo(true)
if (device.status?.co2) setHasCo2(true)
if (device.status?.no2) setHasNo2(true)
setHasPlenums(doesDeviceHavePlenum(device))
setHasSen5x(doesDeviceHaveSen5x(device))
setHasO2(doesDeviceHaveO2(device))
setHasCo(doesDeviceHaveCo(device))
setHasCo2(doesDeviceHaveCo2(device))
setHasNo2(doesDeviceHaveNo2(device))
newDevices.push(Device.create(device))
})
setDevices(currentRows.concat(newDevices))

314
src/pages/Field.tsx Normal file
View file

@ -0,0 +1,314 @@
import { Field, fieldScope, HarvestPlan, teamScope } from "models";
import { useFieldAPI, useGlobalState, useHarvestPlanAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useMobile, useSnackbar, useUserAPI } from "hooks";
import { Box, Card, Grid2, IconButton, Tab, Tabs, Theme, Typography, darken } from "@mui/material";
import FieldActions from "field/FieldActions";
import { Settings } from "@mui/icons-material";
import { pond } from "protobuf-ts/pond";
import FieldMinimap from "field/Fieldminimap";
import Weather from "weather/weather";
import TaskViewer from "tasks/TaskViewer";
import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay";
import { cloneDeep } from "lodash";
import HarvestPlanTable from "harvestPlan/HarvestPlanTable";
import { makeStyles } from "@mui/styles";
import FieldSettings from "field/FieldSettings";
const useStyles = makeStyles((theme: Theme) => {
return ({
card: {
height: "100%",
bgcolor: darken(theme.palette.background.paper, 0.05),
},
sectionHeader: {
fontSize: 35,
fontWeight: 650
}
})
})
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
aria-labelledby={`simple-tab-${index}`}
style={{ height: "94%", paddingTop: "15px" }}
{...other}>
{value === index && <React.Fragment>{children}</React.Fragment>}
</div>
);
}
export default function FieldPage() {
const { state } = useLocation();
//const [{ as }] = useGlobalState()
const classes = useStyles();
const fieldAPI = useFieldAPI();
const userAPI = useUserAPI();
const [{as, user}] = useGlobalState();
const {openSnack} = useSnackbar();
const fieldID = useParams<{ fieldID: string }>()?.fieldID ?? "";
const [field, setField] = useState<Field>(state?.field ? Field.create(state.field) : Field.create())
const [permissions, setPermissions] = useState<pond.Permission[]>(state?.permissions ? state.permissions : [])
const isMobile = useMobile()
const [openFieldSettings, setOpenFieldSettings] = useState(false)
const [planLoading, setPlanLoading] = useState(false)
const [hPlan, setHPlan] = useState<HarvestPlan>(HarvestPlan.create());
const [loading, setLoading] = useState(false)
const hPlanAPI = useHarvestPlanAPI();
const [tabVal, setTabVal] = useState(0)
const navigate = useNavigate()
const loadField = useCallback(() => {
if(state.field){
fieldAPI.getField(state.field).then(resp => {
setField(Field.create(resp.data))
}).catch(err => {
})
}
},[as])
const goTo = () => {
navigate("/fields");
}
useEffect(()=>{
// if already loading do nothing
if(loading) return
//if the field is passed through in the state just use it
if(state?.field){
let field = Field.create(state.field)
field.permissions = state.permissions
setField(field)
setPermissions(state.permissions)
return
}
//otherwise load the field
setLoading(true)
fieldAPI.getField(fieldID, as).then(resp => {
setField(Field.create(resp.data))
}).catch(err => {
openSnack("Failed to load Field")
}).finally(() => {
setLoading(false)
})
//also get the permissions
let scope = fieldScope(fieldID);
if (as) {
scope = teamScope(as)
}
userAPI.getUser(user.id(), scope).then(resp => {
let f = cloneDeep(field)
f.permissions = resp.permissions
setField(f)
setPermissions(resp.permissions);
});
},[loadField, fieldID, as])
useEffect(() => {
if (field.key() !== "") {
hPlanAPI
.listHarvestPlans(1, 0, "desc", "createDate", field.key(), as)
.then(resp => {
if (resp.data.harvestPlan.length > 0) {
let plan = resp.data.harvestPlan[0];
setHPlan(HarvestPlan.any(plan));
} else {
setHPlan(HarvestPlan.create());
}
setPlanLoading(false);
})
.catch(err => {
//openSnack("Failed to load plan");
});
}
}, [field, as, hPlanAPI]);
const fieldActions = () => {
return (
<Grid2 container direction={"row"} justifyContent="space-between" alignContent="center" alignItems="center">
<Grid2>
<Typography className={classes.sectionHeader}>
{field.name()}
</Typography>
</Grid2>
<Grid2>
<IconButton
onClick={() => {
setOpenFieldSettings(true)
}}><Settings /></IconButton>
<FieldActions field={field} permissions={permissions} refreshCallback={()=>{}}/>
</Grid2>
</Grid2>
)
}
const map = () => {
return (
<Box minHeight={550} height="100%">
<FieldMinimap field={field} borderSpacing={0.001} squared/>
</Box>
)
}
const weather = () => {
return (<Weather longitude={field.center().longitude} latitude={field.center().latitude} />)
}
const harvestPlans = () => {
return (
<HarvestPlanDisplay
plan={hPlan}
permissions={permissions}
planField={field}
loading={planLoading}
changePlan={updatedPlan => {
if (updatedPlan) {
setHPlan(updatedPlan);
}
}}
/>)
}
const tasks = () => {
let taskLoadKeys: string[] = [];
if (!planLoading) {
field.key() !== "" && taskLoadKeys.push(field.key());
//hPlan.key() !== "" && taskLoadKeys.push(hPlan.key());
}
return (<TaskViewer drawerView objectKey={field.key()} loadKeys={taskLoadKeys} overlayButton={isMobile}/>)
}
const desktopView = () => {
return (
<React.Fragment>
<Box>
<Grid2 container spacing={1}>
<Grid2 size={4}>
<Card raised className={classes.card}>
{map()}
</Card>
</Grid2>
<Grid2 size={3}>
<Card raised className={classes.card}>
{weather()}
</Card>
</Grid2>
<Grid2 size={5}>
<Card raised className={classes.card}>
{tasks()}
</Card>
</Grid2>
</Grid2>
</Box>
<Box>
<Typography className={classes.sectionHeader}>
Harvest Overview
</Typography>
<Grid2 container spacing={1}>
<Grid2 size={4}>
<Card raised className={classes.card}>
{harvestPlans()}
</Card>
</Grid2>
<Grid2 size={8}>
<Card raised>
<HarvestPlanTable field={field}/>
</Card>
</Grid2>
</Grid2>
</Box>
</React.Fragment>
)
}
const mobileView = () => {
return (
<React.Fragment>
<Tabs
value={tabVal}
onChange={(_, val) => {
setTabVal(val)
}}
TabIndicatorProps={{ style: { background: "rgba(255,255,0,255)" } }}>
<Tab label="Overview" />
<Tab label="Activities" />
<Tab label="Harvest" />
</Tabs>
<TabPanel value={tabVal} index={0}>
<Grid2 container spacing={2} direction="column">
<Grid2 size={12}>
<Card raised sx={{height: "50vh"}}>
{map()}
</Card>
</Grid2>
<Grid2 size={12}>
<Card raised>
{weather()}
</Card>
</Grid2>
</Grid2>
</TabPanel>
<TabPanel value={tabVal} index={1}>
<Card raised>
{tasks()}
</Card>
</TabPanel>
<TabPanel value={tabVal} index={2}>
<Grid2>
<Grid2 size={12}>
<Card raised className={classes.card}>
{harvestPlans()}
</Card>
</Grid2>
<Grid2>
<HarvestPlanTable field={field}/>
</Grid2>
</Grid2>
</TabPanel>
</React.Fragment>
)
}
const fieldSettings = () => {
return (
<FieldSettings
open={openFieldSettings}
onClose={() => {
setOpenFieldSettings(false)
}}
selectedField={field}
updateFields={() => {
loadField()
}}
removeField={()=>{
goTo()
}}
/>
)
}
return (
<PageContainer>
<Box padding={2}>
{fieldActions()}
{isMobile ? mobileView() : desktopView()}
</Box>
{fieldSettings()}
</PageContainer>
)
}

View file

@ -39,6 +39,7 @@ export default function Heaters() {
objectHeaterAPI
.listObjectHeatersPageData(pageSize, tablePage * pageSize, undefined, undefined, searchText, as)
.then(resp => {
// setHeaters(resp.data.heaterData ?? []);
setTotal(resp.data.total)
setHeaters(resp.data.heaterData ?? []);
});

View file

@ -6,7 +6,7 @@ export default function Tasks() {
return (
<PageContainer>
<Box padding={2}>
<TaskViewer />
<TaskViewer overlayButton/>
</Box>
</PageContainer>
);