Merge branch 'hybrid_inventory_control' into dev_environment

This commit is contained in:
csawatzky 2025-06-06 10:59:30 -06:00
commit e09425a465
15 changed files with 729 additions and 38 deletions

2
package-lock.json generated
View file

@ -10864,7 +10864,7 @@
}, },
"node_modules/protobuf-ts": { "node_modules/protobuf-ts": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#c9b5ca47fbd6a5ab23a10fe1101f4fea40d5af9d", "resolved": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#e5ab6369bbfc866e7e8792f94a62f1ca8931e67e",
"dependencies": { "dependencies": {
"protobufjs": "^6.8.8" "protobufjs": "^6.8.8"
} }

View file

@ -183,6 +183,7 @@ export default function BinSettings(props: Props) {
const [inMoistureString, setInMoistureString] = useState("0"); const [inMoistureString, setInMoistureString] = useState("0");
const [tMoistureString, setTMoistureString] = useState("0"); const [tMoistureString, setTMoistureString] = useState("0");
const [moistureTargetDeviation, setMoistureTargetDeviation] = useState("0"); const [moistureTargetDeviation, setMoistureTargetDeviation] = useState("0");
const [autoFillThreshold, setAutoFillThreshold] = useState(0);
const [validInitial, setValidInitial] = useState(true); const [validInitial, setValidInitial] = useState(true);
const [validTarget, setValidTarget] = useState(true); const [validTarget, setValidTarget] = useState(true);
const [validDeviation, setValidDeviation] = useState(true); const [validDeviation, setValidDeviation] = useState(true);
@ -311,6 +312,7 @@ export default function BinSettings(props: Props) {
setInMoistureString(initForm.inventory?.initialMoisture.toString() ?? "0"); setInMoistureString(initForm.inventory?.initialMoisture.toString() ?? "0");
setTMoistureString(initForm.inventory?.targetMoisture.toString() ?? "0"); setTMoistureString(initForm.inventory?.targetMoisture.toString() ?? "0");
setMoistureTargetDeviation(initForm.inventory?.moistureTargetDeviation.toString() ?? "0"); setMoistureTargetDeviation(initForm.inventory?.moistureTargetDeviation.toString() ?? "0");
setAutoFillThreshold(initForm.inventory?.autoThreshold ?? 0)
setActiveStep(0); setActiveStep(0);
if (bin) { if (bin) {
setAutoTopNode(bin.settings.autoGrainNode); setAutoTopNode(bin.settings.autoGrainNode);
@ -405,7 +407,10 @@ export default function BinSettings(props: Props) {
form.highTemp = highTempC; form.highTemp = highTempC;
form.lowTemp = lowTempC; form.lowTemp = lowTempC;
form.autoGrainNode = autoTopNode; form.autoGrainNode = autoTopNode;
if (form.inventory) form.inventory.inventoryControl = inventoryControl; if (form.inventory) {
form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold
}
binAPI binAPI
.addBin(form, as) .addBin(form, as)
@ -449,7 +454,10 @@ export default function BinSettings(props: Props) {
form.highTemp = highTempC; form.highTemp = highTempC;
form.lowTemp = lowTempC; form.lowTemp = lowTempC;
form.autoGrainNode = autoTopNode; form.autoGrainNode = autoTopNode;
if (form.inventory) form.inventory.inventoryControl = inventoryControl; if (form.inventory) {
form.inventory.inventoryControl = inventoryControl
form.inventory.autoThreshold = autoFillThreshold
}
binAPI binAPI
.updateBin(bin.key(), form, as) .updateBin(bin.key(), form, as)
.then(response => { .then(response => {
@ -699,11 +707,16 @@ export default function BinSettings(props: Props) {
}; };
const inventoryControlLabel = (control: pond.BinInventoryControl) => { const inventoryControlLabel = (control: pond.BinInventoryControl) => {
let string = "Manual"; switch(control){
if (control === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC) { case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR:
string = "Auto"; return "Auto (lidar)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC:
return "Auto (cable)"
case pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR:
return "Hybrid (lidar)"
default:
return "Manual"
} }
return string;
}; };
const inventoryForm = () => { const inventoryForm = () => {
@ -819,6 +832,7 @@ export default function BinSettings(props: Props) {
<Accordion className={classes.bottomSpacing}> <Accordion className={classes.bottomSpacing}>
<AccordionSummary expandIcon={<ExpandMore />}> <AccordionSummary expandIcon={<ExpandMore />}>
<Grid <Grid
width={"100%"}
container container
direction="row" direction="row"
alignContent="center" alignContent="center"
@ -854,6 +868,11 @@ export default function BinSettings(props: Props) {
control={<Radio />} control={<Radio />}
label={"Auto (Lidar)"} label={"Auto (Lidar)"}
/> />
<FormControlLabel
value={pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR}
control={<Radio />}
label={"Hybrid (Lidar)"}
/>
</RadioGroup> </RadioGroup>
</AccordionDetails> </AccordionDetails>
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC && {inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC &&
@ -864,6 +883,36 @@ export default function BinSettings(props: Props) {
</Typography> </Typography>
</Box> </Box>
)} )}
{inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR && (
<Box marginX={2} marginBottom={2}>
<Typography variant="caption">
Hybrid control will submit a transaction that must be approved by the user before inventory is changed
</Typography>
</Box>
)}
{(inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_AUTOMATIC_LIDAR ||
inventoryControl === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR) &&
<Box width="100%" padding={2}>
<TextField
fullWidth
variant="outlined"
label="Auto Threshold Percent"
helperText="Fill percent threshold that must be crossed to affect inventory"
InputProps={{
endAdornment: (
<InputAdornment position="end">
%
</InputAdornment>
)
}}
type="number"
value={autoFillThreshold}
onChange={e => {
setAutoFillThreshold(+e.target.value)
}}
/>
</Box>
}
</Accordion> </Accordion>
{empty ? ( {empty ? (
<Box marginTop={3} display="flex" flexDirection="column" alignItems="center"> <Box marginTop={3} display="flex" flexDirection="column" alignItems="center">
@ -1787,8 +1836,9 @@ export default function BinSettings(props: Props) {
</Box> </Box>
<Box marginTop={2}> <Box marginTop={2}>
<FormControl component="fieldset" fullWidth disabled={!canEdit}> <FormControl component="fieldset" variant="outlined" fullWidth disabled={!canEdit}>
<OutlinedBox label={<FormLabel component="legend">Shape</FormLabel>} padding={1}> {/* <OutlinedBox label={<FormLabel component="legend">Shape</FormLabel>} padding={1}> */}
<FormLabel component="legend">Shape</FormLabel>
<RadioGroup <RadioGroup
aria-label="bin shape" aria-label="bin shape"
name="binShape" name="binShape"
@ -1841,7 +1891,7 @@ export default function BinSettings(props: Props) {
/> />
))} ))}
</RadioGroup> </RadioGroup>
</OutlinedBox> {/* </OutlinedBox> */}
<FormHelperText>Aeration effectiveness varies with bin shape</FormHelperText> <FormHelperText>Aeration effectiveness varies with bin shape</FormHelperText>
</FormControl> </FormControl>
</Box> </Box>

520
src/bin/BinTransactions.tsx Normal file
View file

@ -0,0 +1,520 @@
import { CheckOutlined } from "@mui/icons-material";
import { Autocomplete, Box, Button, Card, DialogActions, DialogContent, DialogTitle, TextField, Typography } from "@mui/material";
import ResponsiveDialog from "common/ResponsiveDialog";
import ResponsiveTable from "common/ResponsiveTable";
import { Option } from "common/SearchSelect";
import GrainDescriber from "grain/GrainDescriber";
import { cloneDeep } from "lodash";
import { Bin, Contract, Field, GrainBag } from "models";
import { Transaction } from "models/Transaction";
import moment from "moment";
import ObjectDescriber from "objects/ObjectDescriber";
import { pond } from "protobuf-ts/pond";
import { useBinAPI, useContractAPI, useFieldAPI, useGrainBagAPI, useSnackbar, useTransactionAPI } from "providers";
import React from "react";
import { useCallback, useEffect, useState } from "react";
import TransactionDataDisplay from "transactions/transactionDataDisplay";
import { getGrainUnit } from "utils";
interface Props {
bin: Bin
permissions: pond.Permission[]
refresh: () => void
}
export default function BinTransactions(props: Props){
const { bin, permissions, refresh } = props
const [state, setState] = useState<pond.TransactionState>(pond.TransactionState.TRANSACTION_STATE_APPROVAL_REQUIRED)
const transactionAPI = useTransactionAPI()
const binAPI = useBinAPI()
const grainBagAPI = useGrainBagAPI()
const fieldAPI = useFieldAPI()
const contractAPI = useContractAPI()
const [transactions, setTransactions] = useState<Transaction[]>([])
const [possibleTransactionMatches, setPossibleTransactionMatches] = useState<Transaction[]>([])
const [tablePage, setTablePage] = useState(0)
const [tablePageSize, setTablePageSize] = useState(10)
const { openSnack } = useSnackbar();
const [total, setTotal] = useState(0)
const [grainMessage, setGrainMessage] = useState("")
//only need to do this for the bins since if a bin is picked for the secondary we need to check its inventory control type,
//if it is also hybrid a matching transaction must be selected
const [bins, setBins] = useState<Map<string, Bin>>(new Map())
const [binOptions, setBinOptions] = useState<Option[]>([])
const [grainBagOptions, setGrainBagOptions] = useState<Option[]>([])
const [fieldOptions, setFieldOptions] = useState<Option[]>([])
const [contractOptions, setContractOptions] = useState<Option[]>([])
const [openDialog, setOpenDialog] = useState(false)
const [objectNames, setObjectNames] = useState(new Map<string, string>())
const [selectedTransaction, setSelectedTransaction] = useState<Transaction>()
const [selectedMatch, setselectedMatch] = useState<Transaction>()
const [fromType, setFromType] = useState<pond.ObjectType>(pond.ObjectType.OBJECT_TYPE_UNKNOWN)
const [fromOption, setFromOption] = useState<Option | undefined>()
const [toType, setToType] = useState<pond.ObjectType>(pond.ObjectType.OBJECT_TYPE_UNKNOWN)
const [toOption, setToOption] = useState<Option | undefined>()
const [requiresMatch, setRequiresMatch] = useState(false)
//load bins
const loadBins = ()=>{
//limit of 0 will load them all but this load will only happen once regardless of how many times the dropdown for type is changed
binAPI.listBins(0, 0, "asc", "name").then(resp => {
let options: Option[] = []
let binMap: Map<string, Bin> = new Map()
resp.data.bins.map(b => {
let bin = Bin.create(b)
if(bin.key() !== props.bin.key()){
options.push({label: bin.name(), value: bin.key(), group: bin.grainName()})
binMap.set(bin.key(), bin)
}
})
setBinOptions(options)
setBins(binMap)
})
}
//load grainbags
const loadGrainbags = ()=>{
grainBagAPI.listGrainBags(0, 0, "asc", "name").then(resp => {
setGrainBagOptions(resp.data.grainBags.map(g => {
let grainBag = GrainBag.create(g)
return {label: grainBag.name(), value: grainBag.key(), group: grainBag.grainName()}
}))
})
}
//load fields
const loadFields = useCallback(()=>{
fieldAPI.listFields(0,0, "asc", "field_name").then(resp => {
setFieldOptions(resp.data.fields.map(f => {
let field = Field.create(f)
return {label: field.name(), value: field.key(), group: field.grainName()}
}))
})
},[])
//load contracts
const loadContracts = useCallback(()=>{
contractAPI.listContracts(0,0, "asc", "name").then(resp => {
setContractOptions(resp.data.contracts.map(c => {
let contract = Contract.create(c)
return {label: contract.name(), value: contract.key(), group: contract.grainName()}
}))
})
},[])
const loadTransactionsFor = useCallback(()=>{
// load transactions for bin with matching state (pending is default)
transactionAPI.listTransactionsFor(tablePageSize, tablePage * tablePageSize, "asc", bin.key(), state).then(resp => {
setTotal(resp.data.total)
// console.log(resp.data)
setTransactions(resp.data.transactions.map(t => Transaction.create(t)))
//TODO: for now just use the bin for the name map, once a backend method of getting the names is implemented the response can be used to build the map
let nameMap = new Map<string, string>()
nameMap.set(bin.key(), bin.name())
setObjectNames(nameMap)
})
},[state, bin, tablePage, tablePageSize])
useEffect(()=>{
loadTransactionsFor()
},[state, bin, loadTransactionsFor])
const closeDialog = () => {
setPossibleTransactionMatches([])
setselectedMatch(undefined)
setRequiresMatch(false)
setOpenDialog(false)
}
const submit = () => {
let transaction = cloneDeep(selectedTransaction)
if(transaction){
//use the dropdowns to update the from/to for the transaction, meaning if the user added another object it will adjust its inventory as well
transaction.transaction.fromObject = fromType
transaction.transaction.fromKey = fromOption?.value ?? ""
transaction.transaction.toObject = toType
transaction.transaction.toKey = toOption?.value ?? ""
//need to do a check to make sure the transaction data contains a grain transaction
if(transaction.transaction.transaction && transaction.transaction.transaction.grainTransaction) {
transaction.transaction.transaction.grainTransaction.message = grainMessage
}
//submit approval
transactionAPI.submitApproval(transaction.transaction.key, true, transaction.transaction).then(resp => {
//in the response if it succeeded remove (not reject but fully delete it) the matched transaction if one was selected
if(selectedMatch){
transactionAPI.removeTransaction(selectedMatch.transaction.key).then(resp => {
openSnack("Updated and matched transactions")
}).catch(err => {
openSnack("Updated primary transaction but failed to remove matched transaction")
})
}else{
openSnack("Updated Transaction")
}
}).catch(err => {
openSnack("There was a problem submitting transaction approval")
})
refresh()
}
}
const reject = () => {
//reject the transaction
if(selectedTransaction){
transactionAPI.submitApproval(selectedTransaction.transaction.key, false, selectedTransaction.transaction).then(resp => {
openSnack("Transaction rejected")
}).catch(err => {
openSnack("There was a problem rejecting the transaction")
})
}
refresh()
}
const getOptions = (type: pond.ObjectType) => {
switch(type){
case pond.ObjectType.OBJECT_TYPE_BIN:
return binOptions
case pond.ObjectType.OBJECT_TYPE_GRAIN_BAG:
return grainBagOptions
case pond.ObjectType.OBJECT_TYPE_FIELD:
return fieldOptions
case pond.ObjectType.OBJECT_TYPE_CONTRACT:
return contractOptions
default:
return []
}
}
// dialog for approving a transaction
const approvalDialog = () => {
// get the display information for the transaction
const grainTransaction = selectedTransaction?.transaction.transaction?.grainTransaction ?? pond.GrainTransaction.create()
const bushels = grainTransaction.bushels ?? 0
const weight = bushels / grainTransaction.bushelsPerTonne
let graintype = grainTransaction.grainType ?? pond.Grain.GRAIN_INVALID
const grainDisplay = graintype === pond.Grain.GRAIN_CUSTOM
? grainTransaction.customGrain
: GrainDescriber(grainTransaction.grainType).name
//determine which location (from/to) that can be edited
let fromDisabled = selectedTransaction?.transaction.fromKey !== ""
let toDisabled = selectedTransaction?.transaction.toKey !== ""
return (
<ResponsiveDialog open={openDialog} onClose={closeDialog}>
<DialogTitle>Accept Transaction</DialogTitle>
<DialogContent>
<Box>
<Typography>{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? weight + " mT" : bushels + " bushels"} of {grainTransaction.subtype} {grainDisplay} moved</Typography>
<Typography margin={1} fontWeight={650}>From:</Typography>
{/*Object type selection*/}
<Autocomplete
sx={{marginY: 1}}
disabled={fromDisabled}
id="autoFromTypes"
value={fromType}
options={[
pond.ObjectType.OBJECT_TYPE_BIN,
pond.ObjectType.OBJECT_TYPE_GRAIN_BAG,
pond.ObjectType.OBJECT_TYPE_FIELD
]}
getOptionLabel={(option: pond.ObjectType) => ObjectDescriber(option).name}
fullWidth
onChange={(_, val) => {
setFromType(val ?? pond.ObjectType.OBJECT_TYPE_UNKNOWN)
//based on what it is changed to need to load data if it is not already loaded
if(val === pond.ObjectType.OBJECT_TYPE_BIN){
//if there is data already the initial load already happened
if(binOptions.length === 0){
//do the initial load of options
loadBins()
}
}else if (val === pond.ObjectType.OBJECT_TYPE_GRAIN_BAG){
if(grainBagOptions.length === 0){
loadGrainbags()
}
}else if (val === pond.ObjectType.OBJECT_TYPE_FIELD){
if (fieldOptions.length === 0){
loadFields()
}
}
}}
renderInput={params => <TextField {...params} label="Type" />}
/>
{/* object selection */}
<Autocomplete
sx={{marginY: 1}}
disabled={fromDisabled}
id="autoFromObjects"
value={fromOption}
options={getOptions(fromType)}
getOptionLabel={(option: Option) => option.label}
fullWidth
onChange={(_, val) => {
setFromOption(val ?? undefined)
//once one is selected get the transactions for it that are requiring approval
transactionAPI.listTransactionsFor(0, 0, "asc", val?.value, pond.TransactionState.TRANSACTION_STATE_APPROVAL_REQUIRED).then(resp => {
let matches: Transaction[] = []
resp.data.transactions.forEach(transaction => {
//the transaction could be a match if the from is the object you selected and the to is unknown
if(transaction.fromKey === val?.value && transaction.toKey === ""){
matches.push(Transaction.create(transaction))
}
})
setPossibleTransactionMatches(matches)
})
//if the options are bin types check if the selected bin uses hybrid controls
if(fromType === pond.ObjectType.OBJECT_TYPE_BIN && val){
let bin = bins.get(val.value)
if(bin){
setRequiresMatch(
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_CABLE)
}
}
}}
renderInput={params => <TextField {...params} label="From" />}
/>
<Typography margin={1} fontWeight={650}>To:</Typography>
<Autocomplete
sx={{marginY: 1}}
disabled={toDisabled}
id="autoFromTypes"
value={toType}
options={[
pond.ObjectType.OBJECT_TYPE_BIN,
pond.ObjectType.OBJECT_TYPE_GRAIN_BAG,
pond.ObjectType.OBJECT_TYPE_CONTRACT
]}
getOptionLabel={(option: pond.ObjectType) => ObjectDescriber(option).name}
fullWidth
onChange={(_, val) => {
setToType(val ?? pond.ObjectType.OBJECT_TYPE_UNKNOWN)
//based on what it is changed to need to load data if it is not already loaded
if(val === pond.ObjectType.OBJECT_TYPE_BIN){
//if there is data already the initial load already happened
if(binOptions.length === 0){
//do the initial load of options
loadBins()
}
}else if (val === pond.ObjectType.OBJECT_TYPE_GRAIN_BAG){
if(grainBagOptions.length === 0){
loadGrainbags()
}
}else if (val === pond.ObjectType.OBJECT_TYPE_CONTRACT){
if(contractOptions.length === 0){
loadContracts()
}
}
}}
renderInput={params => <TextField {...params} label="Type" />}
/>
<Autocomplete
sx={{marginY: 1}}
disabled={toDisabled}
groupBy={option => {
if (option.group) {
return option.group;
}
return "No Grain Type";
}}
id="autoFromObjects"
value={toOption}
options={getOptions(toType)}
getOptionLabel={(option: Option) => option.label}
fullWidth
onChange={(_, val) => {
setToOption(val ?? undefined)
//once one is selected get the transactions for it that are requiring approval
transactionAPI.listTransactionsFor(0, 0, "asc", val?.value, pond.TransactionState.TRANSACTION_STATE_APPROVAL_REQUIRED).then(resp => {
let matches: Transaction[] = []
resp.data.transactions.forEach(transaction => {
//the transaction could be a match if the to is the object selected and the from is unknown
if(transaction.toKey === val?.value && transaction.fromKey === ""){
matches.push(Transaction.create(transaction))
}
})
setPossibleTransactionMatches(matches)
})
//if the options are bin types check if the selected bin uses hybrid controls
if(fromType === pond.ObjectType.OBJECT_TYPE_BIN && val){
let bin = bins.get(val.value)
if(bin){
setRequiresMatch(
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_LIDAR ||
bin.inventoryControl() === pond.BinInventoryControl.BIN_INVENTORY_CONTROL_HYBRID_CABLE)
}
}
}}
renderInput={params => <TextField {...params} label="From" />}
/>
</Box>
<Box>
<TextField
label="Message(optional)"
margin="dense"
fullWidth
variant="outlined"
value={grainMessage}
onChange={e => setGrainMessage(e.target.value)}
/>
</Box>
{toOption &&
<Box paddingTop={1}>
<Typography variant="h6">Possible matching Transactions</Typography>
<Typography>{possibleTransactionMatches.length > 0 ? "Pick a transaction below if you want to match them together" : "No matching transactions found"}</Typography>
{possibleTransactionMatches.map(t => {
// get the display information for the transaction
const grainTransaction = t.transaction.transaction?.grainTransaction ?? pond.GrainTransaction.create()
const bushels = grainTransaction.bushels ?? 0
const weight = bushels / grainTransaction.bushelsPerTonne
let graintype = grainTransaction.grainType ?? pond.Grain.GRAIN_INVALID
const grainDisplay = graintype === pond.Grain.GRAIN_CUSTOM
? grainTransaction.customGrain
: GrainDescriber(grainTransaction.grainType).name
return (
<Card sx={{marginTop: 1, padding: 1}} key={t.transaction.key}>
<Box display="flex" justifyContent="space-between" height={40}>
<Typography sx={{fontWeight: 650, margin: "auto", marginLeft: 0}}>
{moment(t.transaction.timestamp).fromNow()}
</Typography>
<Box>
{selectedMatch?.transaction.key === t.transaction.key && <CheckOutlined htmlColor="green" />}
</Box>
</Box>
<Box display="flex" justifyContent="space-between">
<Typography sx={{margin: "auto", marginLeft: 0}}>{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? weight + " mT" : bushels + " bushels"} of {grainTransaction.subtype} {grainDisplay} moved</Typography>
<Button
variant="contained"
disabled={selectedMatch?.transaction.key === t.transaction.key}
color="primary"
onClick={()=>{
setselectedMatch(t)
}}>
Set Match
</Button>
</Box>
</Card>
)
})}
</Box>
}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={closeDialog}>Close</Button>
<Button
variant="contained"
color="error"
onClick={() => {
reject()
closeDialog()
}}>
Reject
</Button>
<Button
variant="contained"
disabled={requiresMatch && selectedMatch === undefined}
color="primary"
onClick={() => {
submit()
closeDialog()
//re-load bin
}}>
Approve
</Button>
</DialogActions>
</ResponsiveDialog>
)
}
return (
<React.Fragment>
{approvalDialog()}
<Card>
{/* dropdown for transaction state
--FUTURE THING--
when we can implement a better way to get the current names of objects involved
other than loading everything up front
*/}
{/* table to display transactions */}
<ResponsiveTable<Transaction>
columns={[
{
title: "Date",
render: row => {
return (
<Box padding={2}>
<Typography>{moment(row.transaction.timestamp).format("YYYY-MM-DD")}</Typography>
</Box>
);
}
},
{
title: "From",
render: row => {
return (
<Box padding={2}>
<Typography>{ObjectDescriber(row.transaction.fromObject).name}</Typography>
<Typography>
{objectNames ? objectNames.get(row.transaction.fromKey) : row.transaction.fromKey}
</Typography>
</Box>
);
}
},
{
title: "To",
render: row => {
return (
<Box padding={2}>
<Typography>{ObjectDescriber(row.transaction.toObject).name}</Typography>
<Typography>
{objectNames ? objectNames.get(row.transaction.toKey) : row.transaction.toKey}
</Typography>
</Box>
);
}
},
{
title: "Transaction",
render: row => {
return (
<Box padding={2}>
<TransactionDataDisplay transaction={row} />
</Box>
);
}
}
]}
title="Pending Transactions"
subtitle={"Select a transaction to approve"}
noDataMessage="No Pending Transactions Found"
onRowClick={permissions.includes(pond.Permission.PERMISSION_WRITE) ? (row)=> {
setSelectedTransaction(row)
setFromType(row.transaction.fromObject)
setFromOption({label: objectNames.get(row.transaction.fromKey) ?? "", value: row.transaction.fromKey})
setToType(row.transaction.toObject)
setToOption({label: objectNames.get(row.transaction.toKey) ?? "", value: row.transaction.toKey})
setOpenDialog(true)
} : undefined}
rows={transactions}
handleRowsPerPageChange={(e)=>{setTablePageSize(e.target.value)}}
setPage={(page)=>{setTablePage(page)}}
page={tablePage}
pageSize={tablePageSize}
total={total}
/>
</Card>
</React.Fragment>
)
}

View file

@ -285,7 +285,9 @@ export default function BinGraphs(props: Props) {
<Box> <Box>
<BinWaterLevel <BinWaterLevel
bin={bin} bin={bin}
range={{ start: startDate, end: endDate }} // range={{ start: startDate, end: endDate }}
start={startDate.toISOString()}
end={endDate.toISOString()}
plenumKey={componentDevices.get(plenums[0].key()) + ":" + plenums[0].key()} plenumKey={componentDevices.get(plenums[0].key()) + ":" + plenums[0].key()}
cableKey={componentDevices.get(moistureCables[0].key()) + ":" + moistureCables[0].key()} cableKey={componentDevices.get(moistureCables[0].key()) + ":" + moistureCables[0].key()}
pressureKey={componentDevices.get(pressures[0].key()) + ":" + pressures[0].key()} pressureKey={componentDevices.get(pressures[0].key()) + ":" + pressures[0].key()}

View file

@ -9,7 +9,9 @@ import React, { useEffect, useState } from "react";
interface Props { interface Props {
bin: Bin; bin: Bin;
range: DateRange; // range: DateRange;
start: string,
end: string,
plenumKey: string; plenumKey: string;
cableKey: string; cableKey: string;
pressureKey: string; pressureKey: string;
@ -23,7 +25,9 @@ interface Props {
export default function BinWaterLevel(props: Props) { export default function BinWaterLevel(props: Props) {
const { const {
bin, bin,
range, //range,
start,
end,
plenumKey, plenumKey,
cableKey, cableKey,
pressureKey, pressureKey,
@ -63,8 +67,8 @@ export default function BinWaterLevel(props: Props) {
binAPI binAPI
.listBinLiters( .listBinLiters(
bin.key(), bin.key(),
range.start.toISOString(), start,
range.end.toISOString(), end,
grainMoisture, grainMoisture,
//(note: the variable needs to be the device the component belongs to and then the component key ie. "10:LTe7Q1sT5g65") //(note: the variable needs to be the device the component belongs to and then the component key ie. "10:LTe7Q1sT5g65")
plenumKey, plenumKey,
@ -94,7 +98,7 @@ export default function BinWaterLevel(props: Props) {
}); });
} }
} }
}, [binAPI, range, bin, grainMoisture, plenumKey, cableKey, pressureKey, as]); // eslint-disable-line react-hooks/exhaustive-deps }, [binAPI, start, end, bin, grainMoisture, plenumKey, cableKey, pressureKey, as]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<React.Fragment> <React.Fragment>

View file

@ -184,16 +184,18 @@ export default function CableEstimator() {
if(tableSearch !== ""){ if(tableSearch !== ""){
//filter the data in the cloned list //filter the data in the cloned list
clone = clone.filter(bin => { clone = clone.filter(bin => {
if(bin.Manufacturer.toLowerCase().includes(tableSearch.toLowerCase())) return true //doing a partial search so if any string contains the search string it will return true
if(bin.Model.toLowerCase().includes(tableSearch.toLowerCase())) return true const lowerSearch = tableSearch.toLowerCase()
if(bin.Capacity.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Manufacturer.toLocaleLowerCase().includes(lowerSearch)) return true
if(bin.HopperAngle.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Model.toLowerCase().includes(lowerSearch)) return true
if(bin.Diameter.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Capacity.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Sidewall.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.HopperAngle.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Peak.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Diameter.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.EaveToPeak.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Sidewall.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.RoofAngle.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.Peak.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Rings.toString().toLowerCase().includes(tableSearch.toLowerCase())) return true if(bin.EaveToPeak.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.RoofAngle.toString().toLowerCase().includes(lowerSearch)) return true
if(bin.Rings.toString().toLowerCase().includes(lowerSearch)) return true
return false return false
}) })
} }

View file

@ -22,7 +22,7 @@ import GrainDescriber from "grain/GrainDescriber";
import { Contract } from "models/Contract"; import { Contract } from "models/Contract";
import moment from "moment"; import moment from "moment";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { useGlobalState, useTaskAPI, useContractAPI } from "providers"; import { useGlobalState, useTaskAPI, useContractAPI, useSnackbar } from "providers";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { getGrainUnit } from "utils"; import { getGrainUnit } from "utils";
@ -63,6 +63,7 @@ export default function ContractSettings(props: Props) {
const [deleteWindow, setDeleteWindow] = useState(false); const [deleteWindow, setDeleteWindow] = useState(false);
const [createDeliveryTask, setCreateDeliveryTask] = useState(false); const [createDeliveryTask, setCreateDeliveryTask] = useState(false);
const [conversionValue, setConversionValue] = useState("1"); const [conversionValue, setConversionValue] = useState("1");
const { openSnack } = useSnackbar()
const [labels, setLabels] = useState<InputLabels>({ const [labels, setLabels] = useState<InputLabels>({
sizeLabel: "Size", sizeLabel: "Size",
commodityLabel: "Commodity", commodityLabel: "Commodity",
@ -174,11 +175,14 @@ export default function ContractSettings(props: Props) {
contractAPI contractAPI
.updateContract(contract.key(), name, settings, as) .updateContract(contract.key(), name, settings, as)
.then(resp => { .then(resp => {
console.log("Updated Contract"); //console.log("Updated Contract");
openSnack("Contract Updated")
close(); close();
}) })
.catch(err => { .catch(err => {
console.log("Error updating contract"); //console.log("Error updating contract");
openSnack("Error Updating Contract")
}); });
} else { } else {
let settings: pond.ContractSettings = pond.ContractSettings.create(); let settings: pond.ContractSettings = pond.ContractSettings.create();

View file

@ -155,7 +155,7 @@ export class Bin {
return GrainDescriber(this.grain()).name; return GrainDescriber(this.grain()).name;
} }
} else { } else {
return ""; return "None";
} }
} }

View file

@ -234,4 +234,16 @@ export class Contract {
} }
return storedunitVal; return storedunitVal;
} }
public grainName(): string {
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
return this.customType();
} else {
return GrainDescriber(this.grain()).name;
}
} else {
return "None";
}
}
} }

View file

@ -4,6 +4,7 @@ import { or } from "utils/types";
import area from "@turf/area"; import area from "@turf/area";
import { Feature } from "geojson"; import { Feature } from "geojson";
import { GeometryMapping } from "./GeometryMapping"; import { GeometryMapping } from "./GeometryMapping";
import GrainDescriber from "grain/GrainDescriber";
export class Field { export class Field {
public settings: pond.FieldSettings = pond.FieldSettings.create(); public settings: pond.FieldSettings = pond.FieldSettings.create();
@ -150,4 +151,16 @@ export class Field {
} }
return coords; return coords;
} }
public grainName(): string {
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
return this.customType();
} else {
return GrainDescriber(this.grain()).name;
}
} else {
return "None";
}
}
} }

View file

@ -1,3 +1,4 @@
import GrainDescriber from "grain/GrainDescriber";
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { pond } from "protobuf-ts/pond"; import { pond } from "protobuf-ts/pond";
import { or } from "utils/types"; import { or } from "utils/types";
@ -132,4 +133,16 @@ export class GrainBag {
} }
return bpt; return bpt;
} }
public grainName(): string {
if (this.grain() !== pond.Grain.GRAIN_INVALID) {
if (this.grain() === pond.Grain.GRAIN_CUSTOM) {
return this.customType();
} else {
return GrainDescriber(this.grain()).name;
}
} else {
return "None";
}
}
} }

View file

@ -24,8 +24,7 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
drawer: { drawer: {
width: drawerWidth, width: drawerWidth,
flexShrink: 0, zIndex: "10 !important"
zIndex: 10
}, },
drawerPaper: { drawerPaper: {
width: drawerWidth width: drawerWidth

View file

@ -67,6 +67,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { Controller } from "models/Controller"; import { Controller } from "models/Controller";
import TaskViewer from "tasks/TaskViewer"; import TaskViewer from "tasks/TaskViewer";
import ButtonGroup from "common/ButtonGroup"; import ButtonGroup from "common/ButtonGroup";
import BinTransactions from "bin/BinTransactions";
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -195,7 +196,7 @@ export default function Bin(props: Props) {
const [heaters, setHeaters] = useState<Controller[]>([]); const [heaters, setHeaters] = useState<Controller[]>([]);
const [fans, setFans] = useState<Controller[]>([]); const [fans, setFans] = useState<Controller[]>([]);
const [detail, setDetail] = useState< const [detail, setDetail] = useState<
"inventory" | "sensors" | "analytics" | "presets" | "alerts" "inventory" | "sensors" | "analytics" | "presets" | "alerts" | "transactions"
>("inventory"); >("inventory");
const [mobileTab, setMobileTab] = useState(0); const [mobileTab, setMobileTab] = useState(0);
const componentAPI = useComponentAPI(); const componentAPI = useComponentAPI();
@ -886,6 +887,10 @@ export default function Bin(props: Props) {
{ {
title: "Presets", title: "Presets",
function: () => setDetail("presets") function: () => setDetail("presets")
},
{
title: "Transactions",
function: () => setDetail("transactions")
} }
]} ]}
/> />
@ -963,6 +968,9 @@ export default function Bin(props: Props) {
compositionNameMap={compositionNameMap} compositionNameMap={compositionNameMap}
/> />
)} )}
{detail === "transactions" &&
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
}
</Grid> </Grid>
</Grid> </Grid>
</Card> </Card>
@ -992,8 +1000,9 @@ export default function Bin(props: Props) {
<Tab label="Inventory" style={{ fontWeight: mobileTab === 1 ? 650 : 500 }} /> <Tab label="Inventory" style={{ fontWeight: mobileTab === 1 ? 650 : 500 }} />
<Tab label="Sensors" style={{ fontWeight: mobileTab === 2 ? 650 : 500 }} /> <Tab label="Sensors" style={{ fontWeight: mobileTab === 2 ? 650 : 500 }} />
<Tab label="Analysis" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} /> <Tab label="Analysis" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} />
<Tab label="Alerts" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} /> <Tab label="Alerts" style={{ fontWeight: mobileTab === 4 ? 650 : 500 }} />
<Tab label="Presets" style={{ fontWeight: mobileTab === 3 ? 650 : 500 }} /> <Tab label="Presets" style={{ fontWeight: mobileTab === 5 ? 650 : 500 }} />
<Tab label="Transactions" style={{ fontWeight: mobileTab === 6 ? 650 : 500 }} />
</Tabs> </Tabs>
<TabPanelMine value={mobileTab} index={0}> <TabPanelMine value={mobileTab} index={0}>
{overview()} {overview()}
@ -1080,6 +1089,9 @@ export default function Bin(props: Props) {
}} }}
/> />
</TabPanelMine> </TabPanelMine>
<TabPanelMine value={mobileTab} index={6}>
<BinTransactions bin={bin} permissions={permissions} refresh={refresh}/>
</TabPanelMine>
</Card> </Card>
); );
}; };

