729 lines
19 KiB
TypeScript
729 lines
19 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
Grid2 as Grid,
|
|
InputAdornment,
|
|
MenuItem,
|
|
Tab,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableRow,
|
|
Tabs,
|
|
TextField,
|
|
Theme,
|
|
Tooltip,
|
|
Typography
|
|
} from "@mui/material";
|
|
import { makeStyles } from "@mui/styles";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import { useMobile, useSnackbar } from "hooks";
|
|
import { Field, HarvestPlan, Task } from "models";
|
|
import moment from "moment";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import { useGlobalState, useHarvestPlanAPI, useTaskAPI } from "providers";
|
|
import React, { useCallback, useEffect, useState } from "react";
|
|
import TaskSettings from "tasks/TaskSettings";
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
close: (reload?: boolean, updatedPlan?: HarvestPlan) => void;
|
|
field: Field;
|
|
update?: boolean;
|
|
plan?: HarvestPlan;
|
|
}
|
|
|
|
const cropOptions = [
|
|
{
|
|
value: 2,
|
|
label: "Barley"
|
|
},
|
|
{
|
|
value: 4,
|
|
label: "Canola"
|
|
},
|
|
{
|
|
value: 5,
|
|
label: "Corn"
|
|
},
|
|
{
|
|
value: 6,
|
|
label: "Oats"
|
|
},
|
|
{
|
|
value: 7,
|
|
label: "Peanuts"
|
|
},
|
|
{
|
|
value: 8,
|
|
label: "Rapeseed"
|
|
},
|
|
{
|
|
value: 9,
|
|
label: "Long Grain Rice"
|
|
},
|
|
{
|
|
value: 10,
|
|
label: "Medium Grain Rice"
|
|
},
|
|
{
|
|
value: 11,
|
|
label: "Short Grain Rice"
|
|
},
|
|
{
|
|
value: 12,
|
|
label: "Sorghum"
|
|
},
|
|
{
|
|
value: 13,
|
|
label: "Soybeans"
|
|
},
|
|
{
|
|
value: 14,
|
|
label: "Sunflower"
|
|
},
|
|
{
|
|
value: 3,
|
|
label: "Buckwheat"
|
|
},
|
|
{
|
|
value: 15,
|
|
label: "Durum Wheat"
|
|
},
|
|
{
|
|
value: 16,
|
|
label: "Hard Red Wheat"
|
|
},
|
|
{
|
|
value: 17,
|
|
label: "Unretted Flax"
|
|
},
|
|
{
|
|
value: 18,
|
|
label: "Dew Retted Flax"
|
|
},
|
|
{
|
|
value: 19,
|
|
label: "Yellow Peas"
|
|
},
|
|
{
|
|
value: 20,
|
|
label: "Robin Lentils"
|
|
},
|
|
{
|
|
value: 21,
|
|
label: "Redberry Lentils"
|
|
},
|
|
{
|
|
value: 22,
|
|
label: "Blaze Lentils"
|
|
}
|
|
];
|
|
|
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
stepper: {
|
|
padding: theme.spacing(0.5)
|
|
},
|
|
secondaryColor: {
|
|
color: theme.palette.secondary.main
|
|
},
|
|
textSecondaryColor: {
|
|
color: theme.palette.text.secondary
|
|
},
|
|
bottomSpacing: {
|
|
marginBottom: theme.spacing(1)
|
|
},
|
|
tabs: {
|
|
width: "100%",
|
|
boxSizing: "border-box",
|
|
flexShrink: 1
|
|
},
|
|
tabSmall: {
|
|
width: "100%",
|
|
boxSizing: "border-box",
|
|
flexShrink: 1,
|
|
minWidth: "48px"
|
|
},
|
|
tab: {
|
|
width: "16%",
|
|
minWidth: "16%"
|
|
},
|
|
buttonSpacing: {
|
|
margin: 5
|
|
}
|
|
}));
|
|
|
|
const steps = ["Pre-Seeding", "Seeding", "Post-Seeding", "Harvest", "Fall"];
|
|
|
|
export default function HarvestSettings(props: Props) {
|
|
const { open, plan, close, field, update } = props;
|
|
const [{as}] = useGlobalState()
|
|
const [planKey, setPlanKey] = useState<string>();
|
|
const [newPlan, setNewPlan] = useState(HarvestPlan.create());
|
|
const [activeStep, setActiveStep] = useState(0);
|
|
const isMobile = useMobile();
|
|
const classes = useStyles();
|
|
const [planTasks, setPlanTasks] = useState<Task[]>([]);
|
|
const taskAPI = useTaskAPI();
|
|
const harvestPlanAPI = useHarvestPlanAPI();
|
|
const [title, setTitle] = useState("");
|
|
const [cropType, setCropType] = useState(0);
|
|
const [customCropType, setCustomCropType] = useState("");
|
|
const [useCustom, setUseCustom] = useState(false);
|
|
const [variety, setVariety] = useState("");
|
|
const [harvestYear, setHarvestYear] = useState(new Date().getFullYear());
|
|
const [targetYield, setTargetYield] = useState(0);
|
|
const [price, setPrice] = useState("");
|
|
const [newTaskDialog, setNewTaskDialog] = useState(false);
|
|
const [type, setType] = useState("");
|
|
const [taskToEdit, setTaskToEdit] = useState<Task>();
|
|
const { openSnack } = useSnackbar();
|
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
|
|
const loadTasks = useCallback(() => {
|
|
if (planKey) {
|
|
taskAPI.getMultiTasks([planKey], as).then(resp => {
|
|
setPlanTasks(resp.data.tasks.map(t => Task.any(t)));
|
|
});
|
|
}
|
|
}, [taskAPI, planKey, as]);
|
|
|
|
useEffect(() => {
|
|
loadTasks();
|
|
}, [loadTasks]);
|
|
|
|
useEffect(() => {
|
|
if (!plan || (plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))) {
|
|
setActiveStep(0);
|
|
} else {
|
|
setActiveStep(1);
|
|
}
|
|
setPlanKey(undefined);
|
|
setTitle("");
|
|
setVariety("");
|
|
setHarvestYear(new Date().getFullYear());
|
|
setTargetYield(0);
|
|
setPrice("");
|
|
if (update && plan) {
|
|
setPlanKey(plan.key());
|
|
setTitle(plan.settings.title);
|
|
setCropType(plan.cropType());
|
|
setCustomCropType(plan.customCropType());
|
|
setVariety(plan.grainType());
|
|
setHarvestYear(plan.harvestYear());
|
|
setTargetYield(plan.yieldTarget());
|
|
setPrice(plan.settings.bushelPrice.toFixed(2).toString());
|
|
} else {
|
|
setCropType(field.crop());
|
|
setCustomCropType(field.customType());
|
|
setUseCustom(field.crop() === pond.Grain.GRAIN_CUSTOM);
|
|
}
|
|
}, [plan, field, update]);
|
|
|
|
const closeDialog = (reload?: boolean, updatedPlan?: HarvestPlan) => {
|
|
close(reload, updatedPlan);
|
|
};
|
|
|
|
const showTasks = (type: string) => {
|
|
let tasks: Task[] = [];
|
|
let costTotal = 0;
|
|
let secondaryCostTotal = 0;
|
|
planTasks.forEach(task => {
|
|
if (task.settings.type === type) {
|
|
tasks.push(task);
|
|
costTotal += task.cost();
|
|
secondaryCostTotal += task.secondaryCost();
|
|
}
|
|
});
|
|
|
|
let frag = (
|
|
<React.Fragment>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>Title</TableCell>
|
|
<TableCell>Description</TableCell>
|
|
{!isMobile && <TableCell>Material Cost(acre)</TableCell>}
|
|
{!isMobile && <TableCell>Equipment Cost(acre)</TableCell>}
|
|
<TableCell>Total(acre)</TableCell>
|
|
{!isMobile && <TableCell>Start -End</TableCell>}
|
|
<TableCell>Edit</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{tasks.map((task, i) => (
|
|
<TableRow key={i}>
|
|
<TableCell>{task.title()}</TableCell>
|
|
<TableCell>{task.description()}</TableCell>
|
|
{!isMobile && <TableCell>${task.cost().toFixed(2)}</TableCell>}
|
|
{!isMobile && <TableCell>${task.secondaryCost().toFixed(2)}</TableCell>}
|
|
<TableCell>
|
|
${(Math.round((task.cost() + task.secondaryCost()) * 100) / 100).toFixed(2)}
|
|
</TableCell>
|
|
{!isMobile && (
|
|
<TableCell>
|
|
{task.start()} {task.end()}
|
|
</TableCell>
|
|
)}
|
|
<TableCell>
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setTaskToEdit(task);
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Edit
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
<Box>
|
|
<Typography>
|
|
Total: $
|
|
{(Math.round((costTotal + secondaryCostTotal) * field.acres() * 100) / 100).toFixed(2)}
|
|
</Typography>
|
|
</Box>
|
|
</React.Fragment>
|
|
);
|
|
return frag;
|
|
};
|
|
|
|
const savePlan = () => {
|
|
let newPlanSettings = pond.HarvestPlanSettings.create({
|
|
title: title,
|
|
field: field.key(),
|
|
createDate: moment.now(),
|
|
cropType: cropType,
|
|
grainType: variety,
|
|
harvestYear: harvestYear,
|
|
yieldTarget: targetYield,
|
|
bushelPrice: !isNaN(parseFloat(price)) ? Math.round(parseFloat(price) * 100) / 100 : 0
|
|
});
|
|
setNewPlan(HarvestPlan.create(pond.HarvestPlan.fromObject({ settings: newPlanSettings })));
|
|
|
|
harvestPlanAPI.addHarvestPlan(newPlanSettings, as).then(resp => {
|
|
setPlanKey(resp.data.harvestPlan);
|
|
openSnack("Plan Created");
|
|
});
|
|
};
|
|
|
|
const updatePlan = () => {
|
|
//material costs per acre
|
|
let preseedMatCost = 0;
|
|
let seedMatCost = 0;
|
|
let postseedMatCost = 0;
|
|
let harvestMatCost = 0;
|
|
let fallMatCost = 0;
|
|
|
|
//equipment costs per acre
|
|
let preseedEqCost = 0;
|
|
let seedEqCost = 0;
|
|
let postseedEqCost = 0;
|
|
let harvestEqCost = 0;
|
|
let fallEqCost = 0;
|
|
planTasks.forEach(task => {
|
|
switch (task.settings.type) {
|
|
case "preseed":
|
|
preseedMatCost += task.cost();
|
|
preseedEqCost += task.secondaryCost();
|
|
break;
|
|
case "seed":
|
|
seedMatCost += task.cost();
|
|
seedEqCost += task.secondaryCost();
|
|
break;
|
|
case "postseed":
|
|
postseedMatCost += task.cost();
|
|
postseedEqCost += task.secondaryCost();
|
|
break;
|
|
case "harvest":
|
|
harvestMatCost += task.cost();
|
|
harvestEqCost += task.secondaryCost();
|
|
break;
|
|
default:
|
|
fallMatCost += task.cost();
|
|
fallEqCost += task.secondaryCost();
|
|
break;
|
|
}
|
|
});
|
|
let tempPlan = plan ?? newPlan;
|
|
tempPlan.settings.cropType = cropType;
|
|
tempPlan.settings.field = field.key();
|
|
tempPlan.settings.title = title;
|
|
tempPlan.settings.grainType = variety;
|
|
tempPlan.settings.harvestYear = harvestYear;
|
|
tempPlan.settings.yieldTarget = targetYield;
|
|
tempPlan.settings.bushelPrice = !isNaN(parseFloat(price))
|
|
? Math.round(parseFloat(price) * 100) / 100
|
|
: 0;
|
|
tempPlan.settings.preSeedMaterials = preseedMatCost;
|
|
tempPlan.settings.preSeedEquipment = preseedEqCost;
|
|
tempPlan.settings.seedMaterials = seedMatCost;
|
|
tempPlan.settings.seedEquipment = seedEqCost;
|
|
tempPlan.settings.postSeedMaterials = postseedMatCost;
|
|
tempPlan.settings.postSeedEquipment = postseedEqCost;
|
|
tempPlan.settings.harvestMaterials = harvestMatCost;
|
|
tempPlan.settings.harvestEquipment = harvestEqCost;
|
|
tempPlan.settings.fallMaterials = fallMatCost;
|
|
tempPlan.settings.fallEquipment = fallEqCost;
|
|
if (planKey) {
|
|
harvestPlanAPI
|
|
.updateHarvestPlan(planKey, tempPlan.settings, undefined, as)
|
|
.then(resp => openSnack("Plan Updated"));
|
|
}
|
|
closeDialog(true, tempPlan);
|
|
};
|
|
|
|
const deletePlan = () => {
|
|
if (planKey) {
|
|
harvestPlanAPI.removeHarvestPlan(planKey, as).then(resp => {
|
|
openSnack("Plan Deleted");
|
|
});
|
|
}
|
|
setDeleteOpen(false);
|
|
closeDialog(true);
|
|
};
|
|
|
|
const handleNextStep = () => {
|
|
setActiveStep(prevStep => prevStep + 1);
|
|
};
|
|
const handleBack = () => {
|
|
setActiveStep(prevActiveStep => prevActiveStep - 1);
|
|
};
|
|
|
|
const stepper = () => {
|
|
return (
|
|
<Box display="flex" justifyContent="center" width="100%">
|
|
<Tabs
|
|
value={activeStep}
|
|
onChange={(_, value) => setActiveStep(value)}
|
|
indicatorColor="primary"
|
|
textColor="primary"
|
|
variant="fullWidth"
|
|
aria-label="bin tabs"
|
|
classes={{ root: classes.tabs }}>
|
|
<Tab
|
|
key={0}
|
|
classes={{ root: classes.tab }}
|
|
disabled={
|
|
!planKey || !(plan && plan.permissions.includes(pond.Permission.PERMISSION_WRITE))
|
|
}
|
|
label={"General"}
|
|
aria-label={"general"}
|
|
/>
|
|
{steps.map((label, i) => (
|
|
<Tab
|
|
key={i + 1}
|
|
classes={{ root: classes.tab }}
|
|
disabled={!planKey}
|
|
label={label}
|
|
aria-label={label}
|
|
/>
|
|
))}
|
|
</Tabs>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const general = () => {
|
|
return (
|
|
<Box>
|
|
<TextField
|
|
fullWidth
|
|
margin="normal"
|
|
type="text"
|
|
label="Title"
|
|
value={title}
|
|
onChange={e => setTitle(e.target.value)}
|
|
/>
|
|
{useCustom ? (
|
|
<TextField
|
|
margin="normal"
|
|
fullWidth
|
|
label="Crop"
|
|
value={customCropType}
|
|
onChange={e => setCustomCropType(e.target.value)}
|
|
color="primary"
|
|
/>
|
|
) : (
|
|
<TextField
|
|
margin="normal"
|
|
fullWidth
|
|
select
|
|
label="Crop"
|
|
value={cropType}
|
|
onChange={e => setCropType(+e.target.value)}
|
|
color="primary">
|
|
{cropOptions.map(option => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</TextField>
|
|
)}
|
|
<TextField
|
|
fullWidth
|
|
margin="normal"
|
|
type="text"
|
|
label="Variety"
|
|
value={variety}
|
|
onChange={e => setVariety(e.target.value)}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
margin="normal"
|
|
type="number"
|
|
label="Year"
|
|
value={harvestYear}
|
|
onChange={e => {
|
|
!isNaN(+e.target.value) && setHarvestYear(+e.target.value);
|
|
}}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
margin="normal"
|
|
type="number"
|
|
label="Target Yield per acre"
|
|
value={targetYield}
|
|
onChange={e => {
|
|
!isNaN(+e.target.value) && setTargetYield(+e.target.value);
|
|
}}
|
|
/>
|
|
<TextField
|
|
margin="dense"
|
|
id="bushPrice"
|
|
label="Estimated sale price per bushel"
|
|
type="text"
|
|
fullWidth
|
|
helperText="Must be a valid number*"
|
|
value={price}
|
|
error={isNaN(+price) && price !== ""}
|
|
InputProps={{
|
|
startAdornment: <InputAdornment position="start">$</InputAdornment>
|
|
}}
|
|
onChange={e => {
|
|
setPrice(e.target.value);
|
|
}}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const preSeed = () => {
|
|
return (
|
|
<Box textAlign="center">
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setType("preseed");
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Add Crop Activity
|
|
</Button>
|
|
<Typography>Planned Activities</Typography>
|
|
{showTasks("preseed")}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const seed = () => {
|
|
return (
|
|
<Box textAlign="center">
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setType("seed");
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Add Crop Activity
|
|
</Button>
|
|
<Typography>Planned Activities</Typography>
|
|
{showTasks("seed")}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const postSeed = () => {
|
|
return (
|
|
<Box textAlign="center">
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setType("postseed");
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Add Crop Activity
|
|
</Button>
|
|
<Typography>Planned Activities</Typography>
|
|
{showTasks("postseed")}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const harvest = () => {
|
|
return (
|
|
<Box textAlign="center">
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setType("harvest");
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Add Crop Activity
|
|
</Button>
|
|
<Typography>Planned Activities</Typography>
|
|
{showTasks("harvest")}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const fall = () => {
|
|
return (
|
|
<Box textAlign="center">
|
|
<Button
|
|
color="primary"
|
|
onClick={() => {
|
|
setType("fall");
|
|
setNewTaskDialog(true);
|
|
}}>
|
|
Add Crop Activity
|
|
</Button>
|
|
<Typography>Planned Activities</Typography>
|
|
{showTasks("fall")}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const stepperContent = (step: number) => {
|
|
switch (step) {
|
|
case 1:
|
|
return preSeed();
|
|
case 2:
|
|
return seed();
|
|
case 3:
|
|
return postSeed();
|
|
case 4:
|
|
return harvest();
|
|
case 5:
|
|
return fall();
|
|
default:
|
|
return general();
|
|
}
|
|
};
|
|
|
|
const deleteDialog = () => {
|
|
return (
|
|
<ResponsiveDialog open={deleteOpen}>
|
|
<DialogContent>
|
|
Are you sure you wish to delete plan? This action is irreversible.
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
|
|
<Button onClick={deletePlan}>
|
|
<Typography color="error">Delete</Typography>
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<ResponsiveDialog open={open} onClose={() => closeDialog()} maxWidth="lg">
|
|
<DialogTitle>Create New Crop Plan</DialogTitle>
|
|
<DialogContent>
|
|
{stepper()}
|
|
{stepperContent(activeStep)}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Grid container direction="row">
|
|
<Grid>
|
|
{planKey && plan && field.permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
|
<Button
|
|
variant="contained"
|
|
style={{ color: "black", backgroundColor: "red" }}
|
|
onClick={() => setDeleteOpen(true)}>
|
|
Delete
|
|
</Button>
|
|
)}
|
|
</Grid>
|
|
<Grid>
|
|
<Button
|
|
className={classes.buttonSpacing}
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={() => closeDialog()}>
|
|
Cancel
|
|
</Button>
|
|
{activeStep === 0 && !planKey && (
|
|
<React.Fragment>
|
|
<Button
|
|
className={classes.buttonSpacing}
|
|
variant="contained"
|
|
onClick={() => {
|
|
savePlan();
|
|
}}
|
|
color="primary">
|
|
Create Plan
|
|
</Button>
|
|
</React.Fragment>
|
|
)}
|
|
{activeStep > 0 && (
|
|
<Button
|
|
className={classes.buttonSpacing}
|
|
variant="contained"
|
|
onClick={handleBack}
|
|
color="primary">
|
|
Back
|
|
</Button>
|
|
)}
|
|
{activeStep < 5 && (activeStep > 0 || planKey) && (
|
|
<Button
|
|
className={classes.buttonSpacing}
|
|
variant="contained"
|
|
onClick={handleNextStep}
|
|
color="primary">
|
|
Next
|
|
</Button>
|
|
)}
|
|
{(planKey || activeStep > 0) && (
|
|
<Tooltip title="Finish Harvest Plan">
|
|
<Button
|
|
className={classes.buttonSpacing}
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={updatePlan}>
|
|
Confirm
|
|
</Button>
|
|
</Tooltip>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
{deleteDialog()}
|
|
<TaskSettings
|
|
task={taskToEdit}
|
|
hasCost
|
|
costTitle="Material Cost(acres)"
|
|
secondaryCostTitle="Equipment Cost(acre)"
|
|
objectKey={planKey}
|
|
type={type}
|
|
open={newTaskDialog}
|
|
onClose={r => {
|
|
setType("");
|
|
setTaskToEdit(undefined);
|
|
setNewTaskDialog(false);
|
|
if (r) {
|
|
loadTasks();
|
|
}
|
|
}}
|
|
/>
|
|
</React.Fragment>
|
|
);
|
|
}
|