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