View file

@ -38,7 +38,6 @@ export default function Contracts() {
as as
) )
.then(resp => { .then(resp => {
console.log(resp)
let contracts: Contract[] = []; let contracts: Contract[] = [];
let contractPermissions: Map<string, pond.Permission[]> = new Map(); let contractPermissions: Map<string, pond.Permission[]> = new Map();
resp.data.contracts.forEach(contract => { resp.data.contracts.forEach(contract => {

View file

@ -4,6 +4,7 @@ import { pond } from "protobuf-ts/pond";
import { pondURL } from "./pond"; import { pondURL } from "./pond";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { useGlobalState } from "providers"; import { useGlobalState } from "providers";
import { or } from "utils";
export interface ITransactionAPIContext { export interface ITransactionAPIContext {
addTransaction: ( addTransaction: (
@ -25,12 +26,22 @@ export interface ITransactionAPIContext {
toKey?: string, toKey?: string,
otherTeam?: string otherTeam?: string
) => Promise<AxiosResponse<pond.ListTransactionsResponse>>; ) => Promise<AxiosResponse<pond.ListTransactionsResponse>>;
listTransactionsFor: (
limit: number,
offset: number,
order: "asc" | "desc",
objectKey: string,
state?: pond.TransactionState,
otherTeam?: string
) => Promise<AxiosResponse<pond.ListTransactionsForResponse>>
revokeTransaction: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RevokeTransactionResponse>>; revokeTransaction: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RevokeTransactionResponse>>;
updateTransaction: ( updateTransaction: (
key: string, key: string,
data: pond.TransactionData, data: pond.TransactionData,
otherTeam?: string otherTeam?: string
) => Promise<AxiosResponse<pond.UpdateTransactionResponse>>; ) => Promise<AxiosResponse<pond.UpdateTransactionResponse>>;
submitApproval: (key: string, approved: boolean, updatedData: pond.Transaction, otherTeam?: string) => Promise<AxiosResponse<pond.SubmitTransactionApprovalResponse>>;
removeTransaction: (key: string, otherTeam?: string) => Promise<AxiosResponse<pond.RemoveTransactionResponse>>
} }
export const TransactionAPIContext = createContext<ITransactionAPIContext>( export const TransactionAPIContext = createContext<ITransactionAPIContext>(
@ -41,7 +52,7 @@ interface Props {}
export default function TransactionProvider(props: PropsWithChildren<Props>) { export default function TransactionProvider(props: PropsWithChildren<Props>) {
const { children } = props; const { children } = props;
const { get, post, put } = useHTTP(); const { get, post, put, del } = useHTTP();
const [{ as }] = useGlobalState(); const [{ as }] = useGlobalState();
const addTransaction = (transaction: pond.Transaction, fileIDs?: string[], otherTeam?: string) => { const addTransaction = (transaction: pond.Transaction, fileIDs?: string[], otherTeam?: string) => {
@ -88,6 +99,35 @@ export default function TransactionProvider(props: PropsWithChildren<Props>) {
); );
}; };
const listTransactionsFor = (
limit: number,
offset: number,
order: "asc" | "desc",
objectKey: string,
state?: pond.TransactionState,
otherTeam?: string
) => {
const view = otherTeam ? otherTeam : as
const url = "/transactionsFor" +
"?limit=" +
limit +
"&offset=" +
offset +
("&order=" + or(order, "asc")) +
"&object=" + objectKey +
(state ? "&state=" + state : "") +
(view ? "&as=" + view : "")
return new Promise<AxiosResponse<pond.ListTransactionsForResponse>>((resolve, reject)=>{
get<pond.ListTransactionsForResponse>(pondURL(url)).then(resp => {
resp.data = pond.ListTransactionsForResponse.fromObject(resp.data)
return resolve(resp)
}).catch(err => {
return reject(err)
})
})
}
const updateTransaction = (key: string, data: pond.TransactionData, otherTeam?: string) => { const updateTransaction = (key: string, data: pond.TransactionData, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as const view = otherTeam ? otherTeam : as
if (view) if (view)
@ -120,14 +160,35 @@ export default function TransactionProvider(props: PropsWithChildren<Props>) {
return put<pond.TransactionPageDataResponse>(pondURL("/transactions/" + key + "/revoke")); return put<pond.TransactionPageDataResponse>(pondURL("/transactions/" + key + "/revoke"));
}; };
const submitApproval = (key: string, approval: boolean, updatedData: pond.Transaction, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as
let url = "/transactions/" + key + "/approval?approve=" + approval.toString()
if(view){
url = url + "&as=" + view
}
return post<pond.SubmitTransactionApprovalResponse>(pondURL(url), updatedData)
}
const removeTransaction = (key: string, otherTeam?: string) => {
const view = otherTeam ? otherTeam : as
let url = "/transactions/" + key + "/remove"
if(view){
url = url + "&as=" + view
}
return del<pond.RemoveTransactionResponse>(pondURL(url))
}
return ( return (
<TransactionAPIContext.Provider <TransactionAPIContext.Provider
value={{ value={{
addTransaction, addTransaction,
listTransactions, listTransactions,
listTransactionsFor,
updateTransaction, updateTransaction,
transactionPageData, transactionPageData,
revokeTransaction revokeTransaction,
submitApproval,
removeTransaction
}}> }}>
{children} {children}
</TransactionAPIContext.Provider> </TransactionAPIContext.Provider>