frontend/src/pages/Grains.tsx

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>
)
}