added the contract and contracts and all supporting files
This commit is contained in:
parent
07ccb42905
commit
832bb0f7e5
18 changed files with 2898 additions and 17 deletions
41
src/common/CustomProgressBar.tsx
Normal file
41
src/common/CustomProgressBar.tsx
Normal file
|
|
@ -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 (
|
||||
<Box className={classes.container}>
|
||||
<Box className={classes.filler} style={{ width: completed + "%", backgroundColor: colour }}>
|
||||
<span className={classes.label}>{completed}%</span>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
52
src/common/ImageViewer.tsx
Normal file
52
src/common/ImageViewer.tsx
Normal file
|
|
@ -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 (
|
||||
<Box>
|
||||
<a href={imageData} download={image.fileName}>
|
||||
<img src={imageData} alt={image.fileName} style={{ maxHeight: height, maxWidth: width }} />
|
||||
</a>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
107
src/contract/constractActions.tsx
Normal file
107
src/contract/constractActions.tsx
Normal file
|
|
@ -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 (
|
||||
<Card style={{ height: "100%", padding: 10 }}>
|
||||
<Box height={"15%"}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 20 }}>
|
||||
{GrainDescriber(contract.grain()).name}
|
||||
</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{contract.deliveredInPreferredUnit().toFixed(2)}/
|
||||
{contract.sizeInPreferredUnit().toFixed(2)} {contract.unit}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer height={"85%"}>
|
||||
<PieChart>
|
||||
{/* <Legend
|
||||
verticalAlign="bottom"
|
||||
content={() => (
|
||||
<Box textAlign="center" marginY={0.5}>
|
||||
<Chip label={contract.commodityLabel()} />
|
||||
</Box>
|
||||
)}
|
||||
/> */}
|
||||
<Pie
|
||||
outerRadius={"100%"}
|
||||
innerRadius="70%"
|
||||
stroke="transparent"
|
||||
data={[
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.delivered,
|
||||
fill: contract.colour
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.size - contract.settings.delivered
|
||||
}
|
||||
]}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%">
|
||||
<Label
|
||||
position="center"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={"2rem"}
|
||||
fontWeight={650}
|
||||
content={props => {
|
||||
const { cx, cy } = props.viewBox as any;
|
||||
return (
|
||||
<g>
|
||||
<Text
|
||||
x={cx}
|
||||
y={cy}
|
||||
dy={-15}
|
||||
textAnchor="middle"
|
||||
verticalAnchor="middle"
|
||||
fill={props.fill}
|
||||
fontWeight={props.fontWeight}
|
||||
fontSize={props.fontSize}>
|
||||
{((contract.settings.delivered / contract.settings.size) * 100).toFixed(1) +
|
||||
"%"}
|
||||
</Text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
position="center"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={"1.5rem"}
|
||||
fontWeight={650}
|
||||
content={props => {
|
||||
const { cx, cy } = props.viewBox as any;
|
||||
return (
|
||||
<g>
|
||||
<Text
|
||||
x={cx}
|
||||
y={cy}
|
||||
dy={15}
|
||||
textAnchor="middle"
|
||||
verticalAnchor="middle"
|
||||
fill={props.fill}
|
||||
fontWeight={props.fontWeight}
|
||||
fontSize={props.fontSize}>
|
||||
Delivered
|
||||
</Text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
219
src/contract/contractActions.tsx
Normal file
219
src/contract/contractActions.tsx
Normal file
|
|
@ -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 | HTMLElement>(null);
|
||||
const [openState, setOpenState] = useState<OpenState>({
|
||||
users: false,
|
||||
teams: false,
|
||||
settings: false,
|
||||
removeSelf: false,
|
||||
share: false
|
||||
});
|
||||
|
||||
const groupMenu = () => {
|
||||
return (
|
||||
<Menu
|
||||
id="menu"
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
keepMounted
|
||||
disableAutoFocusItem>
|
||||
{permissions.includes(pond.Permission.PERMISSION_SHARE) && (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, share: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
className={classes.menuButton}
|
||||
dense>
|
||||
<ListItemIcon>
|
||||
<Share className={classes.blueIcon} />
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary="Share" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, users: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
className={classes.menuButton}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ObjectUsersIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Users" />
|
||||
</MenuItem>
|
||||
)}
|
||||
{permissions.includes(pond.Permission.PERMISSION_USERS) && (
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, teams: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
className={classes.menuButton}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ObjectTeamsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Teams" />
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={() => {
|
||||
setOpenState({ ...openState, removeSelf: true });
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
className={classes.menuButton}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<RemoveSelfIcon className={classes.red} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Leave" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const dialogs = () => {
|
||||
const key = contract.key();
|
||||
const label = contract.name();
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContractSettings
|
||||
contract={contract}
|
||||
open={openState.settings}
|
||||
close={() => {
|
||||
setOpenState({ ...openState, settings: false });
|
||||
}}
|
||||
/>
|
||||
<ShareObject
|
||||
scope={{ kind: "contract", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.share}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, share: false })}
|
||||
/>
|
||||
<ObjectUsers
|
||||
scope={{ kind: "contract", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.users}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, users: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<ObjectTeams
|
||||
scope={{ kind: "contract", key: key } as Scope}
|
||||
label={label}
|
||||
permissions={permissions}
|
||||
isDialogOpen={openState.teams}
|
||||
closeDialogCallback={() => setOpenState({ ...openState, teams: false })}
|
||||
refreshCallback={refreshCallback}
|
||||
/>
|
||||
<RemoveSelfFromObject
|
||||
scope={{ kind: "contract", key: key } as Scope}
|
||||
path={"contracts"}
|
||||
label={label}
|
||||
isDialogOpen={openState.removeSelf}
|
||||
closeDialogCallback={() => {
|
||||
setOpenState({ ...openState, removeSelf: false });
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{permissions.includes(pond.Permission.PERMISSION_WRITE) && (
|
||||
<Tooltip title="Settings">
|
||||
<IconButton onClick={() => setOpenState({ ...openState, settings: true })}>
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
aria-owns={anchorEl ? "groupMenu" : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={(event: React.MouseEvent<HTMLButtonElement>) => setAnchorEl(event.currentTarget)}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
{dialogs()}
|
||||
{groupMenu()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
509
src/contract/contractList.tsx
Normal file
509
src/contract/contractList.tsx
Normal file
|
|
@ -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<string, pond.Permission[]>;
|
||||
}
|
||||
|
||||
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<Map<pond.Grain, Contract[]>>(new Map());
|
||||
//will need need map for custom contracts
|
||||
const [customTypes, setCustomTypes] = useState<Map<string, Contract[]>>(new Map());
|
||||
const [openGrainTransactionDialog, setOpenGrainTransactionDialog] = useState(false);
|
||||
const [currentContract, setCurrentContract] = useState<Contract>();
|
||||
// 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<pond.Grain, Contract[]> = new Map();
|
||||
let customTypes: Map<string, Contract[]> = 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 (
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Name</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Contract Date</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Delivery Due</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Size</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Delivered</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Progress</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Remaining</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Typography style={{ fontSize: 15, fontWeight: 650 }}>Actions</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{contracts.map(contract => {
|
||||
const fillPercent =
|
||||
Math.round((contract.settings.delivered / contract.settings.size) * 100 * 10) / 10;
|
||||
return (
|
||||
<TableRow
|
||||
key={contract.key()}
|
||||
className={classes.contractRow}
|
||||
onClick={() => {
|
||||
goToContract(contract);
|
||||
}}>
|
||||
<TableCell>{contract.name()}</TableCell>
|
||||
<TableCell>{contract.settings.contractDate}</TableCell>
|
||||
<TableCell>{contract.settings.deliveryWindow?.endDate}</TableCell>
|
||||
<TableCell>{contract.sizeInPreferredUnit() + " " + contract.unit}</TableCell>
|
||||
<TableCell>{contract.deliveredInPreferredUnit() + " " + contract.unit}</TableCell>
|
||||
<TableCell>
|
||||
{/* <progress value={contract.settings.delivered} max={contract.settings.size} style={{height: 10}}/> */}
|
||||
<CustomProgressBar colour={colors.blue[500]} completed={fillPercent} />
|
||||
{/* {((contract.settings.delivered/contract.settings.size)*100).toFixed(1) + "%"} */}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{contract.sizeInPreferredUnit() -
|
||||
contract.deliveredInPreferredUnit() +
|
||||
" " +
|
||||
contract.unit}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
!contractPermissions
|
||||
.get(contract.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setOpenGrainTransactionDialog(true);
|
||||
setCurrentContract(contract);
|
||||
}}>
|
||||
Deliver
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: contractPermissions
|
||||
.get(contract.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
? "red"
|
||||
: "",
|
||||
marginLeft: 5
|
||||
}}
|
||||
disabled={
|
||||
!contractPermissions
|
||||
.get(contract.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setDeleteWindow(true);
|
||||
setCurrentContract(contract);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
const circleGraph = (contract: Contract) => {
|
||||
return (
|
||||
<ResponsiveContainer>
|
||||
<PieChart>
|
||||
{/* <Legend
|
||||
verticalAlign="bottom"
|
||||
content={() => (
|
||||
<Box textAlign="center" marginY={0.5}>
|
||||
<Chip label={contract.commodityLabel()} />
|
||||
</Box>
|
||||
)}
|
||||
/> */}
|
||||
<Pie
|
||||
outerRadius={"100%"}
|
||||
innerRadius="70%"
|
||||
stroke="transparent"
|
||||
data={[
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.delivered,
|
||||
fill: contract.colour
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.size - contract.settings.delivered
|
||||
}
|
||||
]}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%">
|
||||
<Label
|
||||
position="center"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={"0.75rem"}
|
||||
fontWeight={650}
|
||||
content={props => {
|
||||
const { cx, cy } = props.viewBox as any;
|
||||
return (
|
||||
<g>
|
||||
<Text
|
||||
x={cx}
|
||||
y={cy}
|
||||
textAnchor="middle"
|
||||
verticalAnchor="middle"
|
||||
fill={props.fill}
|
||||
fontWeight={props.fontWeight}
|
||||
fontSize={props.fontSize}>
|
||||
{((contract.settings.delivered / contract.settings.size) * 100).toFixed(1) +
|
||||
"%"}
|
||||
</Text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const mobileView = (contracts: Contract[]) => {
|
||||
return (
|
||||
<List style={{ width: "100%" }}>
|
||||
{contracts.map((c, i) => {
|
||||
const remaining = c.settings.size - c.settings.delivered;
|
||||
return (
|
||||
<React.Fragment key={c.key()}>
|
||||
<ListItem
|
||||
style={{ paddingLeft: 0, paddingRight: 0 }}
|
||||
className={classes.contractRow}
|
||||
onClick={() => {
|
||||
goToContract(c);
|
||||
}}>
|
||||
<Grid container direction="row" spacing={2}>
|
||||
<Grid size={6}>
|
||||
<Typography noWrap style={{ fontWeight: 650 }}>
|
||||
{c.name()}
|
||||
</Typography>
|
||||
<Typography>{c.settings.contractDate}</Typography>
|
||||
<Typography>
|
||||
{remaining > 0 ? remaining + " " + c.unit + " remaining" : "Completed"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
{circleGraph(c)}
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ width: "100%" }}
|
||||
color="primary"
|
||||
disabled={
|
||||
!contractPermissions
|
||||
.get(c.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setOpenGrainTransactionDialog(true);
|
||||
setCurrentContract(c);
|
||||
}}>
|
||||
Deliver
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid size={6}>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
backgroundColor: contractPermissions
|
||||
.get(c.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
? "red"
|
||||
: "",
|
||||
width: "100%"
|
||||
}}
|
||||
disabled={
|
||||
!contractPermissions
|
||||
.get(c.key())
|
||||
?.includes(pond.Permission.PERMISSION_WRITE)
|
||||
}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setDeleteWindow(true);
|
||||
setCurrentContract(c);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
{i !== contracts.length - 1 && <Divider />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const supportedGrains = () => {
|
||||
let elements: JSX.Element[] = [];
|
||||
supportedTypes.forEach((contracts, key) => {
|
||||
elements.push(
|
||||
<Accordion key={key} style={{ borderRadius: 10, marginBottom: 10 }} defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
style={{
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
backgroundColor: GrainDescriber(key).colour
|
||||
}}>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: 20,
|
||||
fontWeight: 650,
|
||||
color: theme.palette.getContrastText(GrainDescriber(key).colour)
|
||||
}}>
|
||||
{GrainDescriber(key).name} - ({contracts.length} Contract
|
||||
{contracts.length > 1 ? "s" : ""})
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{isMobile ? mobileView(contracts) : displayTable(contracts)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
return elements;
|
||||
};
|
||||
|
||||
const customGrains = () => {
|
||||
let elements: JSX.Element[] = [];
|
||||
customTypes.forEach((contracts, key) => {
|
||||
elements.push(
|
||||
<Accordion key={key} style={{ borderRadius: 10, marginBottom: 10 }} defaultExpanded>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMore />}
|
||||
style={{
|
||||
borderTopLeftRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
backgroundColor: stringToMaterialColour(key)
|
||||
}}>
|
||||
<Typography style={{ fontSize: 20, fontWeight: 650 }}>
|
||||
{key} - ({contracts.length} Contract{contracts.length > 1 ? "s" : ""})
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{isMobile ? mobileView(contracts) : displayTable(contracts)}
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
});
|
||||
return elements;
|
||||
};
|
||||
|
||||
const deleteConfirmation = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={deleteWindow}
|
||||
onClose={() => {
|
||||
setDeleteWindow(false);
|
||||
}}>
|
||||
<DialogTitle>Delete Contract</DialogTitle>
|
||||
<DialogContent>Are you sure you want to delete this contract</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setDeleteWindow(false);
|
||||
}}
|
||||
style={{ marginRight: 10 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{ backgroundColor: "red" }}
|
||||
onClick={deleteContract}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box padding={2} paddingTop={0}>
|
||||
{/* <RadioGroup
|
||||
row
|
||||
value={contractsDisplay}
|
||||
onChange={(_, value) => {
|
||||
setContractsDisplay(value)
|
||||
}}>
|
||||
<FormControlLabel
|
||||
value={"open"}
|
||||
control={<Radio />}
|
||||
label={"Open"}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={"closed"}
|
||||
control={<Radio />}
|
||||
label={"Closed"}
|
||||
/>
|
||||
</RadioGroup> */}
|
||||
<Tabs
|
||||
value={contractsDisplay}
|
||||
onChange={(_, value) => setContractsDisplay(value)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant={isMobile ? "fullWidth" : "standard"}
|
||||
style={{ paddingTop: 0 }}
|
||||
aria-label="statusTabs">
|
||||
<Tab label={"Open"} value={"open"} />
|
||||
<Tab label={"Closed"} value={"closed"} />
|
||||
</Tabs>
|
||||
{supportedGrains()}
|
||||
{customGrains()}
|
||||
</Box>
|
||||
{currentContract && (
|
||||
<GrainTransaction
|
||||
open={openGrainTransactionDialog}
|
||||
mainObject={currentContract}
|
||||
restrictMatching
|
||||
asDestination
|
||||
allowAttachmentUploads
|
||||
close={() => {
|
||||
setOpenGrainTransactionDialog(false);
|
||||
}}
|
||||
callback={() => {}}
|
||||
/>
|
||||
)}
|
||||
{deleteConfirmation()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
660
src/contract/contractSettings.tsx
Normal file
660
src/contract/contractSettings.tsx
Normal file
|
|
@ -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<pond.ContractType>(1); //contracttype -- defaults to grain
|
||||
//const [selectedContractType, setSelectedContractType] = useState<Option | null>(null);
|
||||
const [customCommodity, setCustomCommodity] = useState(""); //customcommodity
|
||||
const [contractSize, setContractSize] = useState("0"); //contractsize
|
||||
const [deliveredAmount, setDeliveredAmount] = useState("0"); //deliveredamount
|
||||
const [contractValue, setContractValue] = useState("0"); //contractvalue
|
||||
const [commodity, setCommodity] = useState(0); //commodity
|
||||
const [useCustom, setUseCustom] = useState(false);
|
||||
const [currentTab, setCurrentTab] = useState(0);
|
||||
const [contractDate, setContractDate] = useState(moment().format("YYYY-MM-DD"));
|
||||
const [deleteWindow, setDeleteWindow] = useState(false);
|
||||
const [createDeliveryTask, setCreateDeliveryTask] = useState(false);
|
||||
const [conversionValue, setConversionValue] = useState("1");
|
||||
const [labels, setLabels] = useState<InputLabels>({
|
||||
sizeLabel: "Size",
|
||||
commodityLabel: "Commodity",
|
||||
conversionLabel: "Primary units per Secondary Unit",
|
||||
deliveryLabel: "Delivered",
|
||||
adornment: ""
|
||||
});
|
||||
const taskAPI = useTaskAPI();
|
||||
const [{ user }] = useGlobalState();
|
||||
|
||||
const closeSettings = () => {
|
||||
close();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (contract) {
|
||||
setName(contract.name());
|
||||
setContractID(contract.settings.contractId);
|
||||
setHolder(contract.settings.contractHolder);
|
||||
setDeliveryStart(contract.settings.deliveryWindow?.startDate ?? "");
|
||||
setDeliveryEnd(contract.settings.deliveryWindow?.endDate ?? "");
|
||||
setContractType(contract.settings.type);
|
||||
setCustomCommodity(contract.settings.customCommodity);
|
||||
setContractSize(contract.sizeInPreferredUnit().toFixed(2));
|
||||
setDeliveredAmount(contract.deliveredInPreferredUnit().toFixed(2));
|
||||
setContractValue(contract.settings.totalValue.toString());
|
||||
setCommodity(contract.settings.commodity);
|
||||
setContractDate(contract.settings.contractDate);
|
||||
setConversionValue(
|
||||
contract.settings.conversionValue > 0 ? contract.settings.conversionValue.toString() : "1"
|
||||
);
|
||||
setUseCustom(contract.isCustom());
|
||||
}
|
||||
}, [contract]);
|
||||
|
||||
useEffect(() => {
|
||||
switch (contractType) {
|
||||
case pond.ContractType.CONTRACT_TYPE_GRAIN:
|
||||
setLabels({
|
||||
sizeLabel: "Total Grain on Contract",
|
||||
commodityLabel: "Select Grain Type",
|
||||
conversionLabel: "Bushels Per Tonne",
|
||||
deliveryLabel: "Grain Delivered",
|
||||
adornment: getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu"
|
||||
});
|
||||
break;
|
||||
default:
|
||||
setLabels({
|
||||
sizeLabel: "Size",
|
||||
commodityLabel: "Commodity",
|
||||
conversionLabel: "Primary units per Secondary Unit",
|
||||
deliveryLabel: "Delivered",
|
||||
adornment: ""
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, [contractType]);
|
||||
|
||||
const createTask = (contractKey: string) => {
|
||||
let task: pond.TaskSettings = pond.TaskSettings.create({
|
||||
userId: user.id(),
|
||||
title: name + " Delivery Window",
|
||||
start: deliveryStart,
|
||||
startTime: "00:00",
|
||||
end: deliveryEnd,
|
||||
endTime: "00:00",
|
||||
objectKey: contractKey,
|
||||
description: "The window for delivering grain for the " + name + " contract",
|
||||
worker: user.id(),
|
||||
complete: false,
|
||||
colour: "22194D"
|
||||
});
|
||||
|
||||
taskAPI
|
||||
.addTask(task)
|
||||
.then(resp => {
|
||||
console.log("Task Created");
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("error creating task");
|
||||
});
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const conVal = isNaN(parseFloat(conversionValue)) ? 1 : parseFloat(conversionValue);
|
||||
const deliveredVal = isNaN(parseFloat(deliveredAmount))
|
||||
? 0
|
||||
: Math.round(Contract.toStoredUnit(parseFloat(deliveredAmount), contractType, conVal));
|
||||
const sizeVal = isNaN(parseFloat(contractSize))
|
||||
? 0
|
||||
: Math.round(Contract.toStoredUnit(parseFloat(contractSize), contractType, conVal));
|
||||
if (contract) {
|
||||
let settings: pond.ContractSettings = contract.settings;
|
||||
settings.commodity = commodity;
|
||||
settings.contractHolder = holder;
|
||||
settings.contractId = contractID;
|
||||
settings.customCommodity = customCommodity;
|
||||
settings.delivered = deliveredVal;
|
||||
settings.deliveryWindow = pond.ContractDelivery.create({
|
||||
startDate: deliveryStart,
|
||||
endDate: deliveryEnd
|
||||
});
|
||||
settings.conversionValue = conVal;
|
||||
settings.size = sizeVal;
|
||||
settings.totalValue = isNaN(parseFloat(contractValue)) ? 0 : parseFloat(contractValue);
|
||||
settings.type = contractType;
|
||||
settings.contractDate = contractDate;
|
||||
|
||||
contractAPI
|
||||
.updateContract(contract.key(), name, settings)
|
||||
.then(resp => {
|
||||
console.log("Updated Contract");
|
||||
close();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Error updating contract");
|
||||
});
|
||||
} else {
|
||||
let settings: pond.ContractSettings = pond.ContractSettings.create();
|
||||
settings.commodity = commodity;
|
||||
settings.contractHolder = holder;
|
||||
settings.contractId = contractID;
|
||||
settings.customCommodity = customCommodity;
|
||||
//settings.commoditySubtype = variant;
|
||||
settings.delivered = deliveredVal;
|
||||
settings.deliveryWindow = pond.ContractDelivery.create({
|
||||
startDate: deliveryStart,
|
||||
endDate: deliveryEnd
|
||||
});
|
||||
settings.size = sizeVal;
|
||||
settings.totalValue = isNaN(parseFloat(contractValue)) ? 0 : parseFloat(contractValue);
|
||||
settings.type = contractType;
|
||||
settings.conversionValue = conVal;
|
||||
settings.contractDate = contractDate;
|
||||
|
||||
contractAPI
|
||||
.addContract(name, settings)
|
||||
.then(resp => {
|
||||
if (createDeliveryTask) {
|
||||
createTask(resp.data.key);
|
||||
}
|
||||
close();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("Error adding contract");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteContract = () => {
|
||||
if (contract) {
|
||||
contractAPI
|
||||
.removeContract(contract.key())
|
||||
.then(resp => {
|
||||
console.log("contract deleted");
|
||||
close();
|
||||
})
|
||||
.catch(err => {
|
||||
console.log("failed to delete contract");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConfirmation = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={deleteWindow}
|
||||
onClose={() => {
|
||||
setDeleteWindow(false);
|
||||
}}>
|
||||
<DialogTitle>Delete Contract</DialogTitle>
|
||||
<DialogContent>Are you sure you want to delete this contract</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setDeleteWindow(false);
|
||||
}}
|
||||
style={{ marginRight: 10 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{ backgroundColor: "red" }}
|
||||
onClick={deleteContract}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const stepValid = () => {
|
||||
let valid = true;
|
||||
if (currentTab === 0) {
|
||||
if (name === "" || holder === "" || contractID === "") {
|
||||
valid = false;
|
||||
}
|
||||
} else if (currentTab === 1) {
|
||||
if (contractType === pond.ContractType.CONTRACT_TYPE_UNKNOWN) {
|
||||
valid = false;
|
||||
} else if (
|
||||
commodity === 0 ||
|
||||
(commodity === Contract.customValue(contractType) &&
|
||||
(customCommodity === "" || conversionValue === ""))
|
||||
) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
|
||||
const general = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography style={{ fontSize: 13, margin: 20 }}>
|
||||
General information about your contract such as: name, reference number, contract holder
|
||||
and delivery window will help organize your contracts for easy reference and deliveries.
|
||||
All fields marked with “*” are required but all fields are recommended
|
||||
</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={name}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setName(e.target.value);
|
||||
}}
|
||||
label={"*Contract Name"}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="date"
|
||||
label="Contract Signing Date"
|
||||
value={contractDate}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setContractDate(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={contractID}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setContractID(e.target.value);
|
||||
}}
|
||||
label={"*Reference #"}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
value={holder}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setHolder(e.target.value);
|
||||
}}
|
||||
label={"*Contract Holder"}
|
||||
/>
|
||||
<Box border={"2px solid grey"} borderRadius={5} padding={2} marginTop={2} marginBottom={2}>
|
||||
<Typography>Delivery Window</Typography>
|
||||
<TextField
|
||||
style={{ width: "45%" }}
|
||||
margin="dense"
|
||||
type="date"
|
||||
label="Start"
|
||||
value={deliveryStart}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setDeliveryStart(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
style={{ width: "45%", marginLeft: "10%" }}
|
||||
margin="dense"
|
||||
type="date"
|
||||
label="End"
|
||||
value={deliveryEnd}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setDeliveryEnd(e.target.value)}
|
||||
/>
|
||||
</Box>
|
||||
{!contract && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={createDeliveryTask}
|
||||
onChange={(_, checked) => {
|
||||
setCreateDeliveryTask(checked);
|
||||
}}
|
||||
name="Create Task For Delivery Window"
|
||||
/>
|
||||
}
|
||||
//disabled={!canEdit}
|
||||
label="Create Task For Delivery Window"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="number"
|
||||
value={contractValue}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">$</InputAdornment>
|
||||
}}
|
||||
error={isNaN(parseFloat(contractValue))}
|
||||
helperText={isNaN(parseFloat(contractValue)) ? "Must be a valid number" : ""}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setContractValue(e.target.value);
|
||||
}}
|
||||
label={"Contract Value"}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const details = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Box>
|
||||
<Typography
|
||||
style={{ fontSize: 13, paddingLeft: 5, paddingRight: 5, margin: 20, marginBottom: 0 }}>
|
||||
Select Grain Type, Total Grain on Contract (bu), and Grain Delivered. These settings can
|
||||
be changed at anytime. If your grain type is not listed use the “Custom” toggle.
|
||||
</Typography>
|
||||
<Typography
|
||||
style={{
|
||||
fontSize: 13,
|
||||
padding: 5,
|
||||
border: "1px solid grey",
|
||||
borderRadius: 5,
|
||||
margin: 20
|
||||
}}>
|
||||
Note: When delivering grain on contract from a field or a bin the Grain Types must match
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
style={{ marginTop: 10 }}>
|
||||
<Grid size={8}>
|
||||
<SearchSelect
|
||||
label="Contract Type*"
|
||||
selected={Contract.toTypeOption(contractType)}
|
||||
changeSelection={option => {
|
||||
if (option) {
|
||||
setContractType(option.value);
|
||||
}
|
||||
}}
|
||||
options={Contract.typeOptions()}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={4}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={useCustom}
|
||||
onChange={(_, checked) => {
|
||||
setUseCustom(checked);
|
||||
setCustomCommodity("");
|
||||
if (checked) {
|
||||
setCommodity(Contract.customValue(contractType));
|
||||
} else {
|
||||
setCommodity(0);
|
||||
}
|
||||
}}
|
||||
name="Custom"
|
||||
/>
|
||||
}
|
||||
//disabled={!canEdit}
|
||||
label="Custom"
|
||||
labelPlacement="start"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{!useCustom ? (
|
||||
<SearchSelect
|
||||
style={{ marginTop: 10 }}
|
||||
label={labels.commodityLabel + "*"}
|
||||
selected={Contract.toCommodityOption(contractType, commodity)}
|
||||
changeSelection={option => {
|
||||
if (option) {
|
||||
let c = 0;
|
||||
let conversion = 1;
|
||||
if (contractType) {
|
||||
switch (contractType) {
|
||||
case pond.ContractType.CONTRACT_TYPE_GRAIN:
|
||||
c = pond.Grain[option.value as keyof typeof pond.Grain];
|
||||
conversion = GrainDescriber(c).bushelsPerTonne;
|
||||
break;
|
||||
default:
|
||||
c = option.value;
|
||||
}
|
||||
}
|
||||
setCommodity(c);
|
||||
setConversionValue(conversion.toFixed(2));
|
||||
}
|
||||
}}
|
||||
options={Contract.commodityOptionsForType(contractType)}
|
||||
/>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
label="Custom*"
|
||||
value={customCommodity}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setCustomCommodity(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="number"
|
||||
label={labels.conversionLabel}
|
||||
value={conversionValue}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
error={isNaN(parseFloat(conversionValue))}
|
||||
helperText={isNaN(parseFloat(conversionValue)) ? "Must be a valid number" : ""}
|
||||
onChange={e => setConversionValue(e.target.value)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{/* <TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="variant"
|
||||
label="Variant"
|
||||
value={variant}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setVariant(e.target.value)}
|
||||
/> */}
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="number"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{labels.adornment}</InputAdornment>
|
||||
}}
|
||||
value={contractSize}
|
||||
error={isNaN(parseFloat(contractSize))}
|
||||
helperText={isNaN(parseFloat(contractSize)) ? "Must be a valid number" : ""}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setContractSize(e.target.value);
|
||||
}}
|
||||
label={labels.sizeLabel}
|
||||
/>
|
||||
<TextField
|
||||
fullWidth
|
||||
margin="dense"
|
||||
type="number"
|
||||
InputProps={{
|
||||
endAdornment: <InputAdornment position="end">{labels.adornment}</InputAdornment>
|
||||
}}
|
||||
value={deliveredAmount}
|
||||
error={isNaN(parseFloat(deliveredAmount))}
|
||||
helperText={isNaN(parseFloat(deliveredAmount)) ? "Must be a valid number" : ""}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => {
|
||||
setDeliveredAmount(e.target.value);
|
||||
}}
|
||||
label={labels.deliveryLabel}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const stepper = () => {
|
||||
if (contract === undefined) {
|
||||
return (
|
||||
<Stepper activeStep={currentTab}>
|
||||
<Step key={"general"}>
|
||||
<StepLabel>General</StepLabel>
|
||||
</Step>
|
||||
<Step key={"details"}>
|
||||
<StepLabel>Commodity</StepLabel>
|
||||
</Step>
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" width="100%">
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onChange={(_, value) => setCurrentTab(value)}
|
||||
indicatorColor="primary"
|
||||
textColor="primary"
|
||||
variant="fullWidth"
|
||||
aria-label="bin tabs"
|
||||
//classes={{ root: classes.tabs }}
|
||||
>
|
||||
<Tab label="General" />
|
||||
<Tab label="Commodity" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const stepperContent = (step: number) => {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return details();
|
||||
default:
|
||||
return general();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ResponsiveDialog open={open} onClose={closeSettings}>
|
||||
<DialogTitle>Contract Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
{/* <Tabs
|
||||
value={currentTab}
|
||||
onChange={(_, value) => setCurrentTab(value)}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Commodity" />
|
||||
</Tabs>
|
||||
<TabPanelMine value={currentTab} index={0}>
|
||||
{general()}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={currentTab} index={1}>
|
||||
{details()}
|
||||
</TabPanelMine> */}
|
||||
{stepper()}
|
||||
{stepperContent(currentTab)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Grid container direction="row-reverse" justifyContent="space-between">
|
||||
<Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={close}
|
||||
style={{ marginRight: 10 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
{!contract && currentTab > 0 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setCurrentTab(currentTab - 1);
|
||||
}}
|
||||
style={{ marginRight: 10 }}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{(contract || currentTab === 1) && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={submit}
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={!stepValid()}>
|
||||
Submit
|
||||
</Button>
|
||||
)}
|
||||
{!contract && currentTab < 1 && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setCurrentTab(currentTab + 1);
|
||||
}}
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={!stepValid()}>
|
||||
Next
|
||||
</Button>
|
||||
)}
|
||||
</Grid>
|
||||
{contract && (
|
||||
<Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ backgroundColor: "red" }}
|
||||
onClick={() => {
|
||||
setDeleteWindow(true);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
{deleteConfirmation()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
77
src/contract/contractTransactionFile.tsx
Normal file
77
src/contract/contractTransactionFile.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
Button,
|
||||
Card,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import ImageViewer from "common/ImageViewer";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useFileControllerAPI } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface FileReferences {
|
||||
images: pond.FileReference[];
|
||||
other: pond.FileReference[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
contractKey: string;
|
||||
transactionKey: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function ContractTransactionFile(props: Props) {
|
||||
const { contractKey, transactionKey, open, onClose } = props;
|
||||
const fileController = useFileControllerAPI();
|
||||
const [fileRefs, setFileRefs] = useState<FileReferences>({
|
||||
images: [],
|
||||
other: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
//retrieve the files for the passed in uuids
|
||||
if (transactionKey) {
|
||||
fileController
|
||||
.listFileRefs(transactionKey, [contractKey, transactionKey], ["contract", "transaction"])
|
||||
.then(resp => {
|
||||
let fileRefs: FileReferences = {
|
||||
images: [],
|
||||
other: []
|
||||
};
|
||||
resp.data.fileRefs.forEach(fileRef => {
|
||||
if (fileRef.metadata && fileRef.metadata.contentType.split("/")[0] === "image") {
|
||||
fileRefs.images.push(fileRef);
|
||||
} else {
|
||||
fileRefs.other.push(fileRef);
|
||||
}
|
||||
});
|
||||
setFileRefs(fileRefs);
|
||||
});
|
||||
}
|
||||
}, [contractKey, transactionKey, fileController]);
|
||||
|
||||
const close = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ResponsiveDialog open={open} onClose={close}>
|
||||
<DialogTitle>Attached Files</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>Attached Images</Typography>
|
||||
{fileRefs.images.map(file => (
|
||||
<Card key={file.uuid} style={{ width: "100%" }}>
|
||||
<ImageViewer image={file} width={"100%"} />
|
||||
</Card>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={close}>Close</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
73
src/contract/contractTransactionGraph.tsx
Normal file
73
src/contract/contractTransactionGraph.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { Box, Card, Typography } from "@mui/material";
|
||||
import { Contract } from "models/Contract";
|
||||
import { Transaction } from "models/Transaction";
|
||||
import BarGraph, { BarData } from "charts/BarGraph";
|
||||
import { useEffect, useState } from "react";
|
||||
import moment, { Moment } from "moment";
|
||||
import { getGrainUnit } from "utils";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
|
||||
interface Props {
|
||||
contract: Contract;
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
export default function ContractTransactionGraph(props: Props) {
|
||||
const { contract, transactions } = props;
|
||||
const [data, setData] = useState<BarData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let d: BarData[] = [];
|
||||
transactions.forEach(t => {
|
||||
let time: Moment = moment(t.transaction.timestamp);
|
||||
let value = 0;
|
||||
//this seems confusing but the transaction model contains the transaction as defined in the pond and then need to get the specific transaction data
|
||||
//which is defined in the pond as a oneof for different types of transactions ie: grainTransaction
|
||||
let transaction = t.transaction.transaction;
|
||||
if (transaction?.grainTransaction) {
|
||||
value = transaction.grainTransaction.bushels;
|
||||
if (
|
||||
getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT &&
|
||||
transaction.grainTransaction.bushelsPerTonne > 1
|
||||
) {
|
||||
value = value / transaction.grainTransaction.bushelsPerTonne;
|
||||
}
|
||||
}
|
||||
d.push({
|
||||
timestamp: time.valueOf(),
|
||||
value: value
|
||||
});
|
||||
});
|
||||
setData(d);
|
||||
}, [transactions]);
|
||||
|
||||
const daysRemaining = () => {
|
||||
let days = "Delivery Window Closed";
|
||||
const endDate = contract.settings.deliveryWindow?.endDate;
|
||||
|
||||
if (endDate && moment(endDate).isAfter(moment.now())) {
|
||||
days = "To be completed " + moment(endDate).fromNow();
|
||||
}
|
||||
return days;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card style={{ height: "100%", padding: 10 }}>
|
||||
<Box height={"10%"}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 20 }}>
|
||||
Deliveries ({daysRemaining()})
|
||||
</Typography>
|
||||
</Box>
|
||||
<BarGraph
|
||||
data={data}
|
||||
barColour={contract.colour}
|
||||
useGradient
|
||||
yMin={0}
|
||||
customHeight={"90%"}
|
||||
labels
|
||||
labelSize={20}
|
||||
labelColour="white"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
391
src/contract/contractTransactionTable.tsx
Normal file
391
src/contract/contractTransactionTable.tsx
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Grid2 as Grid,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { AttachFile, Person, Visibility } from "@mui/icons-material";
|
||||
import FileUploader from "common/FileUploads/FileUploader";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
// import { getTableIcons } from "common/ResponsiveTable";
|
||||
// import MaterialTable, { Column } from "material-table";
|
||||
import { Bin } from "models";
|
||||
import { Contract } from "models/Contract";
|
||||
import { GrainBag } from "models/GrainBag";
|
||||
import { Transaction } from "models/Transaction";
|
||||
import moment from "moment";
|
||||
import ObjectDescriber from "objects/ObjectDescriber";
|
||||
import DeleteIcon from "products/AgIcons/Delete";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useBinAPI, useGrainBagAPI, useSnackbar, useTransactionAPI, useUserAPI } from "providers";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import TransactionDataDisplay from "transactions/transactionDataDisplay";
|
||||
import ContractTransactionFile from "./contractTransactionFile";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import ResponsiveTable, { Column } from "common/ResponsiveTable";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
interface Props {
|
||||
contract: Contract;
|
||||
permissions: pond.Permission[];
|
||||
transactions: Transaction[];
|
||||
objectNames: Map<string, string>;
|
||||
transactionRevokedCallback: (transactionKey: string) => void;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
userAvatar: {
|
||||
width: 36,
|
||||
height: 36
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
export default function ContractTransactionTable(props: Props) {
|
||||
const { transactions, objectNames, contract, permissions, transactionRevokedCallback } = props;
|
||||
const [tableData, setTableData] = useState<Transaction[]>([]);
|
||||
const [displayData, setDisplayData] = useState<Transaction[]>([]);
|
||||
const [tablePage, setTablePage] = useState(0)
|
||||
const [pageSize, setPageSize] = useState(10)
|
||||
const userAPI = useUserAPI();
|
||||
const [userProfiles, setUserProfiles] = useState<Map<string, pond.UserProfile>>(new Map());
|
||||
const [openRevokeDialog, setOpenRevokeDialog] = useState(false);
|
||||
const classes = useStyles();
|
||||
const [activeTransaction, setActiveTransaction] = useState<Transaction>(Transaction.create());
|
||||
const transactionAPI = useTransactionAPI();
|
||||
const binAPI = useBinAPI();
|
||||
const bagAPI = useGrainBagAPI();
|
||||
const [transactionSource, setTransactionSource] = useState<Bin | GrainBag | undefined>(undefined);
|
||||
const [openFileViewer, setOpenFileViewer] = useState(false);
|
||||
const [openAttachmentDialog, setOpenAttachmentDialog] = useState(false);
|
||||
const [selectedTransaction, setSelectedTransaction] = useState<Transaction>(Transaction.create());
|
||||
const [attachmentList, setAttachmentList] = useState<string[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const { openSnack } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
let users: string[] = [];
|
||||
transactions.forEach(transaction => {
|
||||
if (
|
||||
!users.includes(transaction.transaction.userId) &&
|
||||
transaction.transaction.userId !== "unknown"
|
||||
) {
|
||||
users.push(transaction.transaction.userId);
|
||||
}
|
||||
});
|
||||
setTableData(transactions);
|
||||
userAPI.getProfiles(users).then(resp => {
|
||||
let userMap: Map<string, pond.UserProfile> = new Map<string, pond.UserProfile>();
|
||||
resp.forEach(profile => {
|
||||
userMap.set(profile.id, profile);
|
||||
});
|
||||
setUserProfiles(userMap);
|
||||
});
|
||||
}, [transactions, userAPI]);
|
||||
|
||||
useEffect(()=>{
|
||||
let clone: Transaction[] = cloneDeep(tableData)
|
||||
let rows = clone.splice(tablePage * pageSize, pageSize)
|
||||
setDisplayData(rows)
|
||||
},[tablePage, pageSize, tableData])
|
||||
|
||||
const getTransactionSource = (transaction: Transaction) => {
|
||||
setTransactionSource(undefined);
|
||||
switch (transaction.transaction.fromObject) {
|
||||
case pond.ObjectType.OBJECT_TYPE_BIN:
|
||||
binAPI.getBin(transaction.transaction.fromKey).then(resp => {
|
||||
let bin = Bin.create(resp.data);
|
||||
setTransactionSource(bin);
|
||||
});
|
||||
break;
|
||||
case pond.ObjectType.OBJECT_TYPE_GRAIN_BAG:
|
||||
bagAPI.getGrainBag(transaction.transaction.fromKey).then(resp => {
|
||||
if (resp.data.grainBag) {
|
||||
let bag = GrainBag.create(resp.data.grainBag);
|
||||
setTransactionSource(bag);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const canRevoke = () => {
|
||||
let valid = true;
|
||||
if (transactionSource) {
|
||||
const amount = activeTransaction.transaction.transaction?.grainTransaction?.bushels ?? 0;
|
||||
switch (activeTransaction.transaction.fromObject) {
|
||||
case pond.ObjectType.OBJECT_TYPE_BIN:
|
||||
const bin = transactionSource as Bin;
|
||||
const inv = bin.settings.inventory;
|
||||
const specs = bin.settings.specs;
|
||||
if (inv && specs) {
|
||||
if (inv.grainBushels + amount > specs.bushelCapacity) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case pond.ObjectType.OBJECT_TYPE_GRAIN_BAG:
|
||||
const bag = transactionSource as GrainBag;
|
||||
if (bag.bushels() + amount > bag.capacity()) {
|
||||
valid = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
|
||||
const revokeTransaction = () => {
|
||||
//use the transaction api to revoke the transaction
|
||||
//--moved all the functionality for adjusting the objects involved to the backend so this can be handled with a single call--
|
||||
transactionAPI
|
||||
.revokeTransaction(activeTransaction.transaction.key)
|
||||
.then(resp => {
|
||||
openSnack("Transaction revoked and inventory moved back");
|
||||
transactionRevokedCallback(activeTransaction.transaction.key);
|
||||
})
|
||||
.catch(err => {
|
||||
openSnack("There was was a problem revoking the transaction");
|
||||
});
|
||||
};
|
||||
|
||||
const revokeDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={openRevokeDialog}
|
||||
onClose={() => {
|
||||
setOpenRevokeDialog(false);
|
||||
}}>
|
||||
<DialogTitle>Revoke Transaction</DialogTitle>
|
||||
<DialogContent>
|
||||
Revoking this transaction will return the grain from the contract to its origin.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpenRevokeDialog(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
revokeTransaction();
|
||||
setOpenRevokeDialog(false);
|
||||
}}
|
||||
disabled={!canRevoke()}>
|
||||
Confirm
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const displayUser = (userID: string) => {
|
||||
let profile = userProfiles.get(userID);
|
||||
if (profile) {
|
||||
return (
|
||||
<Grid container direction="row" alignItems="center" spacing={2}>
|
||||
<Grid>
|
||||
{profile.avatar ? (
|
||||
<Avatar alt={profile.name} src={profile.avatar} className={classes.userAvatar}>
|
||||
{!profile.avatar && profile.name}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar alt={profile.name} className={classes.userAvatar}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography>{profile.name}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Grid container direction="row" alignItems="center" spacing={2}>
|
||||
<Grid>
|
||||
<Avatar alt={"unknownUser"} className={classes.userAvatar}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Typography>Unknown</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const attachmentDialog = () => {
|
||||
return (
|
||||
<ResponsiveDialog
|
||||
open={openAttachmentDialog}
|
||||
onClose={() => {
|
||||
setOpenAttachmentDialog(false);
|
||||
}}>
|
||||
<DialogTitle>Select Files to Upload</DialogTitle>
|
||||
<DialogContent>
|
||||
<FileUploader
|
||||
keys={[contract.key(), selectedTransaction.transaction.key]}
|
||||
types={["contract", "transaction"]}
|
||||
toAttach={{
|
||||
key: selectedTransaction.transaction.key,
|
||||
type: pond.ObjectType.OBJECT_TYPE_TRANSACTION
|
||||
}}
|
||||
uploadStart={() => {
|
||||
//disable close button
|
||||
setUploading(true);
|
||||
}}
|
||||
uploadEnd={fileID => {
|
||||
let aL = attachmentList;
|
||||
//update the transaction with the new fileID
|
||||
setUploading(false);
|
||||
if (fileID) {
|
||||
aL.push(fileID);
|
||||
}
|
||||
setAttachmentList(aL);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
disabled={uploading}
|
||||
onClick={() => {
|
||||
setOpenAttachmentDialog(false);
|
||||
}}>
|
||||
Close
|
||||
</Button>
|
||||
{/* <Button
|
||||
disabled={uploading}
|
||||
onClick={() => {
|
||||
updateTransaction();
|
||||
}}>
|
||||
Attach Files
|
||||
</Button> */}
|
||||
</DialogActions>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: Column<Transaction>[] = [
|
||||
{
|
||||
title: "Date",
|
||||
render: row => {
|
||||
return <Typography>{moment(row.transaction.timestamp).format("YYYY-MM-DD")}</Typography>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
render: row => {
|
||||
return displayUser(row.transaction.userId);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Source",
|
||||
render: row => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Typography>{ObjectDescriber(row.transaction.fromObject).name}</Typography>
|
||||
<Typography>
|
||||
{objectNames ? objectNames.get(row.transaction.fromKey) : row.transaction.fromKey}
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Transaction",
|
||||
render: row => {
|
||||
return <TransactionDataDisplay transaction={row} />;
|
||||
}
|
||||
},
|
||||
//this column should be hidden from users that dont have edit permissions to the contract
|
||||
{
|
||||
title: "Revoke Transaction",
|
||||
render: row => {
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
style={{ backgroundColor: "red" }}
|
||||
onClick={() => {
|
||||
setActiveTransaction(row);
|
||||
getTransactionSource(row);
|
||||
setOpenRevokeDialog(true);
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "Attachments",
|
||||
render: row => {
|
||||
return (
|
||||
<Grid container direction="row" spacing={2}>
|
||||
<Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
|
||||
//style={{ backgroundColor: "red" }}
|
||||
onClick={() => {
|
||||
setSelectedTransaction(row);
|
||||
setOpenFileViewer(true);
|
||||
}}>
|
||||
<Visibility />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_FILE_MANAGEMENT)}
|
||||
onClick={() => {
|
||||
setSelectedTransaction(row);
|
||||
setOpenAttachmentDialog(true);
|
||||
}}>
|
||||
<AttachFile />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Card>
|
||||
<ResponsiveTable<Transaction>
|
||||
page={tablePage}
|
||||
pageSize={pageSize}
|
||||
rows={displayData}
|
||||
columns={columns}
|
||||
total={tableData.length}
|
||||
handleRowsPerPageChange={()=>{}}
|
||||
setPage={()=>{}}
|
||||
/>
|
||||
{/* <MaterialTable
|
||||
title={"Deliveries"}
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
icons={getTableIcons()}
|
||||
/> */}
|
||||
</Card>
|
||||
<ContractTransactionFile
|
||||
contractKey={contract.key()}
|
||||
transactionKey={selectedTransaction.transaction.key}
|
||||
open={openFileViewer}
|
||||
onClose={() => {
|
||||
setOpenFileViewer(false);
|
||||
}}
|
||||
/>
|
||||
{revokeDialog()}
|
||||
{attachmentDialog()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
107
src/contract/contractVisualizer.tsx
Normal file
107
src/contract/contractVisualizer.tsx
Normal file
|
|
@ -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 (
|
||||
<Card style={{ height: "100%", padding: 10 }}>
|
||||
<Box height={"15%"}>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 20 }}>
|
||||
{GrainDescriber(contract.grain()).name}
|
||||
</Typography>
|
||||
<Typography style={{ fontWeight: 650, fontSize: 15 }}>
|
||||
{contract.deliveredInPreferredUnit().toFixed(2)}/
|
||||
{contract.sizeInPreferredUnit().toFixed(2)} {contract.unit}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ResponsiveContainer height={"85%"}>
|
||||
<PieChart>
|
||||
{/* <Legend
|
||||
verticalAlign="bottom"
|
||||
content={() => (
|
||||
<Box textAlign="center" marginY={0.5}>
|
||||
<Chip label={contract.commodityLabel()} />
|
||||
</Box>
|
||||
)}
|
||||
/> */}
|
||||
<Pie
|
||||
outerRadius={"100%"}
|
||||
innerRadius="70%"
|
||||
stroke="transparent"
|
||||
data={[
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.delivered,
|
||||
fill: contract.colour
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
value: contract.settings.size - contract.settings.delivered
|
||||
}
|
||||
]}
|
||||
dataKey="value"
|
||||
cx="50%"
|
||||
cy="50%">
|
||||
<Label
|
||||
position="center"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={"2rem"}
|
||||
fontWeight={650}
|
||||
content={props => {
|
||||
const { cx, cy } = props.viewBox as any;
|
||||
return (
|
||||
<g>
|
||||
<Text
|
||||
x={cx}
|
||||
y={cy}
|
||||
dy={-15}
|
||||
textAnchor="middle"
|
||||
verticalAnchor="middle"
|
||||
fill={props.fill}
|
||||
fontWeight={props.fontWeight}
|
||||
fontSize={props.fontSize}>
|
||||
{((contract.settings.delivered / contract.settings.size) * 100).toFixed(1) +
|
||||
"%"}
|
||||
</Text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label
|
||||
position="center"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={"1.5rem"}
|
||||
fontWeight={650}
|
||||
content={props => {
|
||||
const { cx, cy } = props.viewBox as any;
|
||||
return (
|
||||
<g>
|
||||
<Text
|
||||
x={cx}
|
||||
y={cy}
|
||||
dy={15}
|
||||
textAnchor="middle"
|
||||
verticalAnchor="middle"
|
||||
fill={props.fill}
|
||||
fontWeight={props.fontWeight}
|
||||
fontSize={props.fontSize}>
|
||||
Delivered
|
||||
</Text>
|
||||
</g>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -40,6 +40,8 @@ const Marketplace = lazy(() => import("userFeatures/UserFeatures"))
|
|||
const Logs = lazy(() => import("pages/Logs"))
|
||||
const Firmware = lazy(() => import("pages/Firmware"));
|
||||
const Nfc = lazy(() => import("pages/Nfc"));
|
||||
const Contracts = lazy(() => import("pages/Contracts"));
|
||||
const Contract = lazy(() => import("pages/Contract"));
|
||||
|
||||
export const appendToUrl = (appendage: number | string) => {
|
||||
const basePath = location.pathname.replace(/\/$/, "");
|
||||
|
|
@ -317,6 +319,8 @@ export default function Router() {
|
|||
<Route path="firmware" element={<Firmware />} />
|
||||
)}
|
||||
<Route path="nfc" element={<Nfc />} />
|
||||
<Route path="contracts" element={<Contracts />} />
|
||||
<Route path="contracts/:contractKey" element={<Contract />} />
|
||||
|
||||
{/* Map pages */}
|
||||
<Route path="visualFarm" element={<FieldMap />} />
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import CableIcon from "products/Bindapt/CableIcon";
|
|||
import FieldListIcon from "products/AgIcons/FieldList";
|
||||
import MarketplaceIcon from "products/CommonIcons/marketplaceIcon";
|
||||
import DataDuckIcon from "products/Bindapt/DataDuckIcon";
|
||||
import ContractsIcon from "products/CommonIcons/contractIcon";
|
||||
|
||||
const drawerWidth = 230;
|
||||
|
||||
|
|
@ -328,18 +329,34 @@ export default function SideNavigator(props: Props) {
|
|||
{open && <ListItemText primary="Tasks" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Transactions" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-transactions"
|
||||
onClick={() => goTo("/transactions")}
|
||||
classes={getClasses("/transactions")}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SyncAlt />
|
||||
</ListItemIcon>
|
||||
{open && <ListItemText primary="Transactions" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
{(isAg || user.hasFeature("admin")) && (
|
||||
<Tooltip title="Transactions" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-transactions"
|
||||
onClick={() => goTo("/transactions")}
|
||||
classes={getClasses("/transactions")}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<SyncAlt />
|
||||
</ListItemIcon>
|
||||
{open && <ListItemText primary="Transactions" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(isAg || user.hasFeature("admin")) && (
|
||||
<Tooltip title="Contracts" placement="right">
|
||||
<ListItemButton
|
||||
id="tour-contracts"
|
||||
onClick={() => goTo("/contracts")}
|
||||
classes={getClasses("/contracts")}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ContractsIcon />
|
||||
</ListItemIcon>
|
||||
{open && <ListItemText primary="Contracts" />}
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(user.hasFeature("installer") && isAg || user.hasFeature("admin")) &&
|
||||
<Tooltip title="Cable Estimator" placement="right">
|
||||
<ListItemButton
|
||||
|
|
|
|||
326
src/pages/Contract.tsx
Normal file
326
src/pages/Contract.tsx
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Drawer,
|
||||
Grid2 as Grid,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
Theme,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import ContractTransactionGraph from "contract/contractTransactionGraph";
|
||||
import ContractVisualizer from "contract/contractVisualizer";
|
||||
import { useMobile, useThemeType } from "hooks";
|
||||
// import { MatchParams } from "navigation/Routes";
|
||||
import { useContractAPI } from "providers/pond/contractAPI";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// import { Redirect, useRouteMatch } from "react-router";
|
||||
import PageContainer from "./PageContainer";
|
||||
import { Contract as IContract } from "models/Contract";
|
||||
import ObjectControls from "common/ObjectControls";
|
||||
import ContractActions from "contract/contractActions";
|
||||
import { useGlobalState, useUserAPI } from "providers";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { Scope } from "models";
|
||||
import { Transaction } from "models/Transaction";
|
||||
import ContractTransactionTable from "contract/contractTransactionTable";
|
||||
import GrainTransaction from "grain/GrainTransaction";
|
||||
import NotesIcon from "@mui/icons-material/Notes";
|
||||
import Chat from "chat/Chat";
|
||||
import ContractsIcon from "products/CommonIcons/contractIcon";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: any;
|
||||
value: any;
|
||||
}
|
||||
|
||||
function TabPanelMine(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
aria-labelledby={`simple-tab-${index}`}
|
||||
{...other}>
|
||||
{value === index && <React.Fragment>{children}</React.Fragment>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => {
|
||||
const themeType = useThemeType();
|
||||
return ({
|
||||
inactiveButton: {
|
||||
color: themeType === "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
|
||||
},
|
||||
activeButton: {
|
||||
color: themeType === "light" ? theme.palette.common.black : theme.palette.common.white,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
width: theme.spacing(5),
|
||||
height: theme.spacing(5),
|
||||
border: "1px solid",
|
||||
borderColor: theme.palette.divider
|
||||
},
|
||||
drawerPaper: {
|
||||
height: "100%",
|
||||
width: "30%"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export default function Contract() {
|
||||
// const match = useRouteMatch<MatchParams>();
|
||||
// const contractKey = match.params.contractKey;
|
||||
const contractKey = useParams<{ contractKey: string }>()?.contractKey ?? "";
|
||||
|
||||
const classes = useStyles();
|
||||
const contractAPI = useContractAPI();
|
||||
const isMobile = useMobile();
|
||||
const [contract, setContract] = useState<IContract>(IContract.create());
|
||||
const [validTransactions, setValidTransactions] = useState<Map<string, Transaction>>(new Map());
|
||||
//have to use this as the array passed to the bar graph, for whatever reason getting the array from the values of the map causes the labels to not appear
|
||||
const [transArray, setTransArray] = useState<Transaction[]>([]);
|
||||
const [openGrainTransactionDialog, setOpenGrainTransactionDialog] = useState(false);
|
||||
const [displayTab, setDisplayTab] = useState(0);
|
||||
const [openNoteDrawer, setOpenNoteDrawer] = useState(false);
|
||||
//const [revokedTransactions, setRevokedTransactions] = useState<Transaction[]>([]);
|
||||
const userAPI = useUserAPI();
|
||||
const [{ user, as }] = useGlobalState();
|
||||
const [permissions, setPermissions] = useState<pond.Permission[]>([]);
|
||||
const [objectMap, setObjectMap] = useState<Map<string, string>>(new Map());
|
||||
const [loading, setLoading] = useState(false);
|
||||
// const [invalid, setInvalid] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let key = contractKey;
|
||||
let kind = "contract";
|
||||
if (as) {
|
||||
key = as;
|
||||
kind = "team";
|
||||
}
|
||||
userAPI.getUser(user.id(), { key: key, kind: kind } as Scope).then(resp => {
|
||||
setPermissions(resp.permissions);
|
||||
});
|
||||
}, [as, contractKey, userAPI, user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
setLoading(true);
|
||||
contractAPI
|
||||
.getContractPageData(contractKey)
|
||||
.then(resp => {
|
||||
setContract(IContract.any(resp.data.contract));
|
||||
//setValidTransactions(resp.data.transactions.map(t => Transaction.create(t)));
|
||||
let valid: Map<string, Transaction> = new Map();
|
||||
let tArray: Transaction[] = [];
|
||||
resp.data.transactions.forEach(transaction => {
|
||||
let t = Transaction.create(transaction);
|
||||
if (
|
||||
t.transaction.state === pond.TransactionState.TRANSACTION_STATE_VALID ||
|
||||
t.transaction.state === pond.TransactionState.TRANSACTION_STATE_CORRECTION
|
||||
) {
|
||||
valid.set(t.transaction.key, t);
|
||||
tArray.push(t);
|
||||
}
|
||||
// else {
|
||||
// revoked.push(t);
|
||||
// }
|
||||
});
|
||||
setValidTransactions(valid);
|
||||
setTransArray(tArray);
|
||||
//setRevokedTransactions(revoked);
|
||||
let tempMap: Map<string, string> = new Map();
|
||||
Object.keys(resp.data.nameMap).forEach(key => {
|
||||
tempMap.set(key, resp.data.nameMap[key]);
|
||||
});
|
||||
setObjectMap(tempMap);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
//setInvalid(true);
|
||||
});
|
||||
}
|
||||
}, [contractKey, contractAPI]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const contractDisplay = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{isMobile ? (
|
||||
<Grid
|
||||
container
|
||||
alignContent="center"
|
||||
alignItems="center"
|
||||
direction="row"
|
||||
style={{ marginBottom: 15 }}>
|
||||
<Grid size={9}>
|
||||
<Typography style={{ paddingRight: 20, fontSize: 30, fontWeight: 650 }} noWrap>
|
||||
{contract.name()}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={3}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
onClick={e => {
|
||||
setOpenGrainTransactionDialog(true);
|
||||
}}>
|
||||
+ Add Delivery
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
) : (
|
||||
<Box display="flex" flexDirection="row" marginBottom={2}>
|
||||
<Typography style={{ paddingRight: 20, fontSize: 30, fontWeight: 650 }} noWrap>
|
||||
{contract.name()}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!permissions.includes(pond.Permission.PERMISSION_WRITE)}
|
||||
onClick={e => {
|
||||
setOpenGrainTransactionDialog(true);
|
||||
}}>
|
||||
+ Add Delivery
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Grid container spacing={2} direction={"row"}>
|
||||
<Grid size={isMobile ? 12 : 4} style={{ height: isMobile ? 300 : 400 }}>
|
||||
<ContractVisualizer contract={contract} />
|
||||
</Grid>
|
||||
<Grid size={isMobile ? 12 : 8} style={{ height: isMobile ? 300 : 400 }}>
|
||||
<ContractTransactionGraph contract={contract} transactions={transArray} />
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<ContractTransactionTable
|
||||
permissions={permissions}
|
||||
contract={contract}
|
||||
transactions={transArray}
|
||||
objectNames={objectMap}
|
||||
transactionRevokedCallback={t => {
|
||||
let vt = validTransactions;
|
||||
vt.delete(t);
|
||||
setValidTransactions(vt);
|
||||
setTransArray(Array.from(vt.values()));
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<ObjectControls
|
||||
actions={
|
||||
<ContractActions
|
||||
contract={contract}
|
||||
permissions={permissions}
|
||||
refreshCallback={() => {}}
|
||||
/>
|
||||
}
|
||||
objectButton={
|
||||
<IconButton
|
||||
size="small"
|
||||
style={{
|
||||
marginTop: "0.3625rem",
|
||||
marginBottom: "0.3625rem",
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
onClick={() => {
|
||||
setDisplayTab(0);
|
||||
}}
|
||||
className={displayTab === 0 ? classes.activeButton : classes.inactiveButton}
|
||||
component="span">
|
||||
<ContractsIcon />
|
||||
</IconButton>
|
||||
}
|
||||
notesButton={
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (isMobile) {
|
||||
setDisplayTab(1);
|
||||
} else {
|
||||
setOpenNoteDrawer(true);
|
||||
}
|
||||
}}
|
||||
size="small"
|
||||
style={{
|
||||
marginTop: "0.3625rem",
|
||||
marginBottom: "0.3625rem",
|
||||
marginRight: "0.5rem"
|
||||
}}
|
||||
className={displayTab === 1 ? classes.activeButton : classes.inactiveButton}
|
||||
component="span">
|
||||
<NotesIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<Box margin={2} marginTop={0}>
|
||||
{loading ? (
|
||||
<LinearProgress />
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<TabPanelMine value={displayTab} index={0}>
|
||||
{contractDisplay()}
|
||||
</TabPanelMine>
|
||||
<TabPanelMine value={displayTab} index={1}>
|
||||
<Typography style={{ paddingRight: 20, fontSize: 30, fontWeight: 650 }}>
|
||||
{contract.name()} - Notes
|
||||
</Typography>
|
||||
<Card style={{ padding: 10, marginTop: 15 }}>
|
||||
<Chat objectKey={contract.key()} type={pond.NoteType.NOTE_TYPE_CONTRACT} />
|
||||
</Card>
|
||||
</TabPanelMine>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Box>
|
||||
<GrainTransaction
|
||||
open={openGrainTransactionDialog}
|
||||
mainObject={contract}
|
||||
allowAttachmentUploads
|
||||
restrictMatching
|
||||
asDestination
|
||||
close={() => {
|
||||
setOpenGrainTransactionDialog(false);
|
||||
}}
|
||||
callback={t => {
|
||||
setOpenGrainTransactionDialog(false);
|
||||
if (t) {
|
||||
let transactions = validTransactions;
|
||||
transactions.set(t.key, Transaction.create(t));
|
||||
setValidTransactions(transactions);
|
||||
setTransArray(Array.from(transactions.values()));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Drawer
|
||||
open={openNoteDrawer}
|
||||
onClose={() => {
|
||||
setOpenNoteDrawer(false);
|
||||
}}
|
||||
anchor="right"
|
||||
classes={{ paper: classes.drawerPaper }}>
|
||||
<Box padding={2}>
|
||||
<Typography>Notes</Typography>
|
||||
<Card style={{ padding: 10 }}>
|
||||
<Chat objectKey={contract.key()} type={pond.NoteType.NOTE_TYPE_CONTRACT} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
112
src/pages/Contracts.tsx
Normal file
112
src/pages/Contracts.tsx
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { Box, Button, Grid2 as Grid, LinearProgress, TextField, Typography } from "@mui/material";
|
||||
import ContractSettings from "contract/contractSettings";
|
||||
import ContractsList from "contract/contractList";
|
||||
import { Contract } from "models/Contract";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useContractAPI, useGlobalState } from "providers";
|
||||
// import { useContractAPI } from "providers/pond/contractAPI";
|
||||
import { useEffect, useState } from "react";
|
||||
import PageContainer from "./PageContainer";
|
||||
|
||||
export default function Contracts() {
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
const [{ as }] = useGlobalState()
|
||||
const contractAPI = useContractAPI();
|
||||
const [contracts, setContracts] = useState<Contract[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
//const [{ backgroundTasksComplete }] = useGlobalState();
|
||||
const [contractPermissions, setContractPermissions] = useState<Map<string, pond.Permission[]>>(
|
||||
new Map()
|
||||
);
|
||||
const [year, setYear] = useState(moment().format("YYYY"));
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
setLoading(true);
|
||||
contractAPI
|
||||
.listContractsByYear(
|
||||
0,
|
||||
0,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
year,
|
||||
as
|
||||
)
|
||||
.then(resp => {
|
||||
console.log(resp)
|
||||
let contracts: Contract[] = [];
|
||||
let contractPermissions: Map<string, pond.Permission[]> = new Map();
|
||||
resp.data.contracts.forEach(contract => {
|
||||
let c = Contract.create(contract);
|
||||
contracts.push(c);
|
||||
let p = pond.EvaluatePermissionsResponse.fromObject(
|
||||
resp.data.contractPermissions[c.key()]
|
||||
);
|
||||
contractPermissions.set(c.key(), p.permissions);
|
||||
});
|
||||
setContracts(contracts);
|
||||
setContractPermissions(contractPermissions);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
}, [contractAPI, year]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Box padding={2}>
|
||||
<Typography style={{ fontSize: 25, fontWeight: 650 }}>Contracts</Typography>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignContent="center"
|
||||
alignItems="center">
|
||||
<Grid>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setOpenSettings(true);
|
||||
}}>
|
||||
Add Contract
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
label="Contract Year"
|
||||
value={year}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
onChange={e => setYear(e.target.value)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
{!loading ? (
|
||||
<ContractsList
|
||||
contracts={contracts}
|
||||
contractPermissions={contractPermissions}
|
||||
updateList={c => {
|
||||
setContracts([...c]);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<LinearProgress />
|
||||
)}
|
||||
<ContractSettings
|
||||
open={openSettings}
|
||||
close={() => {
|
||||
setOpenSettings(false);
|
||||
}}
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -42,7 +42,8 @@ export {
|
|||
useMutationAPI, //TODO: update api with resolve,reject
|
||||
useGrainBagAPI,
|
||||
useTransactionAPI,
|
||||
useFileControllerAPI
|
||||
useFileControllerAPI,
|
||||
useContractAPI //TODO: update api with resolve, reject
|
||||
} from "./pond/pond";
|
||||
// export { SecurityContext, useSecurity } from "./security";
|
||||
export { SnackbarContext, useSnackbar } from "./Snackbar";
|
||||
|
|
|
|||
176
src/providers/pond/contractAPI.tsx
Normal file
176
src/providers/pond/contractAPI.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { useHTTP } from "hooks";
|
||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { pondURL } from "./pond";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useGlobalState } from "providers";
|
||||
|
||||
export interface IContractInterface {
|
||||
addContract: (
|
||||
name: string,
|
||||
settings: pond.ContractSettings
|
||||
) => Promise<AxiosResponse<pond.AddContractResponse>>;
|
||||
listContracts: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
numerical?: boolean,
|
||||
specificUser?: string,
|
||||
as?: string,
|
||||
) => Promise<AxiosResponse<pond.ListContractsResponse>>;
|
||||
updateContract: (
|
||||
key: string,
|
||||
name: string,
|
||||
settings: pond.ContractSettings
|
||||
) => Promise<AxiosResponse<pond.UpdateContractResponse>>;
|
||||
removeContract: (key: string) => Promise<AxiosResponse<pond.RemoveContractResponse>>;
|
||||
listContractsByYear: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
numerical?: boolean,
|
||||
specificUser?: string,
|
||||
contractYear?: string,
|
||||
as?: string
|
||||
) => Promise<AxiosResponse<pond.ListContractsByYearResponse>>;
|
||||
//list contract page data
|
||||
getContractPageData: (key: string) => Promise<AxiosResponse<pond.GetContractPageDataResponse>>;
|
||||
//for the moment however simply passing the contract from the earlier list is sufficient
|
||||
}
|
||||
|
||||
export const ContractAPIcontext = createContext<IContractInterface>({} as IContractInterface);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function ContractProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, del, post, put } = useHTTP();
|
||||
const [{ as }] = useGlobalState();
|
||||
|
||||
//add
|
||||
const addContract = (name: string, settings: pond.ContractSettings) => {
|
||||
if (as) {
|
||||
return post<pond.AddContractResponse>(
|
||||
pondURL("/contracts?name=" + name + "&as=" + as),
|
||||
settings
|
||||
);
|
||||
}
|
||||
return post<pond.AddContractResponse>(pondURL("/contracts?name=" + name), settings);
|
||||
};
|
||||
//list
|
||||
const listContracts = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
numerical?: boolean,
|
||||
specificUser?: string
|
||||
) => {
|
||||
let asText = "";
|
||||
if (as) asText = "&as=" + as;
|
||||
if (specificUser) asText = "&as=" + specificUser;
|
||||
return get<pond.ListContractsResponse>(
|
||||
pondURL(
|
||||
"/contracts?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + (order ? order : "asc")) +
|
||||
("&by=" + (orderBy ? orderBy : "key")) +
|
||||
(numerical ? "&numerical=true" : "") +
|
||||
(search ? "&search=" + search : "") +
|
||||
(keys ? "&keys=" + keys.toString() : "") +
|
||||
(types ? "&types=" + types.toString() : "") +
|
||||
asText
|
||||
)
|
||||
);
|
||||
};
|
||||
const listContractsByYear = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
keys?: string[],
|
||||
types?: string[],
|
||||
numerical?: boolean,
|
||||
specificUser?: string,
|
||||
contractYear?: string,
|
||||
as?: string,
|
||||
) => {
|
||||
let asText = "";
|
||||
if (as) asText = "&as=" + as;
|
||||
if (specificUser) asText = "&as=" + specificUser;
|
||||
return get<pond.ListContractsByYearResponse>(
|
||||
pondURL(
|
||||
"/contractsByYear?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + (order ? order : "asc")) +
|
||||
("&by=" + (orderBy ? orderBy : "key")) +
|
||||
(numerical ? "&numerical=true" : "") +
|
||||
(search ? "&search=" + search : "") +
|
||||
(keys ? "&keys=" + keys.toString() : "") +
|
||||
(types ? "&types=" + types.toString() : "") +
|
||||
(contractYear ? "&year=" + contractYear : "") +
|
||||
asText
|
||||
)
|
||||
);
|
||||
};
|
||||
//update
|
||||
const updateContract = (key: string, name: string, settings: pond.ContractSettings) => {
|
||||
if (as) {
|
||||
return put<pond.UpdateContractResponse>(
|
||||
pondURL("/contracts/" + key + "?as=" + as + "&name=" + name),
|
||||
settings
|
||||
);
|
||||
}
|
||||
return put<pond.UpdateContractResponse>(
|
||||
pondURL("/contracts/" + key + "?name=" + name),
|
||||
settings
|
||||
);
|
||||
};
|
||||
//remove
|
||||
const removeContract = (key: string) => {
|
||||
if (as) {
|
||||
return del<pond.RemoveContractResponse>(pondURL("/contracts/" + key + "?as=" + as));
|
||||
}
|
||||
return del<pond.RemoveContractResponse>(pondURL("/contracts/" + key + "?as=" + as));
|
||||
};
|
||||
|
||||
const getContractPageData = (key: string) => {
|
||||
if (as) {
|
||||
return get<pond.GetContractPageDataResponse>(pondURL("/contractsPage/" + key + "?as=" + as));
|
||||
}
|
||||
|
||||
return get<pond.GetContractPageDataResponse>(pondURL("/contractsPage/" + key));
|
||||
};
|
||||
|
||||
return (
|
||||
<ContractAPIcontext.Provider
|
||||
value={{
|
||||
addContract,
|
||||
listContracts,
|
||||
listContractsByYear,
|
||||
updateContract,
|
||||
removeContract,
|
||||
getContractPageData
|
||||
}}>
|
||||
{children}
|
||||
</ContractAPIcontext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useContractAPI = () => useContext(ContractAPIcontext);
|
||||
|
|
@ -31,9 +31,14 @@ export const FileControllerAPIContext = createContext<IFileControllerAPI>({} as
|
|||
export default function FileControllerProvider(props: PropsWithChildren<any>) {
|
||||
const { children } = props;
|
||||
const [{ as }] = useGlobalState();
|
||||
const { get, post, del } = useHTTP();
|
||||
const { get, post, del, options } = useHTTP();
|
||||
|
||||
const uploadFiles = (files: FileList, toAttach?: toAttach, keys?: string[], types?: string[]) => {
|
||||
let headers = {
|
||||
...options().headers,
|
||||
"Content-Type": "multipart/form-data"
|
||||
};
|
||||
let opt = { headers };
|
||||
let formData = new FormData();
|
||||
Array.from(files).forEach((f, i) => {
|
||||
formData.append("file" + i, f);
|
||||
|
|
@ -53,7 +58,7 @@ export default function FileControllerProvider(props: PropsWithChildren<any>) {
|
|||
(types ? "&types=" + types.join(",") : "")
|
||||
);
|
||||
}
|
||||
return post<pond.AddFileResponse>(url, formData);
|
||||
return post<pond.AddFileResponse>(url, formData, opt);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import MutationProvider, {useMutationAPI} from "./mutationAPI"
|
|||
import PreferenceProvider, {usePreferenceAPI} from "./preferenceAPI";
|
||||
import TaskProvider, { useTaskAPI } from "./taskAPI";
|
||||
import DataDogProvider, { useDataDogProxyAPI } from "./datadogProxyAPI";
|
||||
import ContractProvider, { useContractAPI } from "./contractAPI";
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -78,7 +79,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<PreferenceProvider>
|
||||
<TaskProvider>
|
||||
<DataDogProvider>
|
||||
{children}
|
||||
<ContractProvider>
|
||||
{children}
|
||||
</ContractProvider>
|
||||
</DataDogProvider>
|
||||
</TaskProvider>
|
||||
</PreferenceProvider>
|
||||
|
|
@ -143,5 +146,6 @@ export {
|
|||
useMutationAPI,
|
||||
usePreferenceAPI,
|
||||
useTaskAPI,
|
||||
useDataDogProxyAPI
|
||||
useDataDogProxyAPI,
|
||||
useContractAPI
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue