added tons of files for component functionality; manual component adding added temporarily
This commit is contained in:
parent
58830d480e
commit
45ff49bcea
121 changed files with 25883 additions and 65 deletions
531
src/grain/GrainTransaction.tsx
Normal file
531
src/grain/GrainTransaction.tsx
Normal file
|
|
@ -0,0 +1,531 @@
|
|||
import {
|
||||
Box,
|
||||
Button,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
InputAdornment,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Typography
|
||||
} from "@material-ui/core";
|
||||
import FileUploader from "common/FileUploads/FileUploader";
|
||||
import ResponsiveDialog from "common/ResponsiveDialog";
|
||||
import SearchSelect, { Option } from "common/SearchSelect";
|
||||
import { useMobile } from "hooks";
|
||||
import { Bin, Field } 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 React, { 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)
|
||||
.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).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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue