Merge branch 'staging_environment' into field_dashboard

This commit is contained in:
csawatzky 2025-08-20 10:55:26 -06:00
commit 1028f0639b
38 changed files with 992 additions and 231 deletions

View file

@ -24,7 +24,7 @@ import RemoveSelfFromObject from "user/RemoveSelfFromObject";
import ShareObject from "user/ShareObject";
import { isOffline } from "utils/environment";
import ObjectTeams from "teams/ObjectTeams";
// import BinDuplication from "./BinDuplication";
import BinDuplication from "./BinDuplication";
import BinsIcon from "products/Bindapt/BinsIcon";
import HelpIcon from "@mui/icons-material/Help";
import BinSensors from "./BinSensors";
@ -269,14 +269,14 @@ export default function BinActions(props: Props) {
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
refreshCallback={refreshCallback}
/>
{/* <BinDuplication
<BinDuplication
open={openState.duplication}
closeDialog={() => {
setOpenState({ ...openState, duplication: false });
}}
bin={bin}
refreshCallback={refreshCallback}
/> */}
/>
<RemoveSelfFromObject
scope={binScope(key)}
label={label}

View file

@ -506,9 +506,6 @@ export default function BinCard(props: Props) {
+
</IconButton>
)}
{/* <Box position="absolute" top={4} right={4}>
<BinModeDot mode={bin.settings.mode} />
</Box> */}
<Box padding={1}>
<Typography
align="left"

View file

@ -0,0 +1,72 @@
import {
Button,
DialogActions,
DialogContent,
DialogTitle,
TextField,
Typography
} from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import { Bin } from "models";
import { useBinAPI, useSnackbar } from "providers";
import { useEffect, useState } from "react";
interface Props {
open: boolean;
bin: Bin;
closeDialog: () => void;
refreshCallback: () => void;
}
export default function BinDuplication(props: Props) {
const { open, bin, closeDialog, refreshCallback } = props;
const [binDupName, setBinDupName] = useState("(copy of) " + bin.name());
const binAPI = useBinAPI();
const { openSnack } = useSnackbar();
useEffect(() => {
setBinDupName("(copy of) " + bin.name());
}, [bin]);
const duplicate = () => {
let settings = bin.settings;
settings.name = binDupName;
binAPI
.addBin(settings)
.then(resp => {
openSnack("Successfully duplicated bin");
})
.catch(err => {
openSnack("Failed to duplicate bin");
});
refreshCallback();
closeDialog();
};
return (
<ResponsiveDialog open={open} onClose={closeDialog}>
<DialogTitle>Create Duplicate Bin</DialogTitle>
<DialogContent>
<Typography>
This will create a new bin with the same settings as {bin.name()}.
</Typography>
<TextField
id={"duplicateName"}
fullWidth
margin="normal"
label="Name of Copy"
value={binDupName}
onChange={e => setBinDupName(e.target.value)}
/>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} style={{ color: "red" }}>
Cancel
</Button>
<Button onClick={duplicate} color="primary">
Confirm
</Button>
</DialogActions>
</ResponsiveDialog>
);
}

View file

