Merge branch 'master' into libracart

This commit is contained in:
csawatzky 2025-10-10 10:16:19 -06:00
commit 807c34cbc1
35 changed files with 638 additions and 308 deletions

View file

@ -71,6 +71,12 @@ http {
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Force re-cache of old service-worker
location = /service-worker.js {
root /usr/share/nginx/html; # adjust if your build folder is elsewhere
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# Redirect old /index.html requests to new file
location = /index.html {
return 301 /indexV2.html;

4
package-lock.json generated
View file

@ -42,7 +42,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#libracart",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
@ -10911,7 +10911,7 @@
},
"node_modules/protobuf-ts": {
"version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#8308e3c2a3bbc296d152b2df83246de3d0df4321",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#d0ac088df3822c10c0497f49173d1679b4e107b0",
"dependencies": {
"protobufjs": "^6.8.8"
}

View file

@ -54,7 +54,7 @@
"mui-tel-input": "^7.0.0",
"notistack": "^3.0.1",
"openweathermap-ts": "^1.2.10",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#libracart",
"protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#master",
"query-string": "^9.2.1",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",

24
public/service-worker.js Normal file
View file

@ -0,0 +1,24 @@
// public/service-worker.js
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', async () => {
// Unregister old CRA SW
const regs = await self.registration.scope ? [self.registration] : await self.clients.getRegistrations();
for (const reg of regs) {
if (reg && reg.scriptURL.endsWith('service-worker.js')) {
await reg.unregister();
}
}
// Clear all caches
if ('caches' in self) {
const keys = await caches.keys();
for (const key of keys) {
await caches.delete(key);
}
}
// Reload all clients
const clients = await self.clients.matchAll({ type: 'window' });
clients.forEach(client => client.navigate(client.url));
});

View file

@ -82,7 +82,20 @@ export default function UserWrapper(props: Props) {
firmware: new Map()
})
}).catch(() => {
setGlobal(globalDefault)
userAPI.getUser(user_id).then(user => {
setGlobal({
user: user ? user : User.create(),
team: globalDefault.team,
as: "",
showErrors: false,
userTeamPermissions: [],
backgroundTasksComplete: false,
firmware: new Map()
})
}).catch(() => {
setGlobal(globalDefault)
})
})
.finally(() => {
setLoading(false)

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

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

@ -103,8 +103,10 @@ export default function BinCard(props: Props) {
let filteredNodes = c.filteredNodes(true)
allTemps.push(...filteredNodes.temps);
allHums.push(...filteredNodes.humids);
allMoistures.push(...filteredNodes.moistures);
if(avg(c.humidities) !== 0){ //if the average of the humidities is 0 that means that all the readings are 0 and it is a temp only cable
allHums.push(...filteredNodes.humids);
allMoistures.push(...filteredNodes.moistures);
}
//set the hottest and coldest nodes
if (coldestNode === undefined || Math.min(...filteredNodes.temps) < coldestNode.temp) {
@ -490,7 +492,7 @@ export default function BinCard(props: Props) {
return (
<Card style={{ cursor: "pointer" }}>
{user.hasFeature("admin") && (
{user.hasFeature("installer") && (
<IconButton
style={{
height: 35,
@ -506,9 +508,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

@ -476,11 +476,11 @@ export default function BinSVGV2(props: Props) {
return cableGrainPath;
};
const nodeClass = (nodeTemp: number) => {
if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) {
const nodeClass = (nodeTemp: number, underTop: boolean) => {
if (hottestNodeTemp && hottestNodeTemp.toFixed(2) === nodeTemp.toFixed(2) && underTop) {
return classes.hotNode;
}
if (coldestNodeTemp && coldestNodeTemp.toFixed(2) === nodeTemp.toFixed(2)) {
if (coldestNodeTemp && coldestNodeTemp.toFixed(2) === nodeTemp.toFixed(2) && underTop) {
return classes.coldNode;
}
return classes.cableNode;
@ -507,12 +507,12 @@ export default function BinSVGV2(props: Props) {
//if the cables have the same number of nodes
if (b.temperatures.length === a.temperatures.length) {
//sort by temp only last and any type that has humidity first
if (
a.settings.subtype === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP &&
b.settings.subtype !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP
) {
if ((avg(a.humidities) === 0 || a.humidities.length === 0) && (avg(b.humidities) !== 0 || b.humidities.length > 0)) {
return 1;
} else {
} else if ((avg(b.humidities) === 0 || b.humidities.length === 0) && (avg(a.humidities) !== 0 || a.humidities.length > 0)) {
return -1;
}
else {
return a.key() > b.key() ? 1 : -1;
}
}
@ -759,7 +759,7 @@ export default function BinSVGV2(props: Props) {
return (
<g key={e.key}>
{e.nodeNumber === topNode && !e.excluded && topNodeHat(cablePos, e.y)}
<circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp)} r="16"></circle>
<circle cx={cablePos} cy={e.y} className={e.excluded ? classes.excludedNode : nodeClass(e.nodeTemp, e.nodeNumber <= topNode)} r="16"></circle>
</g>
)
}

View file

@ -374,7 +374,6 @@ export default function BinSettings(props: Props) {
let options: Option[] = []
libracartAPI.listDestinations(0,0,undefined, as)
.then(resp=>{
console.log(resp)
//set the options for the search select
resp.data.destinations.forEach(d => {
let newOp: Option = {
@ -743,7 +742,7 @@ export default function BinSettings(props: Props) {
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART:
return "Auto (LibraCart)"
return "Auto (Libra Cart)"
default:
return "Manual"
}
@ -907,7 +906,7 @@ export default function BinSettings(props: Props) {
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART}
control={<Radio />}
label={"Auto (LibraCart)"}
label={"Auto (Libra Cart)"}
/>
}
</RadioGroup>
@ -953,7 +952,7 @@ export default function BinSettings(props: Props) {
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART &&
<Box width="100%" padding={2}>
<SearchSelect
label="LibraCart Destination"
label="Libra Cart Destination"
selected={lcDestination}
changeSelection={option => {
let newForm = form;
@ -965,7 +964,7 @@ export default function BinSettings(props: Props) {
options={lcDestinationOptions}
/>
<Typography variant="caption">
The linked bins inventory will be adjusted When the LibraCart Destination data is synced every 6 hours
The linked bins inventory will be adjusted When the Libra Cart Destination data is synced every 6 hours
</Typography>
</Box>
}

View file

@ -250,6 +250,7 @@ export default function BinVisualizer(props: Props) {
const [cfmHighOpen, setCFMHighOpen] = useState(false);
const [{ user }] = useGlobalState();
const [newPreset, setNewPreset] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [newBinMode, setNewBinMode] = useState<pond.BinMode>(pond.BinMode.BIN_MODE_NONE);
const [selectedCable, setSelectedCable] = useState<GrainCable>();
const [openNodeDialog, setOpenNodeDialog] = useState(false);
const [cableDevice, setCableDevice] = useState<Device | undefined>();
@ -345,6 +346,7 @@ export default function BinVisualizer(props: Props) {
setGrainOption(ToGrainOption(bin.settings.inventory.grainType));
setBushPerTonne(bin.settings.inventory.bushelsPerTonne.toFixed(2));
setGrainSubtype(bin.subtype());
setNewBinMode(bin.settings.mode);
}
if (bin.settings) {
let t = bin.settings.outdoorTemp;
@ -384,8 +386,11 @@ export default function BinVisualizer(props: Props) {
//also reverse it so that the node 1 (end of the cable) is in the first position
let filteredNodes = cable.filteredNodes(true)
temps.push(...filteredNodes.temps)
humids.push(...filteredNodes.humids)
emcs.push(...filteredNodes.moistures)
//only push to the humidity values if the cable reads humidities
if(cable.subType() !== quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_TEMP){
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();
@ -409,14 +414,15 @@ export default function BinVisualizer(props: Props) {
//if the lowest temp for this cable is less than the temp in the low node conditions or the low node conditions are not set
//use this cables lowest node and trend data in the condition
if (!lowNodeConditions || Math.min(...temps) < lowNodeConditions.tempC) {
if (!lowNodeConditions || Math.min(...filteredNodes.temps) < lowNodeConditions.tempC) {
//determine which node is the coldest so that the data displayed is for the same node
let lowTempIndex =
temps.indexOf(Math.min(...temps)) === -1 ? 0 : temps.indexOf(Math.min(...temps));
filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.min(...filteredNodes.temps));
console.log(lowTempIndex)
lowNodeConditions = {
tempC: temps[lowTempIndex],
humidity: humids[lowTempIndex],
emc: emcs[lowTempIndex],
tempC: filteredNodes.temps[lowTempIndex],
humidity: filteredNodes.humids[lowTempIndex],
emc: filteredNodes.moistures[lowTempIndex],
tempCTrend: cableTrendData.coldNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.coldNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.coldNodeTrend?.emcTrend ?? 0
@ -424,13 +430,13 @@ export default function BinVisualizer(props: Props) {
}
//do the same for the high node
if (!highNodeConditions || cable.maxTemp() > highNodeConditions.tempC) {
if (!highNodeConditions || Math.max(...filteredNodes.temps) > highNodeConditions.tempC) {
let highTempIndex =
temps.indexOf(Math.max(...temps)) === -1 ? 0 : temps.indexOf(Math.max(...temps));
filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps)) === -1 ? 0 : filteredNodes.temps.indexOf(Math.max(...filteredNodes.temps));
highNodeConditions = {
tempC: temps[highTempIndex],
humidity: humids[highTempIndex],
emc: emcs[highTempIndex],
tempC: filteredNodes.temps[highTempIndex],
humidity: filteredNodes.humids[highTempIndex],
emc: filteredNodes.moistures[highTempIndex],
tempCTrend: cableTrendData.hotNodeTrend?.tempTrend ?? 0,
humidityTrend: cableTrendData.hotNodeTrend?.humidityTrend ?? 0,
emcTrend: cableTrendData.hotNodeTrend?.emcTrend ?? 0
@ -1967,6 +1973,7 @@ export default function BinVisualizer(props: Props) {
const setModeStorage = () => {
setNewPreset(pond.BinMode.BIN_MODE_STORAGE);
setNewBinMode(pond.BinMode.BIN_MODE_STORAGE);
};
const setModeCooldown = () => {
@ -1974,6 +1981,7 @@ export default function BinVisualizer(props: Props) {
return;
}
setNewPreset(pond.BinMode.BIN_MODE_COOLDOWN);
setNewBinMode(pond.BinMode.BIN_MODE_COOLDOWN);
};
const setModeDrying = () => {
@ -1982,13 +1990,16 @@ export default function BinVisualizer(props: Props) {
bin.settings.inventory?.initialMoisture < bin.settings.inventory?.targetMoisture
) {
setNewPreset(pond.BinMode.BIN_MODE_HYDRATING);
setNewBinMode(pond.BinMode.BIN_MODE_HYDRATING);
} else {
setNewPreset(pond.BinMode.BIN_MODE_DRYING);
setNewBinMode(pond.BinMode.BIN_MODE_DRYING);
}
};
const closeMoistureDialog = () => {
setNewPreset(0);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false);
};
@ -2049,6 +2060,7 @@ export default function BinVisualizer(props: Props) {
<Button
onClick={() => {
setNewPreset(0);
setNewBinMode(bin.settings.mode);
setShowInputMoisture(false);
}}
color="primary">
@ -2090,7 +2102,13 @@ export default function BinVisualizer(props: Props) {
}
if (b.settings.outdoorHumidity !== undefined)
b.settings.outdoorHumidity = Number(outdoorHumidityInput);
b.settings.mode = newPreset;
if (b.settings.mode != newBinMode){
if(b.settings.inventory){
b.settings.inventory.initialTimestamp = moment().format();
}
b.settings.mode = newBinMode;
}
binAPI.updateBin(bin.key(), b.settings, as).then(resp => {
refresh();

View file

@ -22,6 +22,7 @@ interface Props {
loadMore?: () => void;
//startingTranslate: number;
valDisplay?: "high" | "low" | "average";
insert?: boolean
}
const useStyles = makeStyles((_theme) => {
@ -30,7 +31,6 @@ const useStyles = makeStyles((_theme) => {
position: "relative",
minHeight: "233px",
height: "auto !important",
width: "184px",
padding: 2
},
hidden: {
@ -40,7 +40,7 @@ const useStyles = makeStyles((_theme) => {
});
export default function BinsList(props: Props) {
const { bins, duplicateBin, title, gridView, loadMore, valDisplay } = props;
const { bins, duplicateBin, title, gridView, loadMore, valDisplay, insert } = props;
const classes = useStyles();
// const history = useHistory();
const navigate = useNavigate()
@ -113,9 +113,14 @@ export default function BinsList(props: Props) {
return (
<Grid container direction="row">
{bins.map((b, i) =>
isMobile ? (
(
<Grid
size={{
size={insert ? {
xs: 6,
sm: 6,
md: 4,
lg: 3,
} : {
xs: 6,
sm: 4,
md: 3,
@ -133,21 +138,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

@ -316,6 +316,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={grainBins}
duplicateBin={duplicateBin}
@ -332,6 +333,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={fertBins}
duplicateBin={duplicateBin}
@ -348,6 +350,7 @@ export default function BinyardDisplay(props: Props) {
<Grid size={12}>
<BinsList
gridView
insert
valDisplay="average"
bins={emptyBins}
duplicateBin={duplicateBin}

View file

@ -184,7 +184,7 @@ export default function BinComponentGraph(props: Props) {
<UnitMeasurementSummary
component={component}
reading={UnitMeasurement.convertLastMeasurement(
component.status.lastGoodMeasurement.map(m => UnitMeasurement.create(m, user))
lastMeasurement.map(m => UnitMeasurement.create(m, user))
)}
/>
}

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 => {
@ -271,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}
@ -306,10 +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 ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_LIBRACART) && autoBinModeChart()}
{binModeChart()}
</Card>
);
}

File diff suppressed because one or more lines are too long

View file

@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => {
},
stickyHeader: {
position: "sticky",
// backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)",
backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(40, 40, 40)",
top: 0,
zIndex: 300 //giving a really high z-index to make sure nothing is in front of it
}

View file

@ -8,6 +8,7 @@ import {
MenuItem,
OutlinedInput,
Select,
TextField,
Theme
} from "@mui/material";
import moment, { Moment } from "moment";
@ -16,6 +17,7 @@ import ReactDOM from "react-dom";
import { DateRangePreset, SetDefaultPreset } from "./DateRange";
import ResponsiveDialog from "common/ResponsiveDialog";
import { withStyles, WithStyles } from "@mui/styles";
import { cloneDeep } from "lodash";
const styles = (_theme: Theme) => createStyles({});
@ -249,18 +251,30 @@ class DateSelect extends React.Component<Props, State> {
onClose={this.closeDateRangeDialog}
aria-labelledby="date-range-dialog">
<DialogContent>
{/* <StaticDateRangePicker
disableFuture
value={dateRange}
onChange={date => this.updateDateRange(date)}
renderInput={(startProps, endProps) => (
<React.Fragment>
<TextField {...startProps} />
<DateRangeDelimiter> to </DateRangeDelimiter>
<TextField {...endProps} />
</React.Fragment>
)}
/> */}
<TextField
fullWidth
margin="normal"
type="date"
label="Start Time"
value={dateRange[0]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[0] = moment(e.target.value)
this.updateDateRange(range)
}}
/>
<TextField
fullWidth
margin="normal"
type="date"
label="End Time"
value={dateRange[1]?.format("YYYY-MM-DD")}
onChange={e => {
let range = cloneDeep(dateRange)
range[1] = moment(e.target.value)
this.updateDateRange(range)
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={this.closeDateRangeDialog} color="primary">

View file

@ -28,6 +28,13 @@ export default function PeriodSelect(props: Props) {
}
}, [unit, prevUnit, onChange, value]);
useEffect(()=>{
let initialUnit: TimeUnit = bestUnit(initialMs);
let initialValue = milliToX(initialMs, initialUnit).toString();
setValue(initialValue)
setUnit(initialUnit)
},[initialMs])
const changeValue = (event: any) => {
let value = event.target.value;
setValue(value);

View file

@ -699,6 +699,86 @@ export default function ComponentForm(props: Props) {
);
};
const componentMode = () => {
const { component, coefficient, offset } = form;
return (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip
title="Switch to be able to customize the mode of the component"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="mode"
aria-label="mode"
color="secondary"
/>
}
label="Mode"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{compMode.mode.entryA && (
<TextField
id={compMode.mode.labelA}
label={compMode.mode.labelA}
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
margin="normal"
fullWidth
select={compMode.mode.entryA === "select"}
type={compMode.mode.entryA}
variant="outlined">
{compMode.mode.entryA === "select" &&
compMode.mode.dictionaryA.map((elem: any) => {
if (!elem.restricted || (elem.restricted && user.hasAdmin())) {
return (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
);
}
return undefined; //TODO-CS: Time permitting re-think the structure of this function so that I don't have to return undefined
})}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 5 }}>
{compMode.mode.entryB && (
<TextField
id={compMode.mode.labelB}
label={compMode.mode.labelB}
disabled={!component.settings.calibrate}
error={!isOffsetValid(compMode.mode.minB, compMode.mode.maxB)}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&
compMode.mode.dictionaryB.map((elem: any) => (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
))}
</TextField>
)}
</Grid>
</React.Fragment>
)
}
const describeSource = (measurementType: quack.MeasurementType): MeasurementDescriber => {
const { component } = form;
return describeMeasurement(measurementType, component.type(), component.subType());
@ -1046,87 +1126,17 @@ export default function ComponentForm(props: Props) {
</AccordionDetails>
</Accordion>
}
{!compMode ? (
device.featureSupported("individualCalibrations") ? (
individualCalibration()
) : (
blanketCalibration()
)
) : (
{device.featureSupported("individualCalibrations") ? (
<React.Fragment>
<Grid size={{ xs: 4, sm: 3 }}>
<Tooltip
title="Switch to be able to customize the mode of the component"
placement="bottom">
<FormControlLabel
control={
<Switch
checked={component.settings.calibrate}
onChange={toggleCalibrate}
name="mode"
aria-label="mode"
color="secondary"
/>
}
label="Mode"
labelPlacement="top"
className={classes.switchControl}
disabled={!canEdit}
/>
</Tooltip>
</Grid>
<Grid size={{ xs: 10, sm: 4 }}>
{compMode.mode.entryA && (
<TextField
id={compMode.mode.labelA}
label={compMode.mode.labelA}
disabled={!component.settings.calibrate}
error={!isCoefficientValid()}
value={coefficient}
onChange={changeCoefficient}
margin="normal"
fullWidth
select={compMode.mode.entryA === "select"}
type={compMode.mode.entryA}
variant="outlined">
{compMode.mode.entryA === "select" &&
compMode.mode.dictionaryA.map((elem: any) => {
if (!elem.restricted || (elem.restricted && user.hasAdmin())) {
return (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
);
}
return undefined; //TODO-CS: Time permitting re-think the structure of this function so that I don't have to return undefined
})}
</TextField>
)}
</Grid>
<Grid size={{ xs: 10, sm: 5 }}>
{compMode.mode.entryB && (
<TextField
id={compMode.mode.labelB}
label={compMode.mode.labelB}
disabled={!component.settings.calibrate}
error={!isOffsetValid(compMode.mode.minB, compMode.mode.maxB)}
value={offset}
onChange={changeOffset}
onFocus={selectText}
margin="normal"
fullWidth
type={compMode.mode.entryB}
variant="outlined">
{compMode.mode.entryB === "select" &&
compMode.mode.dictionaryB.map((elem: any) => (
<MenuItem key={elem.key} value={elem.value}>
{elem.key}
</MenuItem>
))}
</TextField>
)}
</Grid>
{compMode && componentMode()}
{individualCalibration()}
</React.Fragment>
) : (
!compMode ? (
blanketCalibration()
):(
componentMode()
)
)}
<TextField
id="dataAveraging"

View file

@ -38,6 +38,45 @@
]
}
},
{
"type": 12,
"subtype": 1,
"mode": {
"labelA": "Power Mode",
"entryA": "select",
"dictionaryA": [
{
"key": "High Power",
"value": 1,
"restricted": false
},
{
"key": "Low Power",
"value": 2,
"restricted": false
},
{
"key": "Calibration",
"value": 3,
"restricted": true
}
],
"labelB": "Number of Averages",
"entryB": "number",
"dictionaryB": [
{
"key": "min",
"value": 1,
"restricted": false
},
{
"key": "max",
"value": 8,
"restricted": false
}
]
}
},
{
"type": 28,
"subtype": 0,

View file

@ -190,7 +190,7 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
[
pond.Grain.GRAIN_OATS,
{
name: "Oats",
name: "Oats a",
group: "Oats",
equation: Equation.henderson,
a: 0.000085511,
@ -203,6 +203,40 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
}
],
[
pond.Grain.GRAIN_OATS_B,
{
name: "Oats b",
group: "Oats",
equation: Equation.chungPfost,
a: 442.85,
b: 0.21228,
c: 35.803,
setTempC: 32.0,
targetMC: 13.6,
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
}
],
[
pond.Grain.GRAIN_OATS_C,
{
name: "Oats c",
group: "Oats",
equation: Equation.oswin,
a: 12.412,
b: -0.060707,
c: 2.9397,
setTempC: 32.0,
targetMC: 13.6,
img: OatImg,
colour: "#79955a",
weightConversionKg: 15.4222988297197,
bushelsPerTonne: 64.842
}
],
[
pond.Grain.GRAIN_PEANUTS,
@ -506,6 +540,66 @@ export const GrainExtensions: Map<pond.Grain, GrainExtension> = new Map([
weightConversionKg: 27.2155,
bushelsPerTonne: 36.744
}
],[
pond.Grain.GRAIN_RYE_A,
{
name: "Rye a",
group: "Rye",
equation: Equation.henderson,
a: 0.00006343,
b: 2.2060,
c: 13.1810,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
}
],[
pond.Grain.GRAIN_RYE_B,
{
name: "Rye b",
group: "Rye",
equation: Equation.chungPfost,
a: 461.0230,
b: 0.1840,
c: 36.7410,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
},
],[
pond.Grain.GRAIN_RYE_C,
{
name: "Rye c",
group: "Rye",
equation: Equation.halsey,
a: 4.2970,
b: 0.380,
c: 2.2710,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
},
],[
pond.Grain.GRAIN_RYE_D,
{
name: "Rye d",
group: "Rye",
equation: Equation.oswin,
a: 11.8870,
b: 0.0210,
c: 3.2620,
setTempC: defaultSetTemp,
targetMC: 15.0,
colour: "grey",
weightConversionKg: 25.4,
bushelsPerTonne: 39.368
}
]
]);

View file

@ -34,8 +34,6 @@ export default function LibraCartAccess() {
//const [dataOps, setDataOps] = useState<pond.DataOption[]>([]);
const { openSnack } = useSnackbar();
const [{ as }] = useGlobalState();
// const integration_url = process.env.REACT_APP_LIBRACART_INTEGRATION_URL;
const integration_url = import.meta.env.VITE_LIBRACART_INTEGRATION_URL;
const submitNewOrganization = () => {
@ -184,7 +182,7 @@ export default function LibraCartAccess() {
color="primary"
onClick={e => {
e.preventDefault();
window.open(`${integration_url}`, "_blank");
window.open(`https://cloud.agrimatics.com/grain/integrations`, "_blank");
}}>
Link Libra Cart Account
</Button>

View file

@ -96,7 +96,7 @@ export class Bin {
}
public empty(): boolean {
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.settings.inventory.grainBushels < 5);
return this.settings.inventory?.empty || (this.settings.inventory !== undefined && this.settings.inventory !== null && this.bushels() < 5);
}
public objectType(): pond.ObjectType {

View file

@ -114,7 +114,7 @@ export class Component {
return 0;
}
public addressDescription = (deviceProduct?: pond.DeviceProduct) => {
public addressDescription(deviceProduct?: pond.DeviceProduct): string {
if (
this.settings.addressType === quack.AddressType.ADDRESS_TYPE_CONFIGURABLE_PIN_ARRAY ||
(this.settings.addressType >= quack.AddressType.ADDRESS_TYPE_PIN_OFFSET1 &&

View file

@ -273,7 +273,7 @@ export class GrainCable {
let filtered: number[] = []
values.forEach((val, index) => {
//if the node is not excluded AND is either under the top node OR top node is not set
if(!this.excludedNodes.includes(index) && (index <= this.topNode || this.topNode === 0)){
if(!this.excludedNodes.includes(index) && (index < this.topNode || this.topNode === 0)){
filtered.push(val)
}
})

View file

@ -70,19 +70,26 @@ const useStyles = makeStyles((theme: Theme) => ({
})
},
sideMenuOnClosed: {
transition: theme.transitions.create(["width", "z-index", "opacity"], {
transition: theme.transitions.create(["width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
overflowX: "hidden",
width: theme.spacing(7),
zIndex: theme.zIndex.drawer,
opacity: 0,
// overflowX: "hidden",
width: theme.spacing(0),
// zIndex: theme.zIndex.drawer,
// opacity: 0,
[theme.breakpoints.up("md")]: {
width: theme.spacing(9),
width: theme.spacing(9.25),
opacity: 1
}
},
sideMenuOnClosedMobile: {
transition: theme.transitions.create(["width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
width: theme.spacing(0),
},
list: {
paddingTop: 0
},
@ -504,7 +511,7 @@ export default function SideNavigator(props: Props) {
classes={{
paper: classNames(
classes.sideMenu,
open ? classes.sideMenuOpened : classes.sideMenuOnClosed
open ? classes.sideMenuOpened : (isMobile ? classes.sideMenuOnClosedMobile : classes.sideMenuOnClosed)
)
}}
anchor="left"

View file

@ -345,6 +345,7 @@ 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);
@ -1214,37 +1215,6 @@ export default function Bins(props: Props) {
}
]}
/>
{/* <StyledToggleButtonGroup
id="cardValueDisplay"
value={cardValDisplay}
exclusive
size="small"
aria-label="card detail">
<StyledToggle
value={"low"}
aria-label="low"
onClick={() => {
setCardValDisplay("low");
}}>
Low
</StyledToggle>
<StyledToggle
value={"average"}
aria-label="average"
onClick={() => {
setCardValDisplay("average");
}}>
Average
</StyledToggle>
<StyledToggle
value={"high"}
aria-label="high"
onClick={() => {
setCardValDisplay("high");
}}>
High
</StyledToggle>
</StyledToggleButtonGroup> */}
</Box>
</Grid>
<Grid >
@ -1270,41 +1240,6 @@ export default function Bins(props: Props) {
}
]}
/>
{/* <StyledToggleButtonGroup
id="tour-graph-tabs"
value={binView}
exclusive
size="small"
aria-label="detail">
<StyledToggle
value={"grid"}
aria-label="grid view"
onClick={() => {
setBinView("grid");
sessionStorage.setItem("binsView", "grid");
}}>
<ViewComfy />
</StyledToggle> */}
{/* hidden at dustins request so that grid view and list are the only two */}
{/* <StyledToggle
value={"scroll"}
aria-label="scroll view"
onClick={() => {
setBinView("scroll");
sessionStorage.setItem("binsView", "scroll");
}}>
<ViewColumn />
</StyledToggle> */}
{/* <StyledToggle
value={"list"}
aria-label="list view"
onClick={() => {
setBinView("list");
sessionStorage.setItem("binsView", "list");
}}>
<ViewList />
</StyledToggle>
</StyledToggleButtonGroup> */}
<MoreVert
className={classes.icon}
onClick={event => {

View file

@ -472,7 +472,7 @@ export default function Devices() {
]
if (hasPlenums) {
columns.push({
title: "Plenum",
title: "Temp/Humidity",
// sortKey: "hi",
// disableSort: true,
render: (device: Device) => {

View file

@ -129,7 +129,7 @@ export default function Users() {
"marketplace",
"installer",
"cnhi",
"libracart"
"libra-cart"
].sort();
const [rows, setRows] = useState<User[]>([])

View file

@ -71,16 +71,19 @@ export function describePower(power?: pond.DevicePower | null): PowerDescriber {
}
} else {
let thresholds = notChargingThresholds;
let suffix = "not charging";
if (power.inputVoltage >= 4) {
thresholds = chargingThresholds;
if (power.chargePercent === 100) {
suffix = "charged";
} else {
suffix = "charging";
}
}
result.description = power.chargePercent.toString() + "%, " + suffix;
// we would not actually know if it is charging here since it is just looking at the voltage now and not comparing to previous voltage
// let suffix = "not charging";
// if (power.inputVoltage >= 4) {
// thresholds = chargingThresholds;
// if (power.chargePercent === 100) {
// suffix = "charged";
// } else {
// suffix = "charging";
// }
// }
// result.description = power.chargePercent.toString() + "%, " + suffix;
// TODO: there is a plan in place to add a boolean value to power in the backend to have it check and set the boolean, we can then use that to know if it is charging or not
result.description = power.chargePercent.toString() + "%"
for (let i = 0; i < thresholds.length; i++) {
if (power.chargePercent >= thresholds[i].threshold) {
result.icon = thresholds[i].icon;

View file

@ -10,7 +10,7 @@ import {
Textsms as TextIcon,
Contrast,
} from "@mui/icons-material";
import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material";
import { AppBar, Box, Button, Checkbox, CircularProgress, Collapse, Divider, FormControl, FormControlLabel, FormGroup, FormHelperText, FormLabel, Grid2, IconButton, List, ListItem, ListItemButton, ListItemIcon, ListItemText, MenuItem, Radio, RadioGroup, Slider, TextField, Theme, ToggleButton, ToggleButtonGroup, Toolbar, Tooltip, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import UserAvatar from "./UserAvatar";
import { useGlobalState, useSnackbar, useUserAPI } from "providers";
@ -31,6 +31,7 @@ import UnitsIcon from "@mui/icons-material/Category";
import { setThemeType } from "theme";
import { IsAdaptiveAgriculture } from "services/whiteLabel";
import { setDistanceUnit, setGrainUnit, setPressureUnit, setTemperatureUnit } from "utils";
import FieldsIcon from "products/AgIcons/FieldsIcon";
const useStyles = makeStyles((theme: Theme) => {
return ({
@ -82,7 +83,9 @@ export default function UserSettings(props: Props) {
const [avatarExpanded, setAvatarExpanded] = useState<boolean>(false);
const [notificationsExpanded, setNotificationsExpanded] = useState<boolean>(false);
const [unitsExpanded, setUnitsExpanded] = useState<boolean>(false);
const [mapsExpanded, setMapsExpanded] = useState<boolean>(false);
const [avatarUrl, setAvatarUrl] = useState<string>("");
const [sliderVal, setSliderVal] = useState(globalState.user.settings.mapZoom);
// const [themeValue, setThemeValue] = useState<"light" | "dark" | "system">(themeMode.mode)
useEffect(() => {
@ -151,6 +154,30 @@ export default function UserSettings(props: Props) {
setUser(updatedUser);
};
const changeDefaultZoom = (value: number) => {
setSliderVal(value);
let updatedUser = User.clone(user);
updatedUser.settings.mapZoom = value;
setUser(updatedUser)
};
const mapSettings = () => {
return (
<Box display="flex">
<Typography>Map Zoom Level</Typography>
<Slider
valueLabelDisplay="auto"
value={sliderVal}
min={1}
max={16}
onChange={(_, val) => {
changeDefaultZoom(val as number);
}}
/>
</Box>
);
};
const generalSettings = () => {
const { name, email, phoneNumber, timezone } = user.settings;
@ -631,15 +658,15 @@ export default function UserSettings(props: Props) {
</List>
</Collapse>
{/* <Divider />
<Divider />
<ListItem button onClick={() => setMapsExpanded(!mapsExpanded)}>
<ListItemButton onClick={() => setMapsExpanded(!mapsExpanded)}>
<ListItemIcon>
<FieldsIcon />
</ListItemIcon>
<ListItemText primary={"Map Settings"} />
{mapsExpanded ? <ExpandLess /> : <ExpandMore />}
</ListItem>
</ListItemButton>
<Collapse in={mapsExpanded} timeout="auto" unmountOnExit>
<List component="div">
<ListItem>
@ -648,7 +675,7 @@ export default function UserSettings(props: Props) {
</ListItemText>
</ListItem>
</List>
</Collapse> */}
</Collapse>
</List>
);
};

View file

@ -17,6 +17,7 @@ import FeatureCard from "./FeatureCard";
//import { ImgIcon } from "common/ImgIcon";
//images to import for the image in the card for the feature make sure to have a 2:1 aspect ratio to keep the cards symmetrical
import FeatureImageTest from "assets/marketplaceImages/VisualFarmMPImage.png";
import LibraCartImage from "assets/marketplaceImages/Libra_Cart_temp.jpg";
// export interface FAQ {
// question: string;
@ -66,7 +67,7 @@ const agFeatureList: ProductDetails[] = [
featureName: "libra-cart",
featureLogo: <LibraCartIcon />,
featureCost: 0,
cardImage: FeatureImageTest,
cardImage: LibraCartImage,
featureTitle: "Libra Cart",
longDescription: "Integrate with Libra Cart to bring your data into our platform"
}