added the contract and contracts and all supporting files

This commit is contained in:
csawatzky 2025-04-16 17:06:54 -06:00
parent 07ccb42905
commit 832bb0f7e5
18 changed files with 2898 additions and 17 deletions

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

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

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

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

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

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

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

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

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

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

View file

@ -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 />} />

View file

@ -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,6 +329,7 @@ export default function SideNavigator(props: Props) {
{open && <ListItemText primary="Tasks" />}
</ListItemButton>
</Tooltip>
{(isAg || user.hasFeature("admin")) && (
<Tooltip title="Transactions" placement="right">
<ListItemButton
id="tour-transactions"
@ -340,6 +342,21 @@ export default function SideNavigator(props: Props) {
{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
View 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
View 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>
);
}

View file

@ -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";

View 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);

View file

@ -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);
};
/**

View file

@ -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>
<ContractProvider>
{children}
</ContractProvider>
</DataDogProvider>
</TaskProvider>
</PreferenceProvider>
@ -143,5 +146,6 @@ export {
useMutationAPI,
usePreferenceAPI,
useTaskAPI,
useDataDogProxyAPI
useDataDogProxyAPI,
useContractAPI
};