@ -58,7 +58,7 @@ import { Bin } from "models";
import moment from "moment";
import { GetBinShapeDescribers } from "pbHelpers/Bin";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useBinYardAPI, useGlobalState } from "providers";
import { useBinAPI, useBinYardAPI, useGlobalState, useLibraCartProxyAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
// import { useHistory } from "react-router";
import { getDistanceUnit, or, getTemperatureUnit, getGrainUnit } from "utils";
@ -176,7 +176,7 @@ export default function BinSettings(props: Props) {
const grainOptions = GrainOptions();
const grainUseOptions = GetGrainUseOptions();
const [inputCapacity, setInputCapacity] = useState<string>("");
const [{ as }] = useGlobalState();
const [{ user, as }] = useGlobalState();
const [grainDiff, setGrainDiff] = useState(0);
const [isCustomInventory, setIsCustomInventory] = useState<boolean>(false);
const [grainUpdate, setGrainUpdate] = useState(false);
@ -197,10 +197,13 @@ export default function BinSettings(props: Props) {
const [inventoryControl, setInventoryControl] = useState<pond.BinInventoryControl>(
pond.BinInventoryControl.BIN_INVENTORY_CONTROL_UNKNOWN
);
const [storageType, setStorageType] = useState<pond.BinStorage>(
pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN
);
//libracart stuff
const libracartAPI = useLibraCartProxyAPI()
const [lcDestination, setlcDestination] = useState<Option | null>()
const [lcDestinationOptions, setlcDestinationOptions] = useState<Option[]>([])
useEffect(() => {
if (open) {
@ -365,9 +368,33 @@ export default function BinSettings(props: Props) {
}
}, [binYardAPI, form.yardKey, userID, as, props.binYards]);
const loadLibraCartDestinations = useCallback(()=>{
if(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART){
//load the destinations for the user/team they are viewing as
let options: Option[] = []
libracartAPI.listDestinations(0,0,undefined, as)
.then(resp=>{
//set the options for the search select
resp.data.destinations.forEach(d => {
let newOp: Option = {
label: d.name,
value: d.id
}
options.push(newOp)
//set the current option based on the key in the form
if (d.id === form.libracartDestinationKey){
setlcDestination(newOp)
}
})
setlcDestinationOptions(options)
}).catch(()=>{})
}
},[libracartAPI, inventoryControl, as, form.libracartDestinationKey])
useEffect(() => {
loadBinYards();
}, [loadBinYards]);
loadLibraCartDestinations();
}, [loadBinYards, loadLibraCartDestinations]);
useEffect(() => {
if (bin?.settings.specs?.bushelCapacity) {
@ -713,6 +740,8 @@ export default function BinSettings(props: Props) {
return "Auto (cable)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART:
return "Auto (LibraCart)"
default:
return "Manual"
}
@ -860,7 +889,7 @@ export default function BinSettings(props: Props) {
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC}
control={<Radio />}
label={"Auto"}
label={"Auto (Cable)"}
/>
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR}
@ -872,6 +901,13 @@ export default function BinSettings(props: Props) {
control={<Radio />}
label={"Hybrid (Lidar)"}
/>
{user.hasFeature("libra-cart") &&
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART}
control={<Radio />}
label={"Auto (LibraCart)"}
/>
}
</RadioGroup>
</AccordionDetails>
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC &&
@ -912,6 +948,25 @@ export default function BinSettings(props: Props) {
/>
</Box>
}
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART &&
<Box width="100%" padding={2}>
<SearchSelect
label="LibraCart Destination"
selected={lcDestination}
changeSelection={option => {
let newForm = form;
newForm.libracartDestinationKey = option?.value;
setForm(newForm);
setlcDestination(option ? option : null);
}}
disabled={!canEdit}
options={lcDestinationOptions}
/>
<Typography variant="caption">
The linked bins inventory will be adjusted When the LibraCart Destination data is synced every 6 hours
</Typography>
</Box>
}
</Accordion>
{empty ? (
<Box marginTop={3} display="flex" flexDirection="column" alignItems="center">

View file

@ -30,7 +30,6 @@ const useStyles = makeStyles((_theme) => {
position: "relative",
minHeight: "233px",
height: "auto !important",
width: "184px",
padding: 2
},
hidden: {
@ -113,7 +112,7 @@ export default function BinsList(props: Props) {
return (
<Grid container direction="row">
{bins.map((b, i) =>
isMobile ? (
(
<Grid
size={{
xs: 6,
@ -133,21 +132,6 @@ export default function BinsList(props: Props) {
valDisplay={valDisplay}
/>
</Grid>
) : (
<Box
key={i}
style={{ width: "184px" }}
className={classes.gridListTile}
onClick={() => {
!duplicate && goToBin(i)
}}>
<BinCardV2
bin={b}
duplicateBin={duplicateBin}
dupHovered={setDuplicate}
valDisplay={valDisplay}
/>
</Box>
)
)}
</Grid>

View file

@ -0,0 +1,67 @@
import { useTheme } from "@mui/material";
import MaterialChartTooltip from "charts/MaterialChartTooltip";
import moment from "moment";
import { Area, AreaChart, ResponsiveContainer, Tooltip, TooltipProps, XAxis, YAxis } from "recharts";
export interface LevelAreaData {
timestamp: number;
value: number;
}
interface Props {
data: LevelAreaData[]
fill: string
customHeight?: string | number
}
export default function BinLevelAreaGraph(props: Props) {
const { data, customHeight, fill } = props
const theme = useTheme();
const now = moment();
return (
<ResponsiveContainer width={"100%"} height={customHeight ?? 350}>
<AreaChart data={data}>
<XAxis
allowDataOverflow
dataKey="timestamp"
domain={["dataMin", "dataMax"]}
name="Time"
tickFormatter={timestamp => {
let t = moment(timestamp);
return now.isSame(t, "day") ? t.format("LT") : t.format("MMM DD");
}}
scale="time"
type="number"
tick={{ fill: theme.palette.text.primary }}
stroke={theme.palette.divider}
interval="preserveStartEnd"
/>
<YAxis />
<Area
dataKey={"value"}
fill={fill}
stroke={fill}
strokeWidth={3}
type="monotone"
/>
<Tooltip
animationEasing="ease-out"
cursor={{ fill: theme.palette.text.primary, opacity: "0.15" }}
labelFormatter={timestamp => moment(timestamp).format("lll")}
content={(props: TooltipProps<any, any>) => {
return (
<MaterialChartTooltip
{...props}
valueFormatter={value => {
return value + " Bushels";
}}
/>
);
}}
/>
</AreaChart>
</ResponsiveContainer>
)
}

View file

@ -1,21 +1,18 @@
import {
Box,
Card,
CircularProgress,
FormControlLabel,
Grid,
Switch,
Typography
} from "@mui/material";
import { blue, grey, red, yellow, orange } from "@mui/material/colors";
import BarGraph, { BarData, RefArea } from "charts/BarGraph";
import BarGraph, { BarData } from "charts/BarGraph";
import { Bin } from "models";
import moment, { Moment } from "moment";
import { pond, quack } from "protobuf-ts/pond";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react";
import { Legend } from "recharts";
import { getGrainUnit } from "utils";
import BinLevelAreaGraph from "./BinLevelAreaGraph";
interface Props {
binLoading: boolean;
@ -27,12 +24,16 @@ interface Props {
customHeight?: number | string;
}
interface InventoryAt {
timestamp: number;
value: number;
}
export default function BinLevelOverTime(props: Props) {
const { bin, fertilizerBin, colour, startDate, endDate, customHeight } = props;
const binAPI = useBinAPI();
const [{as}] = useGlobalState();
const [data, setData] = useState<BarData[]>([]);
const [modeAreas, setModeAreas] = useState<RefArea[]>([]);
const [inventoryData, setInventoryData] = useState<InventoryAt[]>([]);
const [dataLoading, setDataLoading] = useState(false);
const [capacity, setCapacity] = useState<number | undefined>();
@ -76,33 +77,15 @@ export default function BinLevelOverTime(props: Props) {
.listHistoryBetween(bin.key(), 500, startDate.toISOString(), endDate.toISOString())
.then(resp => {
let data: BarData[] = [];
let modeAreas: RefArea[] = [];
let lastBushels = -1;
//values for ref areas
let x1 = 0;
let x2 = 0;
let y1 = -50;
let y2 = -200;
let currentMode: pond.BinMode;
let currentMode: pond.BinMode | undefined = undefined
let modeData: BarData[] = []
resp.data.history.forEach(hist => {
//build the data for the inventory bar graph
if (hist.settings?.inventory) {
let settings = pond.BinSettings.fromObject(hist.settings);
let bushels = hist.settings.inventory.grainBushels ?? 0;
if (bushels !== lastBushels || currentMode !== settings.mode) {
x1 = moment(hist.timestamp).valueOf();
x2 = moment(hist.timestamp).valueOf();
modeAreas.push({
x1: x1,
x2: x2,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(settings.mode)
});
currentMode = settings.mode;
let newData: BarData = {
if (bushels !== lastBushels) {
let newData: InventoryAt = {
timestamp: moment(hist.timestamp).valueOf(),
value: fertilizerBin
? Math.round(bushels * 35.239)
@ -114,10 +97,21 @@ export default function BinLevelOverTime(props: Props) {
lastBushels = bushels;
}
}
//build the data for the mode change bar graph
let histBin = Bin.create()
histBin.settings = hist.settings ?? pond.BinSettings.create()
if(hist.settings && currentMode !== hist.settings.mode){
currentMode = hist.settings.mode
modeData.push({
timestamp: moment(hist.timestamp).valueOf(),
value: 1,
fill: getFill(currentMode)
})
}
});
let currentTime = moment().valueOf();
if (data.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0;
let currentTime = moment().valueOf();
let bushels = bin.bushels();
data.push({
value: fertilizerBin
? Math.round(bushels * 35.239)
@ -126,17 +120,16 @@ export default function BinLevelOverTime(props: Props) {
: bushels,
timestamp: currentTime
});
modeAreas.push({
x1: currentTime,
x2: currentTime,
y1: cap ? cap * -0.1 : y1,
y2: cap ? cap * -0.2 : y2,
fill: getFill(bin.settings.mode)
});
}
setData(data);
setModeAreas(modeAreas);
if(modeData.length === 0){
modeData.push({
timestamp: currentTime,
value: 1,
fill: getFill(bin.settings.mode)
})
}
setInventoryData(data);
setModeData(modeData)
})
.finally(() => {
setDataLoading(false);
@ -180,7 +173,7 @@ export default function BinLevelOverTime(props: Props) {
});
});
if (autoBarData.length === 0) {
let bushels = bin.settings.inventory?.grainBushels ?? 0;
let bushels = bin.bushels();
let currentTime = moment().valueOf();
autoBarData.push({
value: fertilizerBin
@ -191,7 +184,7 @@ export default function BinLevelOverTime(props: Props) {
timestamp: currentTime
});
}
setData(autoBarData);
setInventoryData(autoBarData);
})
.catch(err => {})
.finally(() => {
@ -220,6 +213,13 @@ export default function BinLevelOverTime(props: Props) {
})
}
})
if(modeData.length === 0){
modeData.push({
timestamp: moment().valueOf(),
value: 1,
fill: getFill(bin.settings.mode)
})
}
setModeData(modeData)
})
.catch(err => {
@ -238,7 +238,8 @@ export default function BinLevelOverTime(props: Props) {
let control = bin.inventoryControl()
//for automatic lidar and cables get the data from measurements as an object measurement
if(control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC ||
control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART
){
//load the measurement data for the bar graph
loadMeasurementData()
@ -270,23 +271,30 @@ export default function BinLevelOverTime(props: Props) {
};
const inventoryChart = () => {
return (
<BarGraph
if(inventoryData.length > 10){
return (
<BinLevelAreaGraph
data={inventoryData}
fill={colour}
/>
)
}else{
return (
<BarGraph
customHeight={customHeight}
data={data}
refAreas={modeAreas}
data={inventoryData}
barColour={colour}
yMax={capacity}
yMin={0}
graphLegend={modeAreas.length > 0 ? legend() : undefined}
labelColour="white"
labels
useGradient
/>
);
/>
);
}
};
const autoBinModeChart = () => {
const binModeChart = () => {
return (
<BarGraph
data={modeData}
@ -305,9 +313,7 @@ export default function BinLevelOverTime(props: Props) {
Grain Levels Over Time
</Typography>
{dataLoading ? <CircularProgress /> : inventoryChart()}
{/* when using auto will need to make a new chart just for the bin modes since the auto inventory comes from another place */}
{(bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) && autoBinModeChart()}
{binModeChart()}
</Card>
);
}