frontend/src/contract/contractTransactionTable.tsx

392 lines
12 KiB
TypeScript

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, useGlobalState, 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 [{as}] = useGlobalState();
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, as).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={(e)=>{setPageSize(e.target.value)}}
setPage={(page)=>{setTablePage(page)}}
/>
{/* <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>
);
}