diff --git a/src/bin/BinyardDisplay.tsx b/src/bin/BinyardDisplay.tsx index 6e055b0..ed83912 100644 --- a/src/bin/BinyardDisplay.tsx +++ b/src/bin/BinyardDisplay.tsx @@ -679,16 +679,16 @@ export default function BinyardDisplay(props: Props) { className={classes.gridList} cols={ props.insert && !isMobile - ? 3.25 + ? 3 : width === "xs" - ? 3.25 + ? 3 : width === "sm" - ? 5.5 + ? 5 : width === "md" - ? 6.5 + ? 6 : width === "lg" - ? 7.5 - : 8.5 + ? 7 + : 8 }> {binMetrics && binMetrics.grainInventory.map((inv, key) => ( diff --git a/src/harvestPlan/DuplicateHarvestPlan.tsx b/src/harvestPlan/DuplicateHarvestPlan.tsx new file mode 100644 index 0000000..cb24eae --- /dev/null +++ b/src/harvestPlan/DuplicateHarvestPlan.tsx @@ -0,0 +1,108 @@ +import { + Button, + DialogActions, + DialogContent, + DialogTitle, + MenuItem, + TextField +} from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import { Field, HarvestPlan, /*Task*/ } from "models"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import { useHarvestPlanAPI/*, useTaskAPI*/ } from "providers"; +import { useCallback, useEffect, useState } from "react"; + +interface Props { + open: boolean; + close: () => void; + fields: Field[]; + plan: HarvestPlan; +} + +export default function DuplicateHarvestPlan(props: Props) { + const { open, close, fields, plan } = props; + const [newFieldIndex, setNewFieldIndex] = useState(0); + //const [planTasks, setPlanTasks] = useState([]); + const [title, setTitle] = useState(""); + const harvestPlanAPI = useHarvestPlanAPI(); + //const taskAPI = useTaskAPI(); + const [ready, setReady] = useState(false); + + // const loadTasks = useCallback(() => { + // taskAPI.listTasks(50, 0, "asc", "start", plan.key()).then(resp => { + // setPlanTasks(resp.data.tasks.map(t => Task.any(t))); + // setReady(true); + // }); + // }, [taskAPI, plan]); + + // useEffect(() => { + // loadTasks(); + // }, [loadTasks]); + + const duplicate = () => { + let planSettings = HarvestPlan.clone(plan).settings; + planSettings.field = fields[newFieldIndex].key(); + planSettings.createDate = moment.now(); + if (title !== "") { + planSettings.title = title; + } + harvestPlanAPI.addHarvestPlan(planSettings).then(resp => { + // let tasks = planTasks; + // tasks.forEach(task => { + // task.settings.objectKey = resp.data.harvestPlan; + // taskAPI.addTask(task.settings); + // }); + }); + close(); + }; + return ( + + Duplicate Crop Plan + + setNewFieldIndex(+e.target.value)} + color="primary"> + {fields.map((option, i) => ( + + {option.fieldName()} + + ))} + + setTitle(e.target.value)} + /> + + + + + + + ); +} diff --git a/src/harvestPlan/HarvestPlanActions.tsx b/src/harvestPlan/HarvestPlanActions.tsx new file mode 100644 index 0000000..441446a --- /dev/null +++ b/src/harvestPlan/HarvestPlanActions.tsx @@ -0,0 +1,235 @@ +import { + Box, + Button, + IconButton, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Theme +} from "@mui/material"; +import MoreIcon from "@mui/icons-material/MoreVert"; +import ShareObjectIcon from "@mui/icons-material/Share"; +import ObjectUsersIcon from "@mui/icons-material/AccountCircle"; +import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle"; +import { pond } from "protobuf-ts/pond"; +import React, { useState } from "react"; +import ObjectUsers from "user/ObjectUsers"; +import ShareObject from "user/ShareObject"; +import { isOffline } from "utils/environment"; +import ObjectTeams from "teams/ObjectTeams"; +import { planScope, HarvestPlan, Field } from "models"; +import HarvestSettings from "./HarvestSettings"; +import DuplicateHarvestPlan from "./DuplicateHarvestPlan"; +import { Edit } from "@mui/icons-material"; +import { makeStyles } from "@mui/styles"; +import { blue } from "@mui/material/colors"; + +const useStyles = makeStyles((theme: Theme) => ({ + shareIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + removeIcon: { + color: "var(--status-alert)" + }, + red: { + color: "var(--status-alert)" + } +})); + +interface Props { + plan: HarvestPlan; + planField: Field; + fieldList: Field[]; + permissions: pond.Permission[]; + refreshCallback: (updatedPlan?: HarvestPlan) => void; + hideNew?: boolean; +} + +interface OpenState { + share: boolean; + users: boolean; + teams: boolean; + settings: boolean; + removeSelf: boolean; + duplicate: boolean; +} + +export default function HarvestPlanActions(props: Props) { + const classes = useStyles(); + const { plan, planField, fieldList, permissions, refreshCallback, hideNew } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [openState, setOpenState] = useState({ + share: false, + users: false, + teams: false, + settings: false, + removeSelf: false, + duplicate: false + }); + const [updatePlan, setUpdatePlan] = useState(false); + + const groupMenu = () => { + const canShare = permissions.includes(pond.Permission.PERMISSION_SHARE); + const canManageUsers = permissions.includes(pond.Permission.PERMISSION_USERS); + return ( + setAnchorEl(null)} + keepMounted + disableAutoFocusItem> + {!isOffline() && + planField.permissions.includes(pond.Permission.PERMISSION_WRITE) && + !hideNew && ( + { + setUpdatePlan(false); + setOpenState({ ...openState, settings: true }); + setAnchorEl(null); + }}> + + + + )} + {!isOffline() && planField.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + { + setOpenState({ ...openState, duplicate: true }); + setAnchorEl(null); + }}> + + + + )} + {!isOffline() && canShare && ( + { + setOpenState({ ...openState, share: true }); + setAnchorEl(null); + }}> + + + + + + )} + {!isOffline() && canManageUsers && ( + { + setOpenState({ ...openState, users: true }); + setAnchorEl(null); + }}> + + + + + + )} + {!isOffline() && canManageUsers && ( + { + setOpenState({ ...openState, teams: true }); + setAnchorEl(null); + }}> + + + + + + )} + + ); + }; + + const dialogs = () => { + const key = plan.key(); + const label = plan.settings.title; + return ( + + { + setOpenState({ ...openState, settings: false }); + if (refresh) { + refreshCallback(updatedPlan); + } + }} + /> + { + setOpenState({ ...openState, duplicate: false }); + refreshCallback(); + }} + fields={fieldList} + plan={plan} + /> + setOpenState({ ...openState, share: false })} + /> + setOpenState({ ...openState, users: false })} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, teams: false })} + /> + + ); + }; + + return ( + + + {plan.key() !== "" && ( + + )} + ) => + setAnchorEl(event.currentTarget) + }> + + + + + {groupMenu()} + {dialogs()} + + ); +} diff --git a/src/harvestPlan/HarvestPlanDisplay.tsx b/src/harvestPlan/HarvestPlanDisplay.tsx new file mode 100644 index 0000000..a7161fb --- /dev/null +++ b/src/harvestPlan/HarvestPlanDisplay.tsx @@ -0,0 +1,209 @@ +import { + Box, + Grid2 as Grid, + Theme, + Typography, + IconButton, + LinearProgress, + Drawer +} from "@mui/material"; +import { Notes } from "@mui/icons-material"; +import GrainDescriber from "grain/GrainDescriber"; +import { Field, HarvestPlan } from "models"; +import React, { useState } from "react"; +import { getThemeType } from "theme"; +import Chat from "chat/Chat"; +import { pond } from "protobuf-ts/pond"; +import { useMobile } from "hooks"; +import HarvestPlanActions from "./HarvestPlanActions"; +import { makeStyles } from "@mui/styles"; + +interface Props { + plan: HarvestPlan; + permissions: pond.Permission[]; + planField: Field; + loading: boolean; + fieldList: Field[]; + changePlan: (updatedPlan?: HarvestPlan) => void; +} + +const useStyles = makeStyles((theme: Theme) => ({ + dark: { + backgroundColor: getThemeType() === "light" ? "rgb(245, 245, 245)" : "rgb(50, 50, 50)", + padding: 5 + }, + light: { + backgroundColor: getThemeType() === "light" ? "rgb(235, 235, 235)" : "rgb(60, 60, 60)", + padding: 5 + }, + avatar: { + color: getThemeType() === "light" ? theme.palette.common.black : theme.palette.common.white, + backgroundColor: "transparent", + width: theme.spacing(5), + height: theme.spacing(5), + border: "1px solid", + borderColor: theme.palette.divider + } +})); + +export default function HarvestPlanDisplay(props: Props) { + const { plan, loading, planField, fieldList, changePlan, permissions } = props; + const classes = useStyles(); + const [openNote, setOpenNote] = useState(false); + const isMobile = useMobile(); + + const planTable = () => { + return ( + + + + + Break Even Yield: + + {( + (plan.totalEquipmentCost() + plan.totalMaterialCost()) / + plan.bushelPrice() + ).toFixed(2)}{" "} + BU + + + + + + Break Even Sales Price + + $ + {( + (plan.totalEquipmentCost() + plan.totalMaterialCost()) / + plan.yieldTarget() + ).toFixed(2)} + + + + + + + + Harvest Year: + {plan.harvestYear()} + + + + + Title: + {plan.settings.title} + + + + + Grain: + {GrainDescriber(plan.cropType()).name} + + + + + Variant: + {plan.grainType()} + + + + + Bushel Price: + ${plan.bushelPrice()} + + + + + Yield Target: + {plan.yieldTarget()} + + + + + Total Cost (acre): + + ${(plan.totalEquipmentCost() + plan.totalMaterialCost()).toFixed(2)} + + + + + + Total Cost: + + $ + {( + (plan.totalEquipmentCost() + plan.totalMaterialCost()) * + planField.acres() + ).toFixed(2)} + + + + + + ); + }; + + const noteDrawer = () => { + return ( + setOpenNote(false)}> + + Notes + + + + ); + }; + + return ( + + {loading ? ( + + + + ) : ( + + setOpenNote(true)}> + + + + + Crop Plan{plan.key() !== "" ? " - " + plan.settings.title : " - None Found"} + + {!isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + { + changePlan(updatedPlan); + }} //possibly need to re-load plan here + /> + )} + + {isMobile && permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + { + changePlan(updatedPlan); + }} //possibly need to re-load plan here + /> + )} + {plan.key() !== "" && planTable()} + + )} + {noteDrawer()} + + ); +} diff --git a/src/harvestPlan/HarvestSettings.tsx b/src/harvestPlan/HarvestSettings.tsx new file mode 100644 index 0000000..2fe9760 --- /dev/null +++ b/src/harvestPlan/HarvestSettings.tsx @@ -0,0 +1,728 @@ +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 { 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 [planKey, setPlanKey] = useState(); + const [newPlan, setNewPlan] = useState(HarvestPlan.create()); + const [activeStep, setActiveStep] = useState(0); + const isMobile = useMobile(); + const classes = useStyles(); + //const [planTasks, setPlanTasks] = useState([]); + //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(); + const { openSnack } = useSnackbar(); + const [deleteOpen, setDeleteOpen] = useState(false); + + // const loadTasks = useCallback(() => { + // if (planKey) { + // taskAPI.getMultiTasks([planKey]).then(resp => { + // setPlanTasks(resp.data.tasks.map(t => Task.any(t))); + // }); + // } + // }, [taskAPI, planKey]); + + // 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 = ( + // + // + // + // + // Title + // Description + // {!isMobile && Material Cost(acre)} + // {!isMobile && Equipment Cost(acre)} + // Total(acre) + // {!isMobile && Start -End} + // Edit + // + // + // + // {tasks.map((task, i) => ( + // + // {task.title()} + // {task.description()} + // {!isMobile && ${task.cost().toFixed(2)}} + // {!isMobile && ${task.secondaryCost().toFixed(2)}} + // + // ${(Math.round((task.cost() + task.secondaryCost()) * 100) / 100).toFixed(2)} + // + // {!isMobile && ( + // + // {task.start()} {task.end()} + // + // )} + // + // + // + // + // ))} + // + //
+ // + // + // Total: $ + // {(Math.round((costTotal + secondaryCostTotal) * field.acres() * 100) / 100).toFixed(2)} + // + // + //
+ // ); + // 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).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) + .then(resp => openSnack("Plan Updated")); + } + closeDialog(true, tempPlan); + }; + + const deletePlan = () => { + if (planKey) { + harvestPlanAPI.removeHarvestPlan(planKey).then(resp => { + openSnack("Plan Deleted"); + }); + } + setDeleteOpen(false); + closeDialog(true); + }; + + const handleNextStep = () => { + setActiveStep(prevStep => prevStep + 1); + }; + const handleBack = () => { + setActiveStep(prevActiveStep => prevActiveStep - 1); + }; + + const stepper = () => { + return ( + + setActiveStep(value)} + indicatorColor="primary" + textColor="primary" + variant="fullWidth" + aria-label="bin tabs" + classes={{ root: classes.tabs }}> + + {steps.map((label, i) => ( + + ))} + + + ); + }; + + const general = () => { + return ( + + setTitle(e.target.value)} + /> + {useCustom ? ( + setCustomCropType(e.target.value)} + color="primary" + /> + ) : ( + setCropType(+e.target.value)} + color="primary"> + {cropOptions.map(option => ( + + {option.label} + + ))} + + )} + setVariety(e.target.value)} + /> + { + !isNaN(+e.target.value) && setHarvestYear(+e.target.value); + }} + /> + { + !isNaN(+e.target.value) && setTargetYield(+e.target.value); + }} + /> + $ + }} + onChange={e => { + setPrice(e.target.value); + }} + /> + + ); + }; + + const preSeed = () => { + return ( + + + Planned Activities + {/* {showTasks("preseed")} */} + + ); + }; + + const seed = () => { + return ( + + + Planned Activities + {/* {showTasks("seed")} */} + + ); + }; + + const postSeed = () => { + return ( + + + Planned Activities + {/* {showTasks("postseed")} */} + + ); + }; + + const harvest = () => { + return ( + + + Planned Activities + {/* {showTasks("harvest")} */} + + ); + }; + + const fall = () => { + return ( + + + Planned Activities + {/* {showTasks("fall")} */} + + ); + }; + + 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 ( + + + Are you sure you wish to delete plan? This action is irreversible. + + + + + + + ); + }; + + return ( + + closeDialog()} maxWidth="lg"> + Create New Crop Plan + + {stepper()} + {stepperContent(activeStep)} + + + + + {planKey && plan && field.permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + )} + + + + {activeStep === 0 && !planKey && ( + + + + )} + {activeStep > 0 && ( + + )} + {activeStep < 5 && (activeStep > 0 || planKey) && ( + + )} + {(planKey || activeStep > 0) && ( + + + + )} + + + + + {deleteDialog()} + {/* { + setType(""); + setTaskToEdit(undefined); + setNewTaskDialog(false); + if (r) { + loadTasks(); + } + }} + /> */} + + ); +} diff --git a/src/maps/mapDrawers/FieldDrawer.tsx b/src/maps/mapDrawers/FieldDrawer.tsx index bafd966..88e630f 100644 --- a/src/maps/mapDrawers/FieldDrawer.tsx +++ b/src/maps/mapDrawers/FieldDrawer.tsx @@ -11,8 +11,8 @@ import { } from "@mui/material"; import { Notes } from "@mui/icons-material"; import DisplayDrawer from "common/DisplayDrawer"; -//import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; -import { Field, fieldScope, /*HarvestPlan,*/ teamScope } from "models"; +import HarvestPlanDisplay from "harvestPlan/HarvestPlanDisplay"; +import { Field, fieldScope, HarvestPlan, teamScope } from "models"; import React, { useEffect, useState } from "react"; //import TaskViewer from "tasks/TaskViewer"; import Weather from "weather/weather"; @@ -21,7 +21,7 @@ import GrainDescriber from "grain/GrainDescriber"; import Chat from "chat/Chat"; import { pond } from "protobuf-ts/pond"; import { useMobile } from "hooks"; -import { useGlobalState, /*useHarvestPlanAPI,*/ useUserAPI } from "providers"; +import { useGlobalState, useHarvestPlanAPI, useUserAPI } from "providers"; import { makeStyles } from "@mui/styles"; import FieldActions from "field/FieldActions"; @@ -77,13 +77,13 @@ const useStyles = makeStyles((theme: Theme) => ({ export default function FieldDrawer(props: Props) { const { open, closeDrawer, selectedFieldKey, fields, moveMap } = props; const [field, setField] = useState(Field.create()); - //const [hPlan, setHPlan] = useState(HarvestPlan.create()); + const [hPlan, setHPlan] = useState(HarvestPlan.create()); const [value, setValue] = useState(0); const classes = useStyles(); const [openNote, setOpenNote] = useState(false); const isMobile = useMobile(); const [{ as, user }] = useGlobalState(); - //const hPlanAPI = useHarvestPlanAPI(); + const hPlanAPI = useHarvestPlanAPI(); const [planLoading, setPlanLoading] = useState(false); const userAPI = useUserAPI(); const [permissions, setPermissions] = useState([]); @@ -129,24 +129,24 @@ export default function FieldDrawer(props: Props) { } }, [as, user, userAPI, field]); //eslint-disable-line react-hooks/exhaustive-deps - // useEffect(() => { - // if (field.key() !== "") { - // hPlanAPI - // .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) - // .then(resp => { - // if (resp.data.harvestPlan.length > 0) { - // let plan = resp.data.harvestPlan[0]; - // setHPlan(HarvestPlan.any(plan)); - // } else { - // setHPlan(HarvestPlan.create()); - // } - // setPlanLoading(false); - // }) - // .catch(err => { - // //openSnack("Failed to load plan"); - // }); - // } - // }, [field, as, hPlanAPI]); + useEffect(() => { + if (field.key() !== "") { + hPlanAPI + .listHarvestPlans(1, 0, "desc", "createDate", field.key(), as) + .then(resp => { + if (resp.data.harvestPlan.length > 0) { + let plan = resp.data.harvestPlan[0]; + setHPlan(HarvestPlan.any(plan)); + } else { + setHPlan(HarvestPlan.create()); + } + setPlanLoading(false); + }) + .catch(err => { + //openSnack("Failed to load plan"); + }); + } + }, [field, as, hPlanAPI]); const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { setValue(newValue); @@ -206,7 +206,7 @@ export default function FieldDrawer(props: Props) { let taskLoadKeys: string[] = []; if (!planLoading) { field.key() !== "" && taskLoadKeys.push(field.key()); - //hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); + hPlan.key() !== "" && taskLoadKeys.push(hPlan.key()); } return ( @@ -255,7 +255,7 @@ export default function FieldDrawer(props: Props) { - {/* */} + /> diff --git a/src/models/HarvestPlan.ts b/src/models/HarvestPlan.ts new file mode 100644 index 0000000..a66f63c --- /dev/null +++ b/src/models/HarvestPlan.ts @@ -0,0 +1,160 @@ +import { cloneDeep } from "lodash"; +import { pond } from "protobuf-ts/pond"; +import { or } from "utils/types"; + +export class HarvestPlan { + public settings: pond.HarvestPlanSettings = pond.HarvestPlanSettings.create(); + public status: pond.HarvestPlanStatus = pond.HarvestPlanStatus.create(); + public permissions: pond.Permission[] = []; + + public static create(pb?: pond.HarvestPlan): HarvestPlan { + let my = new HarvestPlan(); + if (pb) { + my.settings = pond.HarvestPlanSettings.fromObject(cloneDeep(or(pb.settings, {}))); + my.status = pond.HarvestPlanStatus.fromObject(cloneDeep(or(pb.status, {}))); + my.permissions = pb.planPermissions; + } + return my; + } + + public static clone(other?: HarvestPlan): HarvestPlan { + if (other) { + return HarvestPlan.create( + pond.HarvestPlan.fromObject({ + settings: cloneDeep(other.settings), + status: cloneDeep(other.status) + }) + ); + } + return HarvestPlan.create(); + } + + public static any(data: any): HarvestPlan { + return HarvestPlan.create(pond.HarvestPlan.fromObject(cloneDeep(data))); + } + + public key(): string { + return this.settings.key; + } + + public totalMaterialCost(): number { + return ( + this.settings.seedMaterials + + this.settings.preSeedMaterials + + this.settings.postSeedMaterials + + this.settings.harvestMaterials + + this.settings.fallMaterials + ); + } + + public totalEquipmentCost(): number { + return ( + this.settings.seedEquipment + + this.settings.preSeedEquipment + + this.settings.postSeedEquipment + + this.settings.harvestEquipment + + this.settings.fallEquipment + ); + } + + public field(): string { + return this.settings.field; + } + + public createDate(): number { + return this.settings.createDate; + } + + public harvestYear(): number { + return this.settings.harvestYear; + } + + public cropType(): pond.Grain { + return this.settings.cropType; + } + + public customCropType(): string { + return this.settings.customGrain; + } + + public grainType(): string { + return this.settings.grainType; + } + + public yieldTarget(): number { + return this.settings.yieldTarget; + } + + public actualYield(): number { + return this.settings.actualYield; + } + + //NOTE all cost are set as per acre + public seedMaterialsCost(): number { + return this.settings.seedMaterials; + } + + public seedEquipmentCost(): number { + return this.settings.seedEquipment; + } + + public seedPerAcre(): number { + return this.settings.seedMaterials + this.settings.seedEquipment; + } + + public preSeedMaterialsCost(): number { + return this.settings.preSeedMaterials; + } + + public preSeedEquipmentCost(): number { + return this.settings.preSeedEquipment; + } + + public preSeedPerAcre(): number { + return this.settings.preSeedMaterials + this.settings.preSeedEquipment; + } + + public postSeedMaterialCost(): number { + return this.settings.postSeedMaterials; + } + + public postSeedEquipmentCost(): number { + return this.settings.postSeedEquipment; + } + + public postSeedPerAcre(): number { + return this.settings.postSeedMaterials + this.settings.postSeedEquipment; + } + + public harvestMaterialCost(): number { + return this.settings.harvestMaterials; + } + + public harvestEquipmentCost(): number { + return this.settings.harvestEquipment; + } + + public harvestPerAcre(): number { + return this.settings.harvestMaterials + this.settings.harvestEquipment; + } + + public fallMaterialCost(): number { + return this.settings.fallMaterials; + } + + public fallEquipmentCost(): number { + return this.settings.fallEquipment; + } + + public fallPerAcre(): number { + return this.settings.fallMaterials + this.settings.fallEquipment; + } + + public bushelPrice(): number { + return this.settings.bushelPrice; + } + + public taskIds(): string[] { + return this.settings.taskIds; + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 8d53d26..fa2b93e 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -17,7 +17,7 @@ export * from "./user"; export * from "./Field"; // export * from "./Team"; export * from "./team"; -// export * from "./HarvestPlan"; +export * from "./HarvestPlan"; // export * from "./HarvestYear"; export * from "./Note"; // export * from "./FieldMapping"; diff --git a/src/providers/index.ts b/src/providers/index.ts index 4b5ba0b..ec95aa9 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -17,7 +17,7 @@ export { useFieldMarkerAPI, useFirmwareAPI, useGroupAPI, - // useHarvestPlanAPI, + useHarvestPlanAPI, // useHarvestYearAPI, useHomeMarkerAPI, useInteractionsAPI, diff --git a/src/providers/pond/harvestPlanAPI.tsx b/src/providers/pond/harvestPlanAPI.tsx new file mode 100644 index 0000000..492642a --- /dev/null +++ b/src/providers/pond/harvestPlanAPI.tsx @@ -0,0 +1,130 @@ +import { AxiosResponse } from "axios"; +import { useHTTP } from "hooks"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState } from "providers/StateContainer"; +import { createContext, PropsWithChildren, useContext } from "react"; +import { or } from "utils"; +import { pondURL } from "./pond"; + +export interface IHarvestPlanAPIContext { + addHarvestPlan: (harvestPlan: pond.HarvestPlanSettings) => Promise; + getHarvestPlan: (harvestPlanId: string) => Promise; + listHarvestPlans: ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => Promise>; + listHistory: ( + id: string, + limit: number, + offset: number + ) => Promise>; + removeHarvestPlan: ( + harvestPlanId: string + ) => Promise>; + updateHarvestPlan: ( + key: string, + harvestPlan: pond.HarvestPlanSettings, + asRoot?: true + ) => Promise>; +} + +export const HarvestPlanAPIContext = createContext( + {} as IHarvestPlanAPIContext +); + +interface Props {} + +export default function HarvestPlanProvider(props: PropsWithChildren) { + const { children } = props; + const { get, del, post, put } = useHTTP(); + const [{ as }] = useGlobalState(); + + const addHarvestPlan = (harvestPlan: pond.HarvestPlanSettings) => { + if (as) return post(pondURL("/harvestPlans?as=" + as), harvestPlan); + return post(pondURL("/harvestPlans"), harvestPlan); + }; + + const getHarvestPlan = (harvestPlanId: string) => { + if (as) return get(pondURL("/harvestPlans/" + harvestPlanId + "?as=" + as)); + return get(pondURL("/harvestPlans/" + harvestPlanId)); + }; + + const removeHarvestPlan = (key: string) => { + if (as) + return del(pondURL("/harvestPlans/" + key + "?as=" + as)); + return del(pondURL("/harvestPlans/" + key)); + }; + + const listHarvestPlans = ( + limit: number, + offset: number, + order?: "asc" | "desc", + orderBy?: string, + search?: string, + as?: string, + asRoot?: boolean + ) => { + return get( + pondURL( + "/harvestPlans" + + "?limit=" + + limit + + "&offset=" + + offset + + ("&order=" + or(order, "asc")) + + ("&by=" + or(orderBy, "key")) + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + + (search ? "&search=" + search : "") + + (as ? "&as=" + as : "") + ) + ); + }; + + const listHistory = (id: string, limit: number, offset: number) => { + return get( + pondURL("/harvestPlans/" + id + "/history?limit=" + limit + "&offset=" + offset) + ); + }; + + const updateHarvestPlan = ( + key: string, + harvestPlan: pond.HarvestPlanSettings, + asRoot?: boolean + ) => { + if (as) + return put( + pondURL( + "/harvestPlans/" + + key + + (as ? "?as=" + as : "") + + (asRoot ? "&asRoot=" + asRoot.toString() : "") + ), + harvestPlan + ); + return put( + pondURL("/harvestPlans/" + key + (asRoot ? "?asRoot=" + asRoot.toString() : "")), + harvestPlan + ); + }; + + return ( + + {children} + + ); +} + +export const useHarvestPlanAPI = () => useContext(HarvestPlanAPIContext); diff --git a/src/providers/pond/pond.tsx b/src/providers/pond/pond.tsx index fe8b617..471f465 100644 --- a/src/providers/pond/pond.tsx +++ b/src/providers/pond/pond.tsx @@ -23,6 +23,7 @@ import FileControllerProvider, { useFileControllerAPI } from "./fileControllerAP import MineProvider, { useMineAPI } from "./mineAPI"; import FieldMarkerProvider, {useFieldMarkerAPI} from "./fieldMarkerAPI"; import HomeMarkerProvider, {useHomeMarkerAPI} from "./homeMarkerAPI"; +import HarvestPlanProvider, {useHarvestPlanAPI} from "./harvestPlanAPI"; // import NoteProvider from "providers/noteAPI"; export const pondURL = (partial: string, demo: boolean = false): string => { @@ -62,7 +63,9 @@ export default function PondProvider(props: PropsWithChildren) { - {children} + + {children} + @@ -111,5 +114,6 @@ export { useFileControllerAPI, useMineAPI, useFieldMarkerAPI, - useHomeMarkerAPI + useHomeMarkerAPI, + useHarvestPlanAPI };