531 lines
18 KiB
TypeScript
531 lines
18 KiB
TypeScript
import {
|
|
Box,
|
|
Button,
|
|
DialogActions,
|
|
DialogContent,
|
|
DialogTitle,
|
|
FormControlLabel,
|
|
InputAdornment,
|
|
Radio,
|
|
RadioGroup,
|
|
TextField,
|
|
Typography
|
|
} from "@mui/material";
|
|
import FileUploader from "common/FileUploads/FileUploader";
|
|
import ResponsiveDialog from "common/ResponsiveDialog";
|
|
import SearchSelect, { Option } from "common/SearchSelect";
|
|
import { useMobile } from "hooks";
|
|
import { Bin, Field, Contract, GrainBag } from "models";
|
|
// import { Contract } from "models/Contract";
|
|
// import { GrainBag } from "models/GrainBag";
|
|
import moment from "moment";
|
|
import { pond } from "protobuf-ts/pond";
|
|
import {
|
|
useBinAPI,
|
|
useFieldAPI,
|
|
useGlobalState,
|
|
useGrainBagAPI,
|
|
useSnackbar,
|
|
useTransactionAPI
|
|
} from "providers";
|
|
import { useState, useEffect } from "react";
|
|
import { getGrainUnit } from "utils";
|
|
|
|
interface Props {
|
|
mainObject: Bin | GrainBag | Field | Contract;
|
|
grainAdjustment?: number;
|
|
hideFields?: boolean;
|
|
asDestination?: boolean;
|
|
restrictMatching?: boolean;
|
|
open: boolean;
|
|
close: () => void;
|
|
callback?: (newTransaction?: pond.Transaction) => void;
|
|
allowAttachmentUploads?: boolean;
|
|
}
|
|
|
|
export default function GrainTransaction(props: Props) {
|
|
const {
|
|
open,
|
|
mainObject,
|
|
close,
|
|
grainAdjustment,
|
|
callback,
|
|
restrictMatching,
|
|
hideFields,
|
|
asDestination,
|
|
allowAttachmentUploads
|
|
} = props;
|
|
const [isObject, setIsObject] = useState<"source" | "destination" | "">("");
|
|
const [selectedSource, setSelectedSource] = useState<Option | null>(null);
|
|
const [selectedDestination, setSelectedDestination] = useState<Option | null>(null);
|
|
const [binOptions, setBinOptions] = useState<Option[]>([]);
|
|
const [bagOptions, setBagOptions] = useState<Option[]>([]);
|
|
const [fieldOptions, setFieldOptions] = useState<Option[]>([]);
|
|
const [destinationOptions, setDestinationOptions] = useState<Option[]>([]);
|
|
const [sourceOptions, setSourceOptions] = useState<Option[]>([]);
|
|
const binAPI = useBinAPI();
|
|
const grainBagAPI = useGrainBagAPI();
|
|
const fieldAPI = useFieldAPI();
|
|
const [{ as }] = useGlobalState();
|
|
const { openSnack } = useSnackbar();
|
|
const [grainChangeDialog, setGrainChangeDialog] = useState(false);
|
|
const [grainEntry, setGrainEntry] = useState("0");
|
|
const [grainChangeVal, setGrainChangeVal] = useState("0");
|
|
const transactionAPI = useTransactionAPI();
|
|
const [grainMessage, setGrainMessage] = useState("");
|
|
const isMobile = useMobile();
|
|
const [binsLoading, setBinsLoading] = useState(false);
|
|
const [bagsLoading, setBagsLoading] = useState(false);
|
|
const [fieldsLoading, setFieldsLoading] = useState(false);
|
|
const [uploadingFile, setUploadingFile] = useState(false);
|
|
const [imageIDs, setImageIDs] = useState<string[]>([]);
|
|
|
|
const closeDialogs = (confirmed: boolean, newTransaction?: pond.Transaction) => {
|
|
close();
|
|
setGrainChangeVal("0");
|
|
setGrainEntry("0");
|
|
setIsObject("");
|
|
setGrainChangeDialog(false);
|
|
if (callback && confirmed) {
|
|
callback(newTransaction);
|
|
}
|
|
};
|
|
|
|
//get the possible destinations/options (grainbags and bins)
|
|
useEffect(() => {
|
|
if (open) {
|
|
if (grainAdjustment) {
|
|
setGrainChangeVal(grainAdjustment.toString());
|
|
} else {
|
|
setGrainChangeVal("0");
|
|
}
|
|
if ((grainAdjustment && grainAdjustment > 0) || asDestination) {
|
|
setIsObject("destination");
|
|
setSelectedDestination({
|
|
label: mainObject.name(),
|
|
value: mainObject,
|
|
group: mainObject.objectTypeString()
|
|
});
|
|
setSelectedSource(null);
|
|
} else if (grainAdjustment && grainAdjustment < 0) {
|
|
setIsObject("source");
|
|
setSelectedSource({
|
|
label: mainObject.name(),
|
|
value: mainObject,
|
|
group: mainObject.objectTypeString()
|
|
});
|
|
setSelectedDestination(null);
|
|
}
|
|
|
|
//list bins and add to both options
|
|
if (!binsLoading) {
|
|
setBinsLoading(true);
|
|
binAPI
|
|
.listBins(0, 0, undefined, undefined, undefined, as)
|
|
.then(resp => {
|
|
//let sourceOps: Option[] = sourceOptions
|
|
let binOps: Option[] = [];
|
|
resp.data.bins.forEach(bin => {
|
|
let b = Bin.create(bin);
|
|
if (mainObject.key() !== b.key()) {
|
|
let op: Option = {
|
|
label: b.name(),
|
|
value: b,
|
|
group: "Bins"
|
|
};
|
|
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && binOps.push(op);
|
|
}
|
|
});
|
|
setBinOptions([...binOps]);
|
|
})
|
|
.finally(() => {
|
|
setBinsLoading(false);
|
|
});
|
|
}
|
|
|
|
//list bags and add to both options
|
|
if (!bagsLoading) {
|
|
setBagsLoading(true);
|
|
grainBagAPI
|
|
.listGrainBags(0, 0, undefined, undefined, undefined, undefined, undefined, undefined, as)
|
|
.then(resp => {
|
|
//let sourceOps: Option[] = sourceOptions
|
|
let bagOps: Option[] = [];
|
|
resp.data.grainBags.forEach(bag => {
|
|
let b = GrainBag.create(bag);
|
|
if (mainObject.key() !== b.key()) {
|
|
let op: Option = {
|
|
label: b.name(),
|
|
value: b,
|
|
group: "Grain Bags"
|
|
};
|
|
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && bagOps.push(op);
|
|
}
|
|
});
|
|
setBagOptions([...bagOps]);
|
|
})
|
|
.finally(() => {
|
|
setBagsLoading(false);
|
|
});
|
|
}
|
|
|
|
//list fields and add to source options
|
|
if (!fieldsLoading && !hideFields) {
|
|
setFieldsLoading(true);
|
|
fieldAPI
|
|
.listFields(0, 0, undefined, undefined, undefined, as)
|
|
.then(resp => {
|
|
//let sourceOps: Option[] = sourceOptions
|
|
let fieldOps: Option[] = [];
|
|
resp.data.fields.forEach(field => {
|
|
let f = Field.create(field);
|
|
let op: Option = {
|
|
label: f.name(),
|
|
value: f,
|
|
group: "Fields"
|
|
};
|
|
(!restrictMatching || grainTypesMatch(op.value, mainObject)) && fieldOps.push(op);
|
|
});
|
|
setFieldOptions([...fieldOps]);
|
|
})
|
|
.finally(() => {
|
|
setFieldsLoading(false);
|
|
});
|
|
}
|
|
}
|
|
}, [binAPI, fieldAPI, grainBagAPI, as, grainAdjustment, mainObject, open, asDestination]); //eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
//watches the object options arrays and build new options when they change
|
|
useEffect(() => {
|
|
setSourceOptions([...binOptions.concat(bagOptions, fieldOptions)]);
|
|
setDestinationOptions([...binOptions.concat(bagOptions)]);
|
|
}, [binOptions, bagOptions, fieldOptions]);
|
|
|
|
//create a transaction to enter into the table
|
|
const createTransaction = (finalVal: number) => {
|
|
let dest = selectedDestination;
|
|
let source = selectedSource;
|
|
let bpt = 1;
|
|
//use the sources bushel per tonne if one was selected otherwise use the destination
|
|
if (source) {
|
|
bpt = source.value.bushelsPerTonne();
|
|
} else if (dest) {
|
|
bpt = dest.value.bushelsPerTonne();
|
|
}
|
|
|
|
let newTransaction = pond.Transaction.create({
|
|
timestamp: moment().toISOString(),
|
|
transaction: pond.TransactionData.create({
|
|
grainTransaction: pond.GrainTransaction.create({
|
|
bushels: finalVal,
|
|
message: grainMessage,
|
|
bushelsPerTonne: bpt
|
|
})
|
|
})
|
|
});
|
|
|
|
if (dest) {
|
|
let obj: Contract | Bin | GrainBag = dest.value;
|
|
newTransaction.toKey = obj.key();
|
|
newTransaction.toObject = obj.objectType();
|
|
let grainTransaction = newTransaction.transaction?.grainTransaction;
|
|
//set the grain in the transaction to use the destination by default
|
|
if (grainTransaction) {
|
|
grainTransaction.grainType = obj.grain();
|
|
grainTransaction.customGrain = obj.customType();
|
|
grainTransaction.subtype = obj.subtype();
|
|
}
|
|
}
|
|
if (source) {
|
|
let obj: Field | Bin | GrainBag = source.value;
|
|
newTransaction.fromKey = obj.key();
|
|
newTransaction.fromObject = obj.objectType();
|
|
let grainTransaction = newTransaction.transaction?.grainTransaction;
|
|
//over write the grain type to be what the source has if there was a source
|
|
if (grainTransaction) {
|
|
grainTransaction.grainType = obj.grain();
|
|
grainTransaction.customGrain = obj.customType();
|
|
grainTransaction.subtype = obj.subtype();
|
|
}
|
|
}
|
|
transactionAPI.addTransaction(newTransaction, imageIDs, as).then(resp => {
|
|
newTransaction.key = resp.data.key;
|
|
closeDialogs(true, newTransaction);
|
|
});
|
|
};
|
|
|
|
const updateInventory = () => {
|
|
//this is the amount to add to the destination
|
|
let destinationAdd = Math.abs(
|
|
isNaN(parseFloat(grainChangeVal)) ? 1 : parseFloat(grainChangeVal)
|
|
);
|
|
|
|
//check the source to see how much can actually be moved
|
|
if (selectedSource) {
|
|
if (selectedSource?.value.objectTypeString() === "Bin") {
|
|
let bin: Bin = selectedSource.value as Bin;
|
|
if (bin.settings.inventory) {
|
|
//if the difference is less than 0 that means there werent enough bushels in the source
|
|
if (bin.settings.inventory.grainBushels - destinationAdd < 0) {
|
|
//set the amount to add to the destination to be what is in the source
|
|
destinationAdd = bin.settings.inventory.grainBushels;
|
|
}
|
|
}
|
|
} else if (selectedSource?.value.objectTypeString() === "Grain Bag") {
|
|
let bag: GrainBag = selectedSource.value as GrainBag;
|
|
if (bag.bushels() - destinationAdd < 0) {
|
|
destinationAdd = bag.bushels();
|
|
}
|
|
}
|
|
}
|
|
//check the destination to see if there is enough room to move from the source
|
|
if (selectedDestination) {
|
|
if (selectedDestination?.value.objectTypeString() === "Bin") {
|
|
let bin: Bin = selectedDestination.value as Bin;
|
|
if (bin.settings.inventory && bin.settings.specs) {
|
|
if (
|
|
bin.settings.inventory.grainBushels + destinationAdd >
|
|
bin.settings.specs.bushelCapacity
|
|
) {
|
|
openSnack("Destination does not have enough room in bin for selected amount");
|
|
return;
|
|
}
|
|
}
|
|
} else if (selectedDestination?.value.objectTypeString() === "Grain Bag") {
|
|
let bag: GrainBag = selectedDestination.value as GrainBag;
|
|
if (bag.bushels() + destinationAdd > bag.capacity()) {
|
|
openSnack("Destination does not have enough room in bag for selected amount");
|
|
return;
|
|
}
|
|
} else if (selectedDestination?.value.objectTypeString() === "Contract") {
|
|
let contract: Contract = selectedDestination.value as Contract;
|
|
if (contract.settings.delivered + destinationAdd > contract.settings.size) {
|
|
openSnack("Selected value will overdeliver on the contract");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//---!!moved the update functionality for updating objects to the backend inside the add transaction call!!---
|
|
createTransaction(destinationAdd);
|
|
};
|
|
|
|
//select for sources (fields, bins, bags)
|
|
const sourceSelect = () => {
|
|
return (
|
|
<Box>
|
|
<SearchSelect
|
|
label="Grain Source"
|
|
selected={selectedSource}
|
|
changeSelection={option => {
|
|
setSelectedSource(option);
|
|
}}
|
|
group
|
|
//disabled={!canEdit}
|
|
options={sourceOptions}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//select for destinations (bins,bags)
|
|
const destinationSelect = () => {
|
|
return (
|
|
<Box>
|
|
<SearchSelect
|
|
label="Grain Destination"
|
|
selected={selectedDestination}
|
|
changeSelection={option => {
|
|
setSelectedDestination(option);
|
|
}}
|
|
group
|
|
options={destinationOptions}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const grainTypesMatch = (
|
|
source: Bin | GrainBag | Field | Contract,
|
|
destination: Bin | GrainBag | Field | Contract
|
|
) => {
|
|
let matching = false;
|
|
//check if the storage types are the same between the two
|
|
if (source.storage() === destination.storage()) {
|
|
if (source.storage() === pond.BinStorage.BIN_STORAGE_SUPPORTED_GRAIN) {
|
|
if (source.grain() === destination.grain()) {
|
|
matching = true;
|
|
}
|
|
} else if (source.storage() === pond.BinStorage.BIN_STORAGE_UNSUPPORTED_GRAIN) {
|
|
if (
|
|
source.customType().toLowerCase() === destination.customType().toLowerCase() &&
|
|
source.bushelsPerTonne() === destination.bushelsPerTonne()
|
|
) {
|
|
matching = true;
|
|
}
|
|
}
|
|
}
|
|
return matching;
|
|
};
|
|
|
|
const confirm = () => {
|
|
if (selectedSource && selectedDestination) {
|
|
if (grainTypesMatch(selectedSource.value, selectedDestination.value)) {
|
|
updateInventory();
|
|
} else {
|
|
//open dialog saying that the destinations grain type will change from this action
|
|
setGrainChangeDialog(true);
|
|
}
|
|
} else {
|
|
updateInventory();
|
|
}
|
|
};
|
|
|
|
const grainChange = () => {
|
|
return (
|
|
<ResponsiveDialog
|
|
open={grainChangeDialog}
|
|
onClose={() => {
|
|
setGrainChangeDialog(false);
|
|
}}>
|
|
<DialogContent>
|
|
The grain type of the destination may change as a result of this action.
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button
|
|
onClick={() => {
|
|
setGrainChangeDialog(false);
|
|
}}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={updateInventory} variant="contained" color="primary">
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ResponsiveDialog
|
|
open={open}
|
|
onClose={() => {
|
|
close();
|
|
}}>
|
|
{grainChange()}
|
|
<DialogTitle>New Grain Transaction</DialogTitle>
|
|
<DialogContent style={{ minWidth: isMobile ? "100%" : 500 }}>
|
|
{/* radio button to determine if the object you are making the adjustment from is the source/destination */}
|
|
{!grainAdjustment && !asDestination && (
|
|
<RadioGroup>
|
|
<FormControlLabel
|
|
value="source"
|
|
control={<Radio />}
|
|
label="Load Out"
|
|
onChange={() => {
|
|
setIsObject("source");
|
|
let option: Option = {
|
|
value: mainObject,
|
|
label: mainObject.name(),
|
|
group: mainObject.objectTypeString()
|
|
};
|
|
setSelectedSource(option);
|
|
setSelectedDestination(null);
|
|
}}
|
|
/>
|
|
<FormControlLabel
|
|
value="destination"
|
|
control={<Radio />}
|
|
label="Load In"
|
|
onChange={() => {
|
|
setIsObject("destination");
|
|
let option: Option = {
|
|
value: mainObject,
|
|
label: mainObject.name(),
|
|
group: mainObject.objectTypeString()
|
|
};
|
|
setSelectedSource(null);
|
|
setSelectedDestination(option);
|
|
}}
|
|
/>
|
|
</RadioGroup>
|
|
)}
|
|
{!grainAdjustment && (
|
|
<TextField
|
|
label="Adjustment"
|
|
margin="normal"
|
|
fullWidth
|
|
type="number"
|
|
variant="outlined"
|
|
value={grainEntry}
|
|
error={isNaN(parseFloat(grainEntry))}
|
|
helperText={isNaN(parseFloat(grainEntry)) ? "must be a valid number" : ""}
|
|
InputProps={{
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
{getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT ? "mT" : "bu"}
|
|
</InputAdornment>
|
|
)
|
|
}}
|
|
onChange={e => {
|
|
//if the user is viewing the grain in weight
|
|
let grainVal = e.target.value;
|
|
if (getGrainUnit() === pond.GrainUnit.GRAIN_UNIT_WEIGHT) {
|
|
//need to convert what the user input (weight) into what is actually stored (bushels)
|
|
//use source of the conversion value if it was selected, otherwise use the destination
|
|
if (!isNaN(parseFloat(e.target.value))) {
|
|
if (selectedSource) {
|
|
grainVal = (+grainVal * selectedSource.value.bushelsPerTonne()).toFixed(2);
|
|
} else if (selectedDestination) {
|
|
grainVal = (+grainVal * selectedDestination.value.bushelsPerTonne()).toFixed(2);
|
|
}
|
|
}
|
|
}
|
|
setGrainEntry(e.target.value);
|
|
setGrainChangeVal(grainVal);
|
|
}}
|
|
/>
|
|
)}
|
|
<Typography>Leave selection blank to perform a correction</Typography>
|
|
{isObject === "source" && destinationSelect()}
|
|
{isObject === "destination" && sourceSelect()}
|
|
<TextField
|
|
label="Message(optional)"
|
|
margin="dense"
|
|
fullWidth
|
|
variant="outlined"
|
|
value={grainMessage}
|
|
onChange={e => setGrainMessage(e.target.value)}
|
|
/>
|
|
<Box marginTop={1}>
|
|
{allowAttachmentUploads && (
|
|
<FileUploader
|
|
uploadEnd={fileID => {
|
|
if (fileID) {
|
|
let ids = imageIDs;
|
|
ids.push(fileID);
|
|
setImageIDs([...ids]);
|
|
}
|
|
setUploadingFile(false);
|
|
}}
|
|
uploadStart={() => {
|
|
setUploadingFile(true);
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</DialogContent>
|
|
|
|
<DialogActions>
|
|
<Button onClick={() => closeDialogs(false)}>Cancel</Button>
|
|
<Button
|
|
onClick={confirm}
|
|
variant="contained"
|
|
color="primary"
|
|
disabled={uploadingFile || isNaN(parseFloat(grainEntry))}>
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</ResponsiveDialog>
|
|
);
|
|
}
|