diff --git a/Binner/Binner.Web/ClientApp/src/common/UserTokenType.js b/Binner/Binner.Web/ClientApp/src/common/UserTokenType.js index 2f69471e..1ce4e5c1 100644 --- a/Binner/Binner.Web/ClientApp/src/common/UserTokenType.js +++ b/Binner/Binner.Web/ClientApp/src/common/UserTokenType.js @@ -1,3 +1,4 @@ export const UserTokenType = { KiCadApiToken: { value: 5, name: 'KiCad Api', icon: 'microchip', description: 'Link KiCad to Binner using an HTTP Library' }, + BinnerBinApiToken: { value: 6, name: 'Binner Bin Api', icon: 'microchip', description: 'Link a Binner Bin' }, }; \ No newline at end of file diff --git a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js index cbe33e42..32447422 100644 --- a/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js +++ b/Binner/Binner.Web/ClientApp/src/components/PartsGrid2Memoized.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useMemo } from "react"; +import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { useNavigate, useLocation, useSearchParams } from "react-router-dom"; import { useTranslation, Trans } from "react-i18next"; import { createMedia } from "@artsy/fresnel"; @@ -69,6 +69,11 @@ export default function PartsGrid2Memoized({ return result; }; + const [showPopup, setShowPopup] = useState(false); + const [popupContent, setPopupContent] = useState(null); + const [partToLocate, setPartToLocate] = useState(""); + let locationKeepaliveTimer = useRef(null); + const [_parts, setParts] = useState(parts); const [_page, setPage] = useState(page); const [pageSize, setPageSize] = useState(getViewPreference('pageSize') || 25); @@ -130,6 +135,7 @@ export default function PartsGrid2Memoized({ useEffect(() => { setDisabledPartIds(disabledPartIds); + stopLocatingPart(); }, [disabledPartIds]); const handlePageChange = (e, control) => { @@ -143,6 +149,70 @@ export default function PartsGrid2Memoized({ window.open(url, "_blank"); }; + const locateKeepalive = async (part) => { + // update the location with our partnumber + return fetchApi(`/api/highlight/update?partNumber=${encodeURIComponent(part)}`, { method: "POST" }); + } + + const startLocatingPart = async (e, part) => { + e.preventDefault(); + e.stopPropagation(); + + // get the partnumber we can use to locate the part + const p = part.partNumber.trim(); + + // set the part to locate for the modal + setPartToLocate(p); + + // clear any keepalives + if (locationKeepaliveTimer.current) { + clearInterval(locationKeepaliveTimer.current); + } + + // do a keepalive straight away to check if we have a good + // response and for the color + const res = await locateKeepalive(p); + + // check for a 200 response + if (res?.responseObject?.status === 200 && res?.data?.color !== null) { + // send a locate to the server every second + locationKeepaliveTimer.current = setInterval(() => { + locateKeepalive(p); + }, 2500); + + // get the color we need to show + const result = res.data.color; + + // show the popup + setPopupContent({ + title: "Part Location", + footerColor: result, + message: ( + <> +

Part is now highlighted in {result}.

+

Press close to stop highlighting {part.partNumber}

+ + ) + }); + setShowPopup(true); + } + }; + + const stopLocatingPart = async () => { + if (locationKeepaliveTimer.current) { + clearInterval(locationKeepaliveTimer.current); + locationKeepaliveTimer.current = null; + + // stop the highlighting straight away when we cancel + if (partToLocate !== undefined) { + fetchApi(`/api/highlight/stop?partNumber=${encodeURIComponent(partToLocate)}`, { method: "POST" }); + } + + setShowPopup(false); + setPartToLocate("") + } + } + const handlePrintLabel = async (e, part) => { e.preventDefault(); e.stopPropagation(); @@ -344,6 +414,7 @@ export default function PartsGrid2Memoized({ case 'actions': return {...def, Header: , columnDefType: 'display', Cell: ({row}) => ( <> + { + stopLocatingPart()} onClose={() => stopLocatingPart()}> + {popupContent?.title} + {popupContent?.message} + + + + ); } diff --git a/Binner/Binner.Web/ClientApp/src/components/modals/AddTokenModal.js b/Binner/Binner.Web/ClientApp/src/components/modals/AddTokenModal.js index 97e04499..5a41d01e 100644 --- a/Binner/Binner.Web/ClientApp/src/components/modals/AddTokenModal.js +++ b/Binner/Binner.Web/ClientApp/src/components/modals/AddTokenModal.js @@ -14,13 +14,16 @@ export function AddTokenModal({ isOpen = false, onAdd, onClose, ...rest }) { const defaultForm = { tokenType: "", partsTimeout: 5, - categoriesTimeout: 10 + categoriesTimeout: 10, + location: "", + binNumber: "" }; const [_isOpen, setIsOpen] = useState(false); const [form, setForm] = useState(defaultForm); const tokenOptions = [ { key: 1, text: GetTypeName(UserTokenType, UserTokenType.KiCadApiToken.value), description: GetTypeProperty(UserTokenType, UserTokenType.KiCadApiToken.value, "description"), value: UserTokenType.KiCadApiToken.value }, + { key: 2, text: GetTypeName(UserTokenType, UserTokenType.BinnerBinApiToken.value), description: GetTypeProperty(UserTokenType, UserTokenType.BinnerBinApiToken.value, "description"), value: UserTokenType.BinnerBinApiToken.value }, ]; useEffect(() => { @@ -43,17 +46,44 @@ export function AddTokenModal({ isOpen = false, onAdd, onClose, ...rest }) { }; const handleAdd = (e) => { - const data = { - tokenType: form.tokenType, - tokenConfig: JSON.stringify({ - timeout_parts_seconds: parseInt(form.partsTimeout), - timeout_categories_seconds: parseInt(form.categoriesTimeout), - }) + if (!onAdd) { + console.error("No onAdd handler defined!"); + return; } - if (onAdd) { - onAdd(e, data); - } else { - console.error("No onAdd handler defined!"); + + switch (form.tokenType) { + case UserTokenType.BinnerBinApiToken.value: + { + // make sure we have all the required parameters + if (form.binNumber.length === 0 || form.location.length === 0) { + return null; + } + + const data = { + tokenType: form.tokenType, + tokenConfig: JSON.stringify({ + location: form.location, + binNumber: form.binNumber + }) + } + onAdd(e, data); + } + break; + case UserTokenType.KiCadApiToken.value: + { + const data = { + tokenType: form.tokenType, + tokenConfig: JSON.stringify({ + timeout_parts_seconds: parseInt(form.partsTimeout), + timeout_categories_seconds: parseInt(form.categoriesTimeout), + }) + } + onAdd(e, data); + } + break; + default: + console.error("No onAdd handler defined for tokenType!"); + break; } }; @@ -88,6 +118,37 @@ export function AddTokenModal({ isOpen = false, onAdd, onClose, ...rest }) { ); + case UserTokenType.BinnerBinApiToken.value: + return ( +
+ + Enter the location of the Binner Bin

} + trigger={} + /> +
+ + Enter the bin number of the Binner Bin

} + trigger={} + /> +
+
+ ); } return (<>); }; diff --git a/Binner/Binner.Web/ClientApp/src/pages/Account.js b/Binner/Binner.Web/ClientApp/src/pages/Account.js index 31960f2e..994157da 100644 --- a/Binner/Binner.Web/ClientApp/src/pages/Account.js +++ b/Binner/Binner.Web/ClientApp/src/pages/Account.js @@ -178,8 +178,10 @@ export function Account(props) { }).then((response) => { if (response.responseObject.ok) { const { data } = response; - // remove any tokens of the same type, only 1 allowed per user - account.tokens = _.filter(account.tokens, (item) => item.tokenType !== request.tokenType); + // remove any tokens of the same type for KiCad, only 1 allowed per user + if (data.tokenType === UserTokenType.KiCadApiToken.value) { + account.tokens = _.filter(account.tokens, (item) => item.tokenType !== request.tokenType); + } account.tokens.push(data); setAccount(account); toast.success(t("success.tokenCreated", "Token created!")); @@ -407,10 +409,10 @@ export function Account(props) { {GetTypeName(UserTokenType, token.tokenType)} -
{token.value}
+
{token.value}
- +
{format(parseJSON(token.dateCreatedUtc), FormatShortDate)} @@ -423,6 +425,17 @@ export function Account(props) { trigger={ handleDownloadKiCadToken(e, token.value)}> Download Config} /> } + + {token.tokenType === UserTokenType.BinnerBinApiToken.value && + Binner bin location (bin number)

} + trigger={ +

+ 📌 {JSON.parse(token.tokenConfig)?.location} (📦 {JSON.parse(token.tokenConfig)?.binNumber}) +

} + /> + }