finished the supported grain view for desktop and the mobile view for both

This commit is contained in:
csawatzky 2026-01-06 10:41:07 -06:00
parent cd283a0ea1
commit eef687746e
2 changed files with 179 additions and 43 deletions

View file

@ -1,7 +1,7 @@
import {MenuItem, TextField } from "@mui/material" import {MenuItem, TextField } from "@mui/material"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import { pond } from "protobuf-ts/pond" import { pond } from "protobuf-ts/pond"
import React, { useEffect, useState } from "react" import React, { useEffect, useRef, useState } from "react"
interface Props { interface Props {
grainSettings?: pond.GrainSettings grainSettings?: pond.GrainSettings
@ -19,6 +19,7 @@ export default function CustomGrainForm(props: Props) {
const [constantC, setConstantC] = useState("0") const [constantC, setConstantC] = useState("0")
const [kgPerBushel, setKgPerBushel] = useState("0") const [kgPerBushel, setKgPerBushel] = useState("0")
const [bushelsPerTonne, setBushelsPerTonne] = useState("0") const [bushelsPerTonne, setBushelsPerTonne] = useState("0")
const valid = useRef(false)
useEffect(()=>{ useEffect(()=>{
if(grainSettings){ if(grainSettings){
@ -34,20 +35,20 @@ export default function CustomGrainForm(props: Props) {
} }
},[grainSettings]) },[grainSettings])
const invalid = () => { const validate = (name: string, group: string, constA: string, constB: string, constC: string, kgPerBushel: string, bushelsPerTonne: string) => {
if (name === "") return true if (name === "") return false
if (group === "") return true if (group === "") return false
if (isNaN(parseFloat(constantA))) return true if (isNaN(parseFloat(constA))) return false
if (isNaN(parseFloat(constantB))) return true if (isNaN(parseFloat(constB))) return false
if (isNaN(parseFloat(constantA))) return true if (isNaN(parseFloat(constC))) return false
if (isNaN(parseFloat(kgPerBushel))) return true if (isNaN(parseFloat(kgPerBushel))) return false
if (isNaN(parseFloat(bushelsPerTonne))) return true if (isNaN(parseFloat(bushelsPerTonne))) return false
return false return true
} }
const settingsChanged = (newSettings: pond.GrainSettings) => { const settingsChanged = (newSettings: pond.GrainSettings) => {
setNewGrainSettings(newSettings) setNewGrainSettings(newSettings)
onGrainSettingsChange(newSettings, !invalid()) onGrainSettingsChange(newSettings, valid.current)
} }
const grainForm = () => { const grainForm = () => {
@ -63,6 +64,7 @@ export default function CustomGrainForm(props: Props) {
setName(name) setName(name)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.name = name settings.name = name
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
settingsChanged(settings) settingsChanged(settings)
}}/> }}/>
<TextField <TextField
@ -75,6 +77,7 @@ export default function CustomGrainForm(props: Props) {
setGroup(group) setGroup(group)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.group = group settings.group = group
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
settingsChanged(settings) settingsChanged(settings)
}}/> }}/>
<TextField <TextField
@ -88,6 +91,7 @@ export default function CustomGrainForm(props: Props) {
setEquation(enumVal) setEquation(enumVal)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
settings.equation = enumVal settings.equation = enumVal
valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, bushelsPerTonne)
settingsChanged(settings) settingsChanged(settings)
}} }}
select> select>
@ -117,11 +121,12 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantA(val) setConstantA(val)
if(!isNaN(parseFloat(val))){ valid.current = validate(name, group, val, constantB, constantC, kgPerBushel, bushelsPerTonne)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.a = parseFloat(val) settings.a = parseFloat(val)
settingsChanged(settings)
} }
settingsChanged(settings)
}} }}
label="Constant A"/> label="Constant A"/>
<TextField <TextField
@ -134,11 +139,12 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantB(val) setConstantB(val)
if(!isNaN(parseFloat(val))){ valid.current = validate(name, group, constantA, val, constantC, kgPerBushel, bushelsPerTonne)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.b = parseFloat(val) settings.b = parseFloat(val)
settingsChanged(settings)
} }
settingsChanged(settings)
}} }}
label="Constant B"/> label="Constant B"/>
<TextField <TextField
@ -151,11 +157,12 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setConstantC(val) setConstantC(val)
if(!isNaN(parseFloat(val))){ valid.current = validate(name, group, constantA, constantB, val, kgPerBushel, bushelsPerTonne)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.c = parseFloat(val) settings.c = parseFloat(val)
settingsChanged(settings)
} }
settingsChanged(settings)
}} }}
label="Constant C"/> label="Constant C"/>
<TextField <TextField
@ -168,11 +175,12 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setKgPerBushel(val) setKgPerBushel(val)
if(!isNaN(parseFloat(val))){ valid.current = validate(name, group, constantA, constantB, constantC, val, bushelsPerTonne)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.kgPerBushel = parseFloat(val) settings.kgPerBushel = parseFloat(val)
settingsChanged(settings)
} }
settingsChanged(settings)
}} }}
label="kg per Bushel"/> label="kg per Bushel"/>
<TextField <TextField
@ -185,11 +193,12 @@ export default function CustomGrainForm(props: Props) {
onChange={(e) => { onChange={(e) => {
let val = e.target.value let val = e.target.value
setBushelsPerTonne(val) setBushelsPerTonne(val)
if(!isNaN(parseFloat(val))){ valid.current = validate(name, group, constantA, constantB, constantC, kgPerBushel, val)
let settings = cloneDeep(newGrainSettings) let settings = cloneDeep(newGrainSettings)
if(!isNaN(parseFloat(val))){
settings.bushelsPerTonne = parseFloat(val) settings.bushelsPerTonne = parseFloat(val)
settingsChanged(settings)
} }
settingsChanged(settings)
}} }}
label="Bushels per Tonne"/> label="Bushels per Tonne"/>
</React.Fragment> </React.Fragment>

View file

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import PageContainer from "./PageContainer"; import PageContainer from "./PageContainer";
import CustomGrainForm from "grain/CustomGrainForm"; import CustomGrainForm from "grain/CustomGrainForm";
import { Box, Button, DialogActions, DialogContent, DialogTitle, Grid2, Typography } from "@mui/material"; import { Accordion, AccordionDetails, AccordionSummary, Box, Button, DialogActions, DialogContent, DialogTitle, Divider, Grid2, IconButton, Typography } from "@mui/material";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import { useMobile, useSnackbar, useUserAPI } from "hooks"; import { useMobile, useSnackbar, useUserAPI } from "hooks";
@ -11,6 +11,8 @@ import { GetGrainExtensionMap, GetValidGrainExtensions, GrainExtension } from "g
import ResponsiveDialog from "common/ResponsiveDialog"; import ResponsiveDialog from "common/ResponsiveDialog";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import { Scope } from "models"; import { Scope } from "models";
import React from "react";
import { AddCircle, ExpandMore } from "@mui/icons-material";
export default function Grains() { export default function Grains() {
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>() const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
@ -18,7 +20,7 @@ export default function Grains() {
const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>() const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>()
const [displayCustom, setDisplayCustom] = useState(false) const [displayCustom, setDisplayCustom] = useState(false)
const { openSnack } = useSnackbar() const { openSnack } = useSnackbar()
const [tableSize, setTableSize] = useState(5) const [tableSize, setTableSize] = useState(10)
const [tablePage, setTablePage] = useState(0) const [tablePage, setTablePage] = useState(0)
const [tableTotal, setTableTotal] = useState(0) const [tableTotal, setTableTotal] = useState(0)
const grainAPI = useGrainAPI() const grainAPI = useGrainAPI()
@ -29,7 +31,11 @@ export default function Grains() {
const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false) const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([]) const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([])
const [supportedGrainTableData, setSupportedGrainTableData] = useState<GrainExtension[]>([]) const [supportedGrainTableData, setSupportedGrainTableData] = useState<GrainExtension[]>([])
// const [selectedGrainExtension, setSelectedGrainExtension] = useState<GrainExtension | undefined>()
const [accordionKey, setAccordionKey] = useState("allClosed")
const [openNewGrainDialog, setOpenNewGrainDialog] = useState(false)
const isMobile = useMobile() const isMobile = useMobile()
const extraLoad = 10 //the amount of more to load on the mobile view when load more is clicked
//function to load the first set of custom grains //function to load the first set of custom grains
const loadCustomGrain = useCallback(() => { const loadCustomGrain = useCallback(() => {
@ -92,8 +98,25 @@ export default function Grains() {
return ( return (
<ResponsiveTable<pond.GrainObject> <ResponsiveTable<pond.GrainObject>
rows={grainObjectTableData} rows={grainObjectTableData}
noDataMessage="No Grain Types Found. Add a new custom grain to see them here"
loadMore={() => {
setTableSize(tableSize + extraLoad)
}}
renderMobile={(row) => <Accordion expanded={accordionKey === row.key} onChange={() => {
//close it if it is already open
if(accordionKey === row.key){
setAccordionKey("allClosed")
}else{
setAccordionKey(row.key)
}
setCurrentCustomGrain(row.settings ?? pond.GrainSettings.create())
}}>
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
<AccordionDetails>
{customGrainDisplay()}
</AccordionDetails>
</Accordion>}
onRowClick={(grainObj) => { onRowClick={(grainObj) => {
setCurrentCustomGrain(grainObj.settings ?? pond.GrainSettings.create())
setSelectedGrain(grainObj) setSelectedGrain(grainObj)
setFormValid(true) setFormValid(true)
/** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type, /** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
@ -108,7 +131,7 @@ export default function Grains() {
columns={[ columns={[
{ {
title: "Name", title: "Name",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{row.name} {row.name}
</Typography> </Typography>
@ -116,7 +139,7 @@ export default function Grains() {
}, },
{ {
title: "Group", title: "Group",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{row.settings?.group} {row.settings?.group}
</Typography> </Typography>
@ -124,7 +147,7 @@ export default function Grains() {
}, },
{ {
title: "Equation", title: "Equation",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{equationName(row.settings?.equation ?? 0)} {equationName(row.settings?.equation ?? 0)}
</Typography> </Typography>
@ -150,11 +173,21 @@ export default function Grains() {
<ResponsiveTable<GrainExtension> <ResponsiveTable<GrainExtension>
rows={supportedGrainTableData} rows={supportedGrainTableData}
page={tablePage} page={tablePage}
// onRowClick={(ext) => {setSelectedGrainExtension(ext)}}
loadMore={() => {
setTableSize(tableSize + extraLoad)
}}
renderMobile={(row) => <Accordion>
<AccordionSummary expandIcon={<ExpandMore />}>{row.name}</AccordionSummary>
<AccordionDetails>
{supportedDisplay(row)}
</AccordionDetails>
</Accordion>}
pageSize={tableSize} pageSize={tableSize}
columns={[ columns={[
{ {
title: "Name", title: "Name",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{row.name} {row.name}
</Typography> </Typography>
@ -162,7 +195,7 @@ export default function Grains() {
}, },
{ {
title: "Group", title: "Group",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{row.group} {row.group}
</Typography> </Typography>
@ -170,11 +203,27 @@ export default function Grains() {
}, },
{ {
title: "Equation", title: "Equation",
render: row => <Box> render: row => <Box padding={2}>
<Typography> <Typography>
{equationName(row.equation ?? 0)} {equationName(row.equation ?? 0)}
</Typography> </Typography>
</Box> </Box>
},
{
title: "Kg Per Bushel",
render: row => <Box padding={2}>
<Typography>
{row.weightConversionKg.toFixed(2)}
</Typography>
</Box>
},
{
title: "Bushels Per Tonne",
render: row => <Box padding={2}>
<Typography>
{row.bushelsPerTonne.toFixed(2)}
</Typography>
</Box>
} }
]} ]}
handleRowsPerPageChange={(e) => { handleRowsPerPageChange={(e) => {
@ -189,9 +238,42 @@ export default function Grains() {
} }
//the grain display for supported grain type to show the rest of the data such as what constants are used in the formula //the grain display for supported grain type to show the rest of the data such as what constants are used in the formula
const supportedDisplay = () => { const supportedDisplay = (grain?: GrainExtension) => {
return ( return (
<Box></Box> <Box>
{grain ?
<React.Fragment>
<Typography>
Info
</Typography>
<Divider />
<Box padding={2}>
<Typography>Name: {grain.name}</Typography>
<Typography>Group: {grain.group}</Typography>
</Box>
<Typography>
Formula
</Typography>
<Divider />
<Box padding={2}>
<Typography>Equation: {equationName(grain.equation)}</Typography>
{/* <Typography>Constant A: {grain.a}</Typography>
<Typography>Constant B: {grain.b}</Typography>
<Typography>Constant C: {grain.c}</Typography> */}
</Box>
<Typography>
Conversions
</Typography>
<Divider />
<Box padding={2}>
<Typography>Kg per Bushel: {grain.weightConversionKg.toFixed(2)}</Typography>
<Typography>Bushels per Tonne: {grain.bushelsPerTonne.toFixed(2)}</Typography>
</Box>
</React.Fragment>
:
<Typography>Select a Grain from the table to view its details</Typography>
}
</Box>
) )
} }
@ -201,6 +283,9 @@ export default function Grains() {
openSnack("Grain type updated") openSnack("Grain type updated")
}).catch(err => { }).catch(err => {
openSnack("There was an issue updating the selected grain type") openSnack("There was an issue updating the selected grain type")
}).finally(() => {
setOpenUpdateConfirmation(false)
loadCustomGrain()
}) })
} }
} }
@ -221,6 +306,19 @@ export default function Grains() {
) )
} }
//because the form is embedded on desktop this dialog is specifically for mobile view
const newGrainDialog = () => {
return (
<ResponsiveDialog open={openNewGrainDialog} onClose={() => setOpenNewGrainDialog(false)}>
<DialogTitle>Add New Grain</DialogTitle>
<DialogActions>
{customGrainDisplay()}
</DialogActions>
</ResponsiveDialog>
)
}
const addNewGrain = () => { const addNewGrain = () => {
if(currentCustomGrain){ if(currentCustomGrain){
grainAPI.addGrain(currentCustomGrain).then(resp => { grainAPI.addGrain(currentCustomGrain).then(resp => {
@ -228,6 +326,9 @@ export default function Grains() {
openSnack("New grain type added") openSnack("New grain type added")
}).catch(err => { }).catch(err => {
openSnack("There was a problem creating the new grain type") openSnack("There was a problem creating the new grain type")
}).finally(() => {
loadCustomGrain()
setOpenNewGrainDialog(false)//only for the mobile view since the desktop has it embedded
}) })
} }
} }
@ -238,6 +339,9 @@ export default function Grains() {
openSnack("Grain type removed") openSnack("Grain type removed")
}).catch(err => { }).catch(err => {
openSnack("There was a problem removing the selected grain type") openSnack("There was a problem removing the selected grain type")
}).finally(() => {
setOpenRemoveConfirmation(false)
loadCustomGrain()
}) })
} }
} }
@ -272,7 +376,8 @@ export default function Grains() {
{selectedGrain && {selectedGrain &&
<Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</Button> <Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</Button>
} }
<Button onClick={() => addNewGrain()} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Save New</Button> {isMobile && !selectedGrain && <Button onClick={() => {setOpenNewGrainDialog(false)}}>Cancel</Button>}
<Button onClick={() => addNewGrain()} disabled={!formValid || (as !== "" && !permissions.includes(pond.Permission.PERMISSION_WRITE))} variant="contained" color="primary">Save New</Button>
</Box> </Box>
</Box> </Box>
) )
@ -283,7 +388,7 @@ export default function Grains() {
return ( return (
<Box> <Box>
<CustomGrainForm <CustomGrainForm
grainSettings={currentCustomGrain} grainSettings={selectedGrain?.settings || undefined}
onGrainSettingsChange={(newSettings, formValid)=>{ onGrainSettingsChange={(newSettings, formValid)=>{
setCurrentCustomGrain(newSettings) setCurrentCustomGrain(newSettings)
setFormValid(formValid) setFormValid(formValid)
@ -295,30 +400,41 @@ export default function Grains() {
} }
const desktopView = () => { const desktopView = () => {
if(displayCustom){
return ( return (
<Grid2 container> <Grid2 container>
<Grid2 size={7} padding={2}> <Grid2 size={7} padding={2}>
{displayCustom ? grainObjectTable() : supportedGrainTable()} {grainObjectTable()}
</Grid2> </Grid2>
<Grid2 size={5} padding={2}> <Grid2 size={5} padding={2}>
{displayCustom ? customGrainDisplay() : supportedDisplay()} {customGrainDisplay()}
</Grid2> </Grid2>
</Grid2> </Grid2>
) )
}else{
return (
<Box marginTop={2}>
{supportedGrainTable()}
</Box>
)
}
} }
const mobileView = () => { const mobileView = () => {
return ( return (
<Box></Box> <Box>
{displayCustom ? grainObjectTable() : supportedGrainTable()}
</Box>
) )
} }
return ( return (
<PageContainer> <PageContainer>
<Typography> <Box padding={2}>
GRAIN PAGE <Typography variant="h4">
Adaptive Grains
</Typography> </Typography>
<Box display="flex" margin={2}> <Box display="flex" marginTop={2} justifyContent="space-between">
<ButtonGroup <ButtonGroup
toggle toggle
buttons={[ buttons={[
@ -338,10 +454,21 @@ export default function Grains() {
} }
]} ]}
/> />
{isMobile && displayCustom &&
<IconButton sx={{padding: 0}} color="primary" onClick={()=>{
setSelectedGrain(undefined)
setCurrentCustomGrain(undefined)
setOpenNewGrainDialog(true)
}}>
<AddCircle />
</IconButton>
}
</Box> </Box>
{!isMobile ? desktopView() : mobileView()} {!isMobile ? desktopView() : mobileView()}
</Box>
{updateGrainConfirmation()} {updateGrainConfirmation()}
{removeConfirmation()} {removeConfirmation()}
{newGrainDialog()}
</PageContainer> </PageContainer>
) )
} }