diff --git a/src/common/CustomProgressBar.tsx b/src/common/CustomProgressBar.tsx new file mode 100644 index 0000000..037259d --- /dev/null +++ b/src/common/CustomProgressBar.tsx @@ -0,0 +1,41 @@ +import { Box, colors } from "@mui/material"; +import { makeStyles } from "@mui/styles"; + +interface Props { + colour: string; + completed: number; +} + +const useStyles = makeStyles(() => ({ + container: { + height: 20, + width: "100%", + backgroundColor: "#e0e0de", + borderRadius: 50 + }, + filler: { + height: "100%", + // width: `${completed}%`, + // backgroundColor: bgcolor, + borderRadius: "inherit", + textAlign: "right" + }, + label: { + padding: 5, + color: colors.grey[900], + fontWeight: "bold" + } + }) +); + +export default function CustomProgressBar(props: Props) { + const { colour, completed } = props; + const classes = useStyles(); + return ( + + + {completed}% + + + ); +} diff --git a/src/common/ImageViewer.tsx b/src/common/ImageViewer.tsx new file mode 100644 index 0000000..4759185 --- /dev/null +++ b/src/common/ImageViewer.tsx @@ -0,0 +1,52 @@ +import { Box } from "@mui/material"; +import { pond } from "protobuf-ts/pond"; +import { useFileControllerAPI } from "providers"; +import React, { useEffect, useState } from "react"; + +interface Props { + image: pond.FileReference; + height?: number | string; + width?: number | string; +} + +export default function ImageViewer(props: Props) { + const { image, height, width } = props; + const fileController = useFileControllerAPI(); + // const [contentType, setContentType] = useState("") + const [imageData, setImageData] = useState(""); + + useEffect(() => { + //check local storage for the image + let imageURL = localStorage.getItem(image.uuid); + if (imageURL !== null) { + setImageData(imageURL); + } else { + //if its not there retrieve it + fileController + .retrieveFile(image.spacesPath) + .then(resp => { + let url = URL.createObjectURL(resp.data); + setImageData(url); + let reader = new FileReader(); + reader.addEventListener("load", () => { + //store the file url in base64 in local storage so it doesn't need to be loaded everytime + localStorage.setItem(image.uuid, reader.result as string); + }); + reader.readAsDataURL(resp.data); + }) + .catch(err => { + //console.log("There were errors retrieving the file"); + }); + } + }, [image, fileController]); + + //get the file from digital ocean by using our backend as a proxy + + return ( + + + {image.fileName} + + + ); +} diff --git a/src/contract/constractActions.tsx b/src/contract/constractActions.tsx new file mode 100644 index 0000000..3c1e036 --- /dev/null +++ b/src/contract/constractActions.tsx @@ -0,0 +1,107 @@ +import { Box, Card, Typography, useTheme } from "@mui/material"; +import GrainDescriber from "grain/GrainDescriber"; +import { Contract } from "models/Contract"; +import { Label, Pie, PieChart, ResponsiveContainer, Text } from "recharts"; + +interface Props { + contract: Contract; +} + +export default function ContractVisualizer(props: Props) { + const { contract } = props; + const theme = useTheme(); + + return ( + + + + {GrainDescriber(contract.grain()).name} + + + {contract.deliveredInPreferredUnit().toFixed(2)}/ + {contract.sizeInPreferredUnit().toFixed(2)} {contract.unit} + + + + + {/* ( + + + + )} + /> */} + + + + + + ); +} diff --git a/src/contract/contractActions.tsx b/src/contract/contractActions.tsx new file mode 100644 index 0000000..7d13123 --- /dev/null +++ b/src/contract/contractActions.tsx @@ -0,0 +1,219 @@ +import { + IconButton, + lighten, + ListItemIcon, + ListItemText, + Menu, + MenuItem, + Theme, + Tooltip +} from "@mui/material"; +import SettingsIcon from "@mui/icons-material/Settings"; +import MoreIcon from "@mui/icons-material/MoreVert"; +import React, { useState } from "react"; +import ObjectUsersIcon from "@mui/icons-material/AccountCircle"; +import ObjectTeamsIcon from "@mui/icons-material/SupervisedUserCircle"; +import ObjectUsers from "user/ObjectUsers"; +import { pond } from "protobuf-ts/pond"; +import { Scope } from "models"; +import ObjectTeams from "teams/ObjectTeams"; +import RemoveSelfFromObject from "user/RemoveSelfFromObject"; +import ShareObject from "user/ShareObject"; +import { blue } from "@mui/material/colors"; +import RemoveSelfIcon from "@mui/icons-material/ExitToApp"; +import { Share } from "@mui/icons-material"; +import { Contract } from "models/Contract"; +import ContractSettings from "./contractSettings"; +import { makeStyles } from "@mui/styles"; + +const useStyles = makeStyles((theme: Theme) => ({ + shareIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + removeIcon: { + color: "var(--status-alert)" + }, + red: { + color: "var(--status-alert)" + }, + blueIcon: { + color: blue["500"], + "&:hover": { + color: blue["600"] + } + }, + menuButton: { + cursor: "pointer", + "&:hover": { + color: lighten(theme.palette.background.default, 0.2), + } + } + }) +); +interface OpenState { + users: boolean; + teams: boolean; + settings: boolean; + removeSelf: boolean; + share: boolean; +} + +interface Props { + contract: Contract; + refreshCallback: () => void; + permissions: pond.Permission[]; +} + +export default function ContractActions(props: Props) { + const classes = useStyles(); + const { contract, refreshCallback, permissions } = props; + const [anchorEl, setAnchorEl] = React.useState(null); + const [openState, setOpenState] = useState({ + users: false, + teams: false, + settings: false, + removeSelf: false, + share: false + }); + + const groupMenu = () => { + return ( + setAnchorEl(null)} + keepMounted + disableAutoFocusItem> + {permissions.includes(pond.Permission.PERMISSION_SHARE) && ( + { + setOpenState({ ...openState, share: true }); + setAnchorEl(null); + }} + className={classes.menuButton} + dense> + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setOpenState({ ...openState, users: true }); + setAnchorEl(null); + }} + className={classes.menuButton} + > + + + + + + )} + {permissions.includes(pond.Permission.PERMISSION_USERS) && ( + { + setOpenState({ ...openState, teams: true }); + setAnchorEl(null); + }} + className={classes.menuButton} + > + + + + + + )} + { + setOpenState({ ...openState, removeSelf: true }); + setAnchorEl(null); + }} + className={classes.menuButton} + > + + + + + + + ); + }; + + const dialogs = () => { + const key = contract.key(); + const label = contract.name(); + return ( + + { + setOpenState({ ...openState, settings: false }); + }} + /> + setOpenState({ ...openState, share: false })} + /> + setOpenState({ ...openState, users: false })} + refreshCallback={refreshCallback} + /> + setOpenState({ ...openState, teams: false })} + refreshCallback={refreshCallback} + /> + { + setOpenState({ ...openState, removeSelf: false }); + }} + /> + + ); + }; + + return ( + + {permissions.includes(pond.Permission.PERMISSION_WRITE) && ( + + setOpenState({ ...openState, settings: true })}> + + + + )} + ) => setAnchorEl(event.currentTarget)}> + + + {dialogs()} + {groupMenu()} + + ); +} diff --git a/src/contract/contractList.tsx b/src/contract/contractList.tsx new file mode 100644 index 0000000..345ccc6 --- /dev/null +++ b/src/contract/contractList.tsx @@ -0,0 +1,509 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Button, + Typography, + Theme, + DialogContent, + DialogActions, + DialogTitle, + useTheme, + List, + ListItem, + Divider, + Grid2 as Grid, + Tabs, + Tab, + colors, + lighten +} from "@mui/material"; +import { ExpandMore } from "@mui/icons-material"; +import CustomProgressBar from "common/CustomProgressBar"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import GrainDescriber from "grain/GrainDescriber"; +import GrainTransaction from "grain/GrainTransaction"; +import { useMobile } from "hooks"; +import { Contract } from "models/Contract"; +import { pond } from "protobuf-ts/pond"; +import { useContractAPI } from "providers/pond/contractAPI"; +import React, { useEffect, useState } from "react"; +// import { useHistory } from "react-router"; +import { Label, Pie, PieChart, ResponsiveContainer, Text } from "recharts"; +import { stringToMaterialColour } from "utils"; +import { makeStyles } from "@mui/styles"; +import { useNavigate } from "react-router-dom"; + +interface Props { + contracts: Contract[]; + updateList: (newContractList: Contract[]) => void; + contractPermissions: Map; +} + +const useStyles = makeStyles((theme: Theme) => ({ + contractRow: { + cursor: "pointer", + "&:hover": { + backgroundColor: lighten(theme.palette.background.default, 0.25) + } + } + }) +); + +export default function ContractsList(props: Props) { + const { contracts, contractPermissions, updateList } = props; + const theme = useTheme(); + //will need map for contracts that have supported grain types + const [supportedTypes, setSupportedTypes] = useState>(new Map()); + //will need need map for custom contracts + const [customTypes, setCustomTypes] = useState>(new Map()); + const [openGrainTransactionDialog, setOpenGrainTransactionDialog] = useState(false); + const [currentContract, setCurrentContract] = useState(); + // const history = useHistory(); + const classes = useStyles(); + const [deleteWindow, setDeleteWindow] = useState(false); + const contractAPI = useContractAPI(); + const isMobile = useMobile(); + const [contractsDisplay, setContractsDisplay] = useState("open"); + const navigate = useNavigate(); + + useEffect(() => { + //loop through contracts setting them into the appropriate map at the appropriate location + let supportedTypes: Map = new Map(); + let customTypes: Map = new Map(); + + contracts.forEach(con => { + let contract: Contract | undefined; + if (contractsDisplay === "open" && con.settings.delivered < con.settings.size) contract = con; + if (contractsDisplay === "closed" && con.settings.delivered >= con.settings.size) + contract = con; + + if (contract) { + if (!contract.isCustom()) { + let mapItem = supportedTypes.get(contract.settings.commodity); + if (mapItem) { + mapItem.push(contract); + } else { + supportedTypes.set(contract.settings.commodity, [contract]); + } + } else { + let mapItem = customTypes.get(con.settings.customCommodity); + if (mapItem) { + mapItem.push(contract); + } else { + customTypes.set(contract.settings.customCommodity, [contract]); + } + } + } + }); + setSupportedTypes(supportedTypes); + setCustomTypes(customTypes); + }, [contracts, contractsDisplay]); + + const goToContract = (contract: Contract) => { + let path = "/contracts/" + contract.key(); + navigate(path, {state: { contract: contract }}) + // history.replace(path); + }; + + const deleteContract = () => { + if (currentContract) { + contractAPI + .removeContract(currentContract.key()) + .then(resp => { + let c = contracts; + contracts.splice(c.indexOf(currentContract), 1); + updateList(c); + }) + .catch(err => {}); + } + setDeleteWindow(false); + }; + + const displayTable = (contracts: Contract[]) => { + return ( + + + + + Name + + + Contract Date + + + Delivery Due + + + Size + + + Delivered + + + Progress + + + Remaining + + + Actions + + + + + {contracts.map(contract => { + const fillPercent = + Math.round((contract.settings.delivered / contract.settings.size) * 100 * 10) / 10; + return ( + { + goToContract(contract); + }}> + {contract.name()} + {contract.settings.contractDate} + {contract.settings.deliveryWindow?.endDate} + {contract.sizeInPreferredUnit() + " " + contract.unit} + {contract.deliveredInPreferredUnit() + " " + contract.unit} + + {/* */} + + {/* {((contract.settings.delivered/contract.settings.size)*100).toFixed(1) + "%"} */} + + + {contract.sizeInPreferredUnit() - + contract.deliveredInPreferredUnit() + + " " + + contract.unit} + + + + + + + ); + })} + +
+ ); + }; + + const circleGraph = (contract: Contract) => { + return ( + + + {/* ( + + + + )} + /> */} + + + + + ); + }; + + const mobileView = (contracts: Contract[]) => { + return ( + + {contracts.map((c, i) => { + const remaining = c.settings.size - c.settings.delivered; + return ( + + { + goToContract(c); + }}> + + + + {c.name()} + + {c.settings.contractDate} + + {remaining > 0 ? remaining + " " + c.unit + " remaining" : "Completed"} + + + + {circleGraph(c)} + + + + + + + + + + {i !== contracts.length - 1 && } + + ); + })} + + ); + }; + + const supportedGrains = () => { + let elements: JSX.Element[] = []; + supportedTypes.forEach((contracts, key) => { + elements.push( + + } + style={{ + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + backgroundColor: GrainDescriber(key).colour + }}> + + {GrainDescriber(key).name} - ({contracts.length} Contract + {contracts.length > 1 ? "s" : ""}) + + + + {isMobile ? mobileView(contracts) : displayTable(contracts)} + + + ); + }); + return elements; + }; + + const customGrains = () => { + let elements: JSX.Element[] = []; + customTypes.forEach((contracts, key) => { + elements.push( + + } + style={{ + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + backgroundColor: stringToMaterialColour(key) + }}> + + {key} - ({contracts.length} Contract{contracts.length > 1 ? "s" : ""}) + + + + {isMobile ? mobileView(contracts) : displayTable(contracts)} + + + ); + }); + return elements; + }; + + const deleteConfirmation = () => { + return ( + { + setDeleteWindow(false); + }}> + Delete Contract + Are you sure you want to delete this contract + + + + + + ); + }; + + return ( + + + {/* { + setContractsDisplay(value) + }}> + } + label={"Open"} + /> + } + label={"Closed"} + /> + */} + setContractsDisplay(value)} + indicatorColor="primary" + textColor="primary" + variant={isMobile ? "fullWidth" : "standard"} + style={{ paddingTop: 0 }} + aria-label="statusTabs"> + + + + {supportedGrains()} + {customGrains()} + + {currentContract && ( + { + setOpenGrainTransactionDialog(false); + }} + callback={() => {}} + /> + )} + {deleteConfirmation()} + + ); +} diff --git a/src/contract/contractSettings.tsx b/src/contract/contractSettings.tsx new file mode 100644 index 0000000..5d7cab4 --- /dev/null +++ b/src/contract/contractSettings.tsx @@ -0,0 +1,660 @@ +import { + Box, + Button, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Grid2 as Grid, + InputAdornment, + Step, + StepLabel, + Stepper, + Switch, + Tab, + Tabs, + TextField, + Typography +} from "@mui/material"; +import ResponsiveDialog from "common/ResponsiveDialog"; +import SearchSelect from "common/SearchSelect"; +import GrainDescriber from "grain/GrainDescriber"; +import { Contract } from "models/Contract"; +import moment from "moment"; +import { pond } from "protobuf-ts/pond"; +import { useGlobalState, useTaskAPI, useContractAPI } from "providers"; +import React, { useEffect, useState } from "react"; +import { getGrainUnit } from "utils"; + +interface Props { + open: boolean; + close: () => void; + contract?: Contract; + //if we do other types of contracts (not grain) the default type should be an input prop +} + +interface InputLabels { + commodityLabel: string; + sizeLabel: string; + deliveryLabel: string; + conversionLabel: string; + adornment: string; +} + +export default function ContractSettings(props: Props) { + const { open, close, contract } = props; + const contractAPI = useContractAPI(); + const [name, setName] = useState(""); + const [contractID, setContractID] = useState(""); //id + const [holder, setHolder] = useState(""); //holder + const [deliveryStart, setDeliveryStart] = useState(moment().format("YYYY-MM-DD")); //deliverystart + const [deliveryEnd, setDeliveryEnd] = useState(moment().format("YYYY-MM-DD")); //deliveryend + const [contractType, setContractType] = useState(1); //contracttype -- defaults to grain + //const [selectedContractType, setSelectedContractType] = useState