474 lines
No EOL
16 KiB
TypeScript
474 lines
No EOL
16 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import PageContainer from "./PageContainer";
|
|
import CustomGrainForm from "grain/CustomGrainForm";
|
|
import { Accordion, AccordionDetails, AccordionSummary, Box, Button, DialogActions, DialogContent, DialogTitle, Divider, Grid2, IconButton, Typography } from "@mui/material";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import ButtonGroup from "common/ButtonGroup";
|
|
import { useMobile, useSnackbar, useUserAPI } from "hooks";
|
|
import { useGrainAPI } from "providers/pond/grainAPI";
|
|
import ResponsiveTable from "common/ResponsiveTable";
|
|
import { GetGrainExtensionMap, GetValidGrainExtensions, GrainExtension } from "grain";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { useGlobalState } from "providers";
|
|
import { Scope } from "models";
|
|
import React from "react";
|
|
import { AddCircle, ExpandMore } from "@mui/icons-material";
|
|
|
|
export default function Grains() {
|
|
const [currentCustomGrain, setCurrentCustomGrain] = useState<pond.GrainSettings>()
|
|
const [formValid, setFormValid] = useState(false)
|
|
const [selectedGrain, setSelectedGrain] = useState<pond.GrainObject | undefined>()
|
|
const [displayCustom, setDisplayCustom] = useState(false)
|
|
const { openSnack } = useSnackbar()
|
|
const [tableSize, setTableSize] = useState(10)
|
|
const [tablePage, setTablePage] = useState(0)
|
|
const [tableTotal, setTableTotal] = useState(0)
|
|
const grainAPI = useGrainAPI()
|
|
const userAPI = useUserAPI()
|
|
const [{as, user}] = useGlobalState();
|
|
const [permissions, setPermissions] = useState<pond.Permission[]>([])
|
|
const [openRemoveConfirmation, setOpenRemoveConfirmation] = useState(false)
|
|
const [openUpdateConfirmation, setOpenUpdateConfirmation] = useState(false)
|
|
const [grainObjectTableData, setGrainObjectTableData] = useState<pond.GrainObject[]>([])
|
|
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 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
|
|
const loadCustomGrain = useCallback(() => {
|
|
grainAPI.listGrains(tableSize, tablePage * tableSize, "asc", "name").then(resp => {
|
|
setGrainObjectTableData(resp.data.grains)
|
|
setTableTotal(resp.data.total)
|
|
}).catch(err => {
|
|
openSnack("There was an issue loading your custom grains")
|
|
})
|
|
},[tableSize, tablePage])
|
|
|
|
useEffect(() => {
|
|
//load the custom grains
|
|
if(displayCustom){
|
|
loadCustomGrain()
|
|
}else{
|
|
let displayedGrains: GrainExtension[] = []
|
|
let offset = tablePage * tableSize
|
|
let limit = tableSize
|
|
let grainList = GetValidGrainExtensions()
|
|
let sortedGrain = grainList.sort((a, b) => (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : -1))
|
|
sortedGrain.forEach((ext, i) => {
|
|
if(i >= offset && displayedGrains.length < limit){
|
|
displayedGrains.push(ext)
|
|
}
|
|
})
|
|
setSupportedGrainTableData(displayedGrains)
|
|
setTableTotal(sortedGrain.length)
|
|
}
|
|
},[loadCustomGrain, tableSize, tablePage, displayCustom, as])
|
|
|
|
useEffect(()=>{
|
|
//if the user is viewing as a team get their permissions to the team, if they are not then we will get their permission to the grain type individually when a type is selected
|
|
if(as){
|
|
userAPI.getUser(user.id(), { key: as, kind: "team" } as Scope).then(resp => {
|
|
setPermissions(resp.permissions);
|
|
});
|
|
}
|
|
},[as, userAPI])
|
|
|
|
const equationName = (enumVal: number) => {
|
|
switch(enumVal){
|
|
case pond.MoistureEquation.MOISTURE_EQUATION_CHUNG_PFOST:
|
|
return "Chung-Pfost"
|
|
case pond.MoistureEquation.MOISTURE_EQUATION_HALSEY:
|
|
return "Halsey"
|
|
case pond.MoistureEquation.MOISTURE_EQUATION_HENDERSON:
|
|
return "Henderson"
|
|
case pond.MoistureEquation.MOISTURE_EQUATION_OSWIN:
|
|
return "Oswin"
|
|
default:
|
|
return "None"
|
|
}
|
|
}
|
|
|
|
//function to update
|
|
|
|
//table that will display the custom grains created by the user
|
|
const grainObjectTable = () => {
|
|
return (
|
|
<ResponsiveTable<pond.GrainObject>
|
|
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) => {
|
|
setSelectedGrain(grainObj)
|
|
setFormValid(true)
|
|
/** if they are viewin as a team leave the permissions alone, if not then use the users permissions to the grain type,
|
|
* for the moment grain types are not shareable so this will be unused, but this will cover our bases if we want to implement sharing in the future
|
|
* */
|
|
if (!as) {
|
|
userAPI.getUser(user.id(), { key: grainObj.key, kind: "grain" } as Scope).then(resp => {
|
|
setPermissions(resp.permissions);
|
|
});
|
|
}
|
|
}}
|
|
columns={[
|
|
{
|
|
title: "Name",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{row.name}
|
|
</Typography>
|
|
</Box>
|
|
},
|
|
{
|
|
title: "Group",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{row.settings?.group}
|
|
</Typography>
|
|
</Box>
|
|
},
|
|
{
|
|
title: "Equation",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{equationName(row.settings?.equation ?? 0)}
|
|
</Typography>
|
|
</Box>
|
|
}
|
|
]}
|
|
page={tablePage}
|
|
pageSize={tableSize}
|
|
handleRowsPerPageChange={(e) => {
|
|
setTableSize(e.target.value)
|
|
}}
|
|
setPage={(page) => {
|
|
setTablePage(page)
|
|
}}
|
|
total={tableTotal}
|
|
/>
|
|
)
|
|
}
|
|
|
|
//table that will display the grain extensions provided by us
|
|
const supportedGrainTable = () => {
|
|
return (
|
|
<ResponsiveTable<GrainExtension>
|
|
rows={supportedGrainTableData}
|
|
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}
|
|
columns={[
|
|
{
|
|
title: "Name",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{row.name}
|
|
</Typography>
|
|
</Box>
|
|
},
|
|
{
|
|
title: "Group",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{row.group}
|
|
</Typography>
|
|
</Box>
|
|
},
|
|
{
|
|
title: "Equation",
|
|
render: row => <Box padding={2}>
|
|
<Typography>
|
|
{equationName(row.equation ?? 0)}
|
|
</Typography>
|
|
</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) => {
|
|
setTableSize(e.target.value)
|
|
}}
|
|
setPage={(page) => {
|
|
setTablePage(page)
|
|
}}
|
|
total={tableTotal}
|
|
/>
|
|
)
|
|
}
|
|
|
|
//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 = (grain?: GrainExtension) => {
|
|
return (
|
|
<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>
|
|
)
|
|
}
|
|
|
|
const updateGrain = () => {
|
|
if(selectedGrain && currentCustomGrain){
|
|
grainAPI.updateGrain(selectedGrain.key, currentCustomGrain).then(resp => {
|
|
openSnack("Grain type updated")
|
|
}).catch(err => {
|
|
openSnack("There was an issue updating the selected grain type")
|
|
}).finally(() => {
|
|
setOpenUpdateConfirmation(false)
|
|
loadCustomGrain()
|
|
})
|
|
}
|
|
}
|
|
const updateGrainConfirmation = () => {
|
|
return (
|
|
<ResponsiveDialog open={openUpdateConfirmation} onClose={() => {setOpenUpdateConfirmation(false)}}>
|
|
<DialogTitle>Update Custom Grain</DialogTitle>
|
|
<DialogContent>
|
|
Update existing custom grain type?
|
|
<br/>
|
|
Note that updating the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button variant="contained" color="primary" onClick={()=>{setOpenUpdateConfirmation(false)}}>Cancel</Button>
|
|
<Button variant="contained" color="primary" onClick={()=>{updateGrain()}}>Confirm</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
)
|
|
}
|
|
|
|
//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 = () => {
|
|
if(currentCustomGrain){
|
|
grainAPI.addGrain(currentCustomGrain).then(resp => {
|
|
//new grain added
|
|
openSnack("New grain type added")
|
|
}).catch(err => {
|
|
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
|
|
})
|
|
}
|
|
}
|
|
|
|
const removeGrain = () => {
|
|
if(selectedGrain){
|
|
grainAPI.removeGrain(selectedGrain.key).then(resp => {
|
|
openSnack("Grain type removed")
|
|
}).catch(err => {
|
|
openSnack("There was a problem removing the selected grain type")
|
|
}).finally(() => {
|
|
setOpenRemoveConfirmation(false)
|
|
loadCustomGrain()
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
const removeConfirmation = () => {
|
|
return (
|
|
<ResponsiveDialog open={openRemoveConfirmation} onClose={() => {setOpenRemoveConfirmation(false)}}>
|
|
<DialogTitle>Remove Custom Grain</DialogTitle>
|
|
<DialogContent>
|
|
Remove custom grain type?
|
|
<br />
|
|
Note that removing the grain here will not change any bins or components this type was used for, those will retain the old settings.
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button variant="contained" color="primary" onClick={()=>{setOpenRemoveConfirmation(false)}}>Cancel</Button>
|
|
<Button variant="contained" color="primary" onClick={()=>{removeGrain()}}>Confirm</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
)
|
|
}
|
|
|
|
const customGrainActions = () => {
|
|
return (
|
|
<Box display="flex" justifyContent="space-between">
|
|
<Box>
|
|
{selectedGrain &&
|
|
<Button disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="error" onClick={() => {setOpenRemoveConfirmation(true)}}>Remove Grain</Button>
|
|
}
|
|
</Box>
|
|
<Box>
|
|
{selectedGrain &&
|
|
<Button sx={{marginRight: 1}} onClick={() => setOpenUpdateConfirmation(true)} disabled={!formValid || !permissions.includes(pond.Permission.PERMISSION_WRITE)} variant="contained" color="primary">Update</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>
|
|
)
|
|
}
|
|
|
|
//the grain display for custom grain the user has defined
|
|
const customGrainDisplay = () => {
|
|
return (
|
|
<Box>
|
|
<CustomGrainForm
|
|
grainSettings={selectedGrain?.settings || undefined}
|
|
onGrainSettingsChange={(newSettings, formValid)=>{
|
|
setCurrentCustomGrain(newSettings)
|
|
setFormValid(formValid)
|
|
}}
|
|
/>
|
|
{customGrainActions()}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
const desktopView = () => {
|
|
if(displayCustom){
|
|
return (
|
|
<Grid2 container>
|
|
<Grid2 size={7} padding={2}>
|
|
{grainObjectTable()}
|
|
</Grid2>
|
|
<Grid2 size={5} padding={2}>
|
|
{customGrainDisplay()}
|
|
</Grid2>
|
|
</Grid2>
|
|
)
|
|
}else{
|
|
return (
|
|
<Box marginTop={2}>
|
|
{supportedGrainTable()}
|
|
</Box>
|
|
)
|
|
}
|
|
}
|
|
|
|
const mobileView = () => {
|
|
return (
|
|
<Box>
|
|
{displayCustom ? grainObjectTable() : supportedGrainTable()}
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PageContainer>
|
|
<Box padding={2}>
|
|
<Typography variant="h4">
|
|
Adaptive Grains
|
|
</Typography>
|
|
<Box display="flex" marginTop={2} justifyContent="space-between">
|
|
<ButtonGroup
|
|
toggle
|
|
buttons={[
|
|
{
|
|
title: "Supported",
|
|
function: () => {
|
|
setDisplayCustom(false)
|
|
setTablePage(0)
|
|
}
|
|
},
|
|
{
|
|
title: "Custom",
|
|
function: () => {
|
|
setDisplayCustom(true)
|
|
setTablePage(0)
|
|
}
|
|
}
|
|
]}
|
|
/>
|
|
{isMobile && displayCustom &&
|
|
<IconButton sx={{padding: 0}} color="primary" onClick={()=>{
|
|
setSelectedGrain(undefined)
|
|
setCurrentCustomGrain(undefined)
|
|
setOpenNewGrainDialog(true)
|
|
}}>
|
|
<AddCircle />
|
|
</IconButton>
|
|
}
|
|
</Box>
|
|
{!isMobile ? desktopView() : mobileView()}
|
|
</Box>
|
|
{updateGrainConfirmation()}
|
|
{removeConfirmation()}
|
|
{newGrainDialog()}
|
|
</PageContainer>
|
|
)
|
|
} |