392 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|