├── icon.ico ├── icon.icns ├── src ├── images │ ├── favicon.png │ ├── loading.gif │ ├── logo-dark.png │ ├── logo-light.png │ ├── logo-light-sm.png │ ├── placeholderItemImage.png │ ├── computerIconBlueLines.png │ └── computerIconWhiteLines.png ├── components │ ├── Card.jsx │ ├── Loading.jsx │ ├── Dropdown.jsx │ ├── Error.jsx │ └── Layout.jsx ├── index.jsx ├── index.css ├── App.jsx └── pages │ ├── sign_in.jsx │ ├── scan_a_tote.jsx │ └── order.jsx ├── tailwind.config.js ├── .gitignore ├── README.md ├── public └── index.html ├── app.js └── package.json /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/icon.ico -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/icon.icns -------------------------------------------------------------------------------- /src/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/favicon.png -------------------------------------------------------------------------------- /src/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/loading.gif -------------------------------------------------------------------------------- /src/images/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/logo-dark.png -------------------------------------------------------------------------------- /src/images/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/logo-light.png -------------------------------------------------------------------------------- /src/images/logo-light-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/logo-light-sm.png -------------------------------------------------------------------------------- /src/images/placeholderItemImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/placeholderItemImage.png -------------------------------------------------------------------------------- /src/images/computerIconBlueLines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/computerIconBlueLines.png -------------------------------------------------------------------------------- /src/images/computerIconWhiteLines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Blue-BigTech/Shipping-Electron-React/HEAD/src/images/computerIconWhiteLines.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | } -------------------------------------------------------------------------------- /src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Card = (props, ref) => { 4 | return ( 5 |
{props.children}
6 | ) 7 | } 8 | 9 | export default React.forwardRef(Card) -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import "./index.css" 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import loader from '../images/loading.gif' 4 | 5 | const Loading = () => { 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default Loading -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | /release-builds 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | .env 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html,body, #root { height: 100vh; margin: 0; } 6 | 7 | label span:before, 8 | label span:after { 9 | content: ''; 10 | } 11 | 12 | 13 | /* 14 | * We are using the :before peudo elemnt as the actual button, 15 | * then we'll position the :after over it. You could also use a background-image, 16 | * font-icon, or really anything if you want different styles. 17 | * For the specific style we're going for, this approach is simply the easiest, but 18 | * once you understand the concept you can really do it however you like. 19 | */ 20 | 21 | label span:before { 22 | border: 1px solid #222021; 23 | width: 20px; 24 | height: 20px; 25 | display: inline-block; 26 | vertical-align: top; 27 | } 28 | 29 | label span:after { 30 | background-color: rgb(59 130 246); 31 | width: 14px; 32 | height: 14px; 33 | position: absolute; 34 | top: -1px; 35 | left: 3px; 36 | transition: 300ms; 37 | opacity: 0; 38 | } 39 | 40 | label input:checked+span:after { 41 | opacity: 1; 42 | } 43 | 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Build Instructions 2 | 3 | **Make sure all dependices are installed** 4 | `npm i --save` 5 | 6 | **Mac** 7 | 8 | 1. change isDev to false in app.js 9 | 2. change .env variables to correct ones 10 | 3. build react 11 | `npm run build` 12 | 4. delete release-builds and dist in root 13 | 5. run package for darwin command 14 | `electron-packager . --overwrite --platform=darwin --arch=x64 --icon=assets/icons/mac/icon.icns --prune=true --out=release-builds` 15 | 6. packed application into dist folder 16 | `./node_modules/.bin/electron-builder --prepackaged ./release-builds/HoopSwagg\ Shipping-darwin-x64` 17 | 7. final packaged dmg executable will be in dist folder 18 | 19 | **Windows** 20 | 21 | 1. change isDev to false in app.js 22 | 2. change .env variables to correct ones 23 | 3. build react 24 | `npm run build` 25 | 4. delete release-builds and dist in root 26 | 5. run package for win32 command 27 | `electron-packager . --overwrite --platform=win32 --arch=x64 --icon=assets/icons/mac/icon.icns --prune=true --out=release-builds` 28 | 6. folder is large, and will take a while to move, but move the folder ./release-builds/shipping-win32-x64 to the PC 29 | -------------------------------------------------------------------------------- /src/components/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | // React Imports 2 | import React from 'react' 3 | import { useState } from 'react' 4 | 5 | const Dropdown = ({children, icon, text, textColorClass = "text-white", arrowColorClass = "text-blue-500", className}) => { 6 | 7 | // Menu Open State 8 | const [dropdownOpen, setDropdownOpen] = useState(false); 9 | 10 | // Opens and closes dropdown 11 | const toggleDropdown = function () { 12 | setDropdownOpen(!dropdownOpen) 13 | } 14 | 15 | return ( 16 |
17 | {icon} 18 | {text} 19 | 20 |
21 | 24 |
25 |
26 | ) 27 | } 28 | 29 | export default Dropdown -------------------------------------------------------------------------------- /src/components/Error.jsx: -------------------------------------------------------------------------------- 1 | // React Imports 2 | import React from 'react' 3 | 4 | // Component Imports 5 | import Card from './Card' 6 | import Popup from 'reactjs-popup' 7 | 8 | const Error = ({message, buttonText, onClick}) => { 9 | 10 | const convertErrorMessage = (errorMessage) => { 11 | 12 | return errorMessage 13 | 14 | } 15 | 16 | return ( 17 |
18 | 19 |
OOPS, There's been an error!
20 |

{convertErrorMessage(message)}

21 | 22 | {/* more details popup */} 23 | More Details} modal> 24 |

{message}

25 |
26 | 27 |
28 |
29 | {buttonText} 30 |
31 |
32 |
33 |
34 | ) 35 | } 36 | 37 | export default Error -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | // Bootstrap imports 2 | import "bootstrap-icons/font/bootstrap-icons.css"; 3 | 4 | // React + React Dom Imports 5 | import { useState } from "react"; 6 | import { 7 | HashRouter as Router, 8 | Routes, 9 | Route 10 | } from "react-router-dom"; 11 | 12 | 13 | 14 | // Page Imports 15 | import ScanATotePage from './pages/scan_a_tote'; 16 | import OrderPage from "./pages/order" 17 | import SignInPage from "./pages/sign_in"; 18 | 19 | // Component Imports 20 | import Layout from "./components/Layout"; 21 | 22 | function App() { 23 | 24 | const [user, setUser] = useState(JSON.parse(localStorage.getItem("user"))); 25 | 26 | if (!user) { 27 | return 28 | } 29 | 30 | return ( 31 | 32 | 33 | }> 34 | 35 | }> 36 | 37 | }> 38 | 39 | }> 40 | 41 | 42 | ); 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | HoopSwagg Shipping 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/Layout.jsx: -------------------------------------------------------------------------------- 1 | // React Imports 2 | import React from 'react' 3 | import { useState } from 'react' 4 | import { useNavigate, useSearchParams } from 'react-router-dom' 5 | 6 | // Component Imports 7 | import Dropdown from "./Dropdown" 8 | import Error from './Error' 9 | import Loading from './Loading' 10 | 11 | // Img Imports 12 | import logoLight from "../images/logo-light.png" 13 | 14 | const Layout = ({children, user, setUser}) => { 15 | 16 | // Layout States 17 | const [loading, setLoading] = useState(false); 18 | const [error, setError] = useState(""); 19 | 20 | // Use search Params 21 | const [searchParams] = useSearchParams(); 22 | 23 | // Init Navigate 24 | const navigate = useNavigate(); 25 | 26 | // Logs user out 27 | const logout = async () => { 28 | setLoading(true) 29 | 30 | const orderID = searchParams.get("order_id") 31 | if (orderID) { 32 | const ok = await unlockOrder(parseInt(orderID)) 33 | if (!ok) { 34 | setLoading(false) 35 | return 36 | } 37 | } 38 | 39 | localStorage.removeItem("user") 40 | setUser(); 41 | setLoading(false) 42 | 43 | } 44 | 45 | 46 | // Unlocks order when user goes back to tote page 47 | const unlockOrder = async (orderID) => { 48 | 49 | // Unlock Order 50 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/unlock-order`, { 51 | method: "POST", 52 | headers: { 53 | "Content-Type": "application/json", 54 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 55 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 56 | }, 57 | body: JSON.stringify({ 58 | order_id: orderID, 59 | user_id: user.ID 60 | }) 61 | }) 62 | 63 | // Error Handling 64 | if (!res.ok) { 65 | const errorRes = await res.json(); 66 | const errorMessage = errorRes.error; 67 | setError(errorMessage) 68 | return false 69 | } 70 | 71 | // Navigate to sign in 72 | navigate("/sign-in") 73 | 74 | } 75 | 76 | return ( 77 |
78 |
79 | 80 | } text={user.Username}> 81 |
  • 82 | Sign Out 83 |
  • 84 |
    85 |
    86 |
    87 | 88 | {/* Render error message */} 89 | { error && } 90 | 91 | {/* Render Loading */} 92 | {loading && !error && } 93 | 94 | {/* Render Children */} 95 | {!loading && !error && children} 96 | 97 | 98 |
    99 |
    100 | ) 101 | } 102 | 103 | export default Layout -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Node Imports 2 | const path = require('path'); 3 | const fs = require("fs") 4 | const url = require('url'); 5 | 6 | // PDF Printing Imports 7 | const { download } = require('electron-dl'); 8 | const unixPrint = require("unix-print"); 9 | const windowsPrint = require("pdf-to-printer"); 10 | 11 | // Electron Imports 12 | const { app, BrowserWindow, ipcMain } = require('electron'); 13 | const isDev = false; 14 | 15 | // Create window once Electron has been initialized 16 | app.whenReady().then(createWindow); 17 | 18 | // Quit app on all windows closed 19 | app.on('window-all-closed', () => { 20 | if (process.platform !== 'darwin') { 21 | app.quit(); 22 | } 23 | }); 24 | 25 | // On activation, open a window if one doesn't exist 26 | app.on('activate', () => { 27 | if (BrowserWindow.getAllWindows().length === 0) { 28 | createWindow(); 29 | } 30 | }); 31 | 32 | // Creates Browser Window 33 | function createWindow() { 34 | 35 | // Create the browser window. 36 | const win = new BrowserWindow({ 37 | webPreferences: { 38 | nodeIntegration: true, 39 | contextIsolation: false 40 | }, 41 | }); 42 | 43 | // Make Browser Window Full Screen 44 | win.maximize(); 45 | 46 | // Load in correct page 47 | 48 | if (process.platform === 'darwin') { 49 | win.loadURL( 50 | isDev 51 | ? 'http://localhost:3000/scan' 52 | : `file://${path.join(__dirname, 'build/index.html#/scan')}` 53 | ); 54 | } else if (process.platform === 'win32') { 55 | win.loadURL( 56 | isDev 57 | ? 'http://localhost:3000/scan' 58 | : url.format({ 59 | pathname: path.join(__dirname,`build/index.html`), 60 | protocol:'file', 61 | slashes:true, 62 | hash:`#/scan` 63 | }) 64 | 65 | ); 66 | } 67 | 68 | 69 | // EVENT LISTENERS 70 | 71 | 72 | // Wait for a request to print a label from the front end 73 | ipcMain.on("print-label", (event, data) => { 74 | 75 | // Parse the print data 76 | const printData = JSON.parse(data) 77 | 78 | 79 | 80 | // Start download 81 | download(BrowserWindow.getFocusedWindow(), printData.URL, { filename: "temporary-label.pdf" }).then(async (dl) => { 82 | 83 | // Print label if it's a mac 84 | if (process.platform === "darwin") { 85 | await unixPrint.print(dl.getSavePath(), printData.printerName); 86 | } 87 | 88 | // Print label if it's a windows 89 | if (process.platform === "win32") { 90 | 91 | // Set Printing Options 92 | const printingOptions = { 93 | silent: true 94 | } 95 | if (printData.printerName) { 96 | printingOptions.printer = printData.printerName; 97 | } 98 | 99 | 100 | await windowsPrint.print(dl.getSavePath(), printingOptions); 101 | } 102 | 103 | 104 | // Delete temporary label file 105 | fs.unlinkSync(dl.getSavePath()); 106 | 107 | }) 108 | 109 | }) 110 | 111 | 112 | // Listen to request to get list of printers 113 | ipcMain.on("get-printer-list", async (event, data) => { 114 | 115 | // Create window 116 | const printWindow = new BrowserWindow({ 'auto-hide-menu-bar': true, show: false }); 117 | 118 | // Load random URL 119 | await printWindow.loadURL("https://www.google.com"); 120 | 121 | // Get Printer List 122 | const list = await printWindow.webContents.getPrintersAsync(); 123 | 124 | // Return Printer List to Front End 125 | event.reply("get-printer-list-reply", list) 126 | }) 127 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipping", 3 | "productName": "HoopSwagg Shipping", 4 | "version": "0.1.0", 5 | "private": true, 6 | "despcription": "", 7 | "author": "Brady Agranoff", 8 | "dependencies": { 9 | "@testing-library/jest-dom": "^5.16.4", 10 | "@testing-library/react": "^13.3.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "ajv": "^8.11.0", 13 | "bootstrap-icons": "^1.8.3", 14 | "electron-dl": "^3.3.1", 15 | "electron-squirrel-startup": "^1.0.0", 16 | "fs": "^0.0.1-security", 17 | "jsonfile": "^6.1.0", 18 | "node-downloader-helper": "^2.1.1", 19 | "path": "^0.12.7", 20 | "pdf-page-counter": "^1.0.3", 21 | "pdf-to-printer": "^5.3.0", 22 | "pdf2pic": "^2.1.4", 23 | "react": "^18.2.0", 24 | "react-dom": "^18.2.0", 25 | "react-router-dom": "6.3.0", 26 | "react-scripts": "^5.0.1", 27 | "reactjs-popup": "^2.0.5", 28 | "request-promise-native": "^1.0.9", 29 | "serve": "^14.0.1", 30 | "unix-print": "^1.1.0", 31 | "web-vitals": "^2.1.4" 32 | }, 33 | "resolutions": { 34 | "react-error-overlay": "6.0.9" 35 | }, 36 | "main": "app.js", 37 | "homepage": "./", 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject", 43 | "dev": "concurrently -k \"BROWSER=none npm start\" \"npm:electron\"", 44 | "electron": "while ! bash -c \"echo > /dev/tcp/localhost/3000\"; do sleep 1; done; electron .", 45 | "preinstall": "npx npm-force-resolutions", 46 | "postinstall": "electron-builder install-app-deps", 47 | "package": "electron-forge package", 48 | "make": "electron-forge make", 49 | "pack": "electron-builder --dir", 50 | "dist": "electron-builder" 51 | }, 52 | "postinstall": "electron-builder install-app-deps", 53 | "eslintConfig": { 54 | "extends": [ 55 | "react-app", 56 | "react-app/jest" 57 | ] 58 | }, 59 | "browserslist": { 60 | "production": [ 61 | ">0.2%", 62 | "not dead", 63 | "not op_mini all" 64 | ], 65 | "development": [ 66 | "last 1 chrome version", 67 | "last 1 firefox version", 68 | "last 1 safari version" 69 | ] 70 | }, 71 | "devDependencies": { 72 | "@electron-forge/cli": "^6.0.0-beta.65", 73 | "@electron-forge/maker-deb": "^6.0.0-beta.65", 74 | "@electron-forge/maker-dmg": "^6.0.0-beta.65", 75 | "@electron-forge/maker-rpm": "^6.0.0-beta.65", 76 | "@electron-forge/maker-squirrel": "^6.0.0-beta.65", 77 | "@electron-forge/maker-zip": "^6.0.0-beta.65", 78 | "autoprefixer": "^10.4.7", 79 | "concurrently": "^7.2.2", 80 | "electron": "^19.0.13", 81 | "electron-builder": "^23.3.3", 82 | "electron-is-dev": "^2.0.0", 83 | "electron-packager": "^15.5.2", 84 | "electron-rebuild": "^3.2.8", 85 | "postcss": "^8.4.14", 86 | "tailwindcss": "^3.1.4", 87 | "wait-on": "^6.0.1" 88 | }, 89 | "build": { 90 | "extends": null, 91 | "appId": "your.id", 92 | "mac": { 93 | "category": "your.app.category.type" 94 | } 95 | }, 96 | "config": { 97 | "forge": { 98 | "packagerConfig": {}, 99 | "makers": [ 100 | { 101 | "name": "@electron-forge/maker-squirrel", 102 | "config": { 103 | "name": "shipping", 104 | "authors": "Brady Agranoff", 105 | "exe": "shipping.exe" 106 | } 107 | }, 108 | { 109 | "name": "@electron-forge/maker-zip", 110 | "platforms": [ 111 | "darwin" 112 | ] 113 | }, 114 | { 115 | "name": "@electron-forge/maker-deb", 116 | "config": {} 117 | }, 118 | { 119 | "name": "@electron-forge/maker-rpm", 120 | "config": {} 121 | } 122 | ] 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/pages/sign_in.jsx: -------------------------------------------------------------------------------- 1 | // React imports 2 | import React from 'react' 3 | import { useState } from 'react'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | // Img imports 7 | import computerIconWhiteLines from "../images/computerIconWhiteLines.png" 8 | import logoDark from "../images/logo-dark.png" 9 | 10 | const SignInPage = ({ setUser }) => { 11 | 12 | // Component States 13 | const [loginFormError, setLoginFormError] = useState(""); 14 | const [buttonLoading, setButtonLoading] = useState(false); 15 | const [passwordShowing, setPasswordShowing] = useState(false); 16 | 17 | // Init Navigate 18 | const navigate = useNavigate(); 19 | 20 | // Function to login 21 | const login = async (e) => { 22 | 23 | // Prevent Default Form Submission 24 | e.preventDefault(); 25 | 26 | // Set button loading 27 | setButtonLoading(true) 28 | 29 | // Call to electron backend to get user from HQ 30 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/user`, { 31 | headers: { 32 | "Content-Type": "application/x-www-form-urlencoded", 33 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 34 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 35 | }, 36 | method: "POST", 37 | body: `username=${encodeURIComponent(e.target[0].value)}&password=${encodeURIComponent(e.target[1].value)}` 38 | }) 39 | 40 | 41 | 42 | // Error Handle 43 | if (!res.ok) { 44 | const errorRes = await res.json(); 45 | const errorMessage = errorRes.error; 46 | if (await errorMessage && (errorMessage === "pg: no rows in result set" || errorMessage === "username or password is incorrect.")) { 47 | setLoginFormError("Incorrect Username or Password.") 48 | } else { 49 | setLoginFormError("Uh oh. An error has occured. Please try again later.") 50 | } 51 | 52 | setButtonLoading(false) 53 | return 54 | } 55 | 56 | 57 | // Set User 58 | const userData = await res.json(); 59 | setUser(userData) 60 | 61 | // Set user to localstorage so that if the page gets refreshed, user is still logged in 62 | localStorage.setItem("user", JSON.stringify(userData)) 63 | 64 | // Navigate to scan page if it isn't already there 65 | navigate("/scan") 66 | 67 | } 68 | 69 | // Handles toggle password showing 70 | const togglePasswordShowing = () => { 71 | setPasswordShowing(!passwordShowing) 72 | } 73 | 74 | return ( 75 |
    76 |
    77 | 78 |
    79 |

    Sign In

    80 |
      {loginFormError}
    81 | 82 | 83 |
    84 | 85 | {/* eyeball toggle */} 86 | 87 |
    88 | 89 | 92 |
    93 |
    94 |
    95 |

    96 | All orders in one place for you and your team 97 |

    98 | 99 |
    100 |
    101 | ) 102 | } 103 | 104 | export default SignInPage -------------------------------------------------------------------------------- /src/pages/scan_a_tote.jsx: -------------------------------------------------------------------------------- 1 | // React Imports 2 | import React from 'react' 3 | import Popup from 'reactjs-popup'; 4 | import 'reactjs-popup/dist/index.css'; 5 | import { useState, useEffect, useRef } from 'react' 6 | import { useNavigate } from 'react-router-dom'; 7 | 8 | // Img Imports 9 | import computerIconBlueLines from "../images/computerIconBlueLines.png" 10 | import loader from '../images/loading.gif' 11 | 12 | const ScanATotePage = ({ user }) => { 13 | 14 | // Init Navigate 15 | let navigate = useNavigate(); 16 | 17 | // Component states 18 | const [formError, setFormError] = useState(""); 19 | const [formLoading, setFormLoading] = useState(true); 20 | 21 | // Input Ref 22 | const inputRef = useRef(); 23 | 24 | // Set Barcode scan 25 | let barcodeScan = ""; 26 | 27 | // Check if the user already has an order locked 28 | useEffect(() => { 29 | getPreviouslyLockedOrder() 30 | }); 31 | 32 | // Listens for and handles barcode scan 33 | useEffect(() => { 34 | 35 | // Handles Keydown 36 | function handleKeyDown (e) { 37 | 38 | // If keyCode is 13 (enter) then check if there are barcode scan keys and if there are handle barcode scan 39 | if (e.keyCode === 13 && barcodeScan.length > 3) { 40 | lockOrder(barcodeScan); 41 | return 42 | } 43 | 44 | // Skip if pressed key is shift key 45 | if (e.keyCode === 16) { 46 | return 47 | } 48 | 49 | // Push Keycode to barcode scan variable 50 | barcodeScan += e.key; 51 | 52 | // Set Timeout to clear state 53 | setTimeout(() => { 54 | barcodeScan = "" 55 | }, 110) 56 | 57 | } 58 | 59 | // Adds event listener to page for keydown 60 | document.addEventListener('keydown', handleKeyDown) 61 | 62 | // Don't forget to clean up 63 | return function cleanup() { 64 | document.removeEventListener('keydown', handleKeyDown); 65 | } 66 | }, []) 67 | 68 | // Gets previously locked order and redirects to that page 69 | async function getPreviouslyLockedOrder () { 70 | 71 | // Check for previously locked order 72 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/user-get-locked-order`, { 73 | method: "POST", 74 | headers: { 75 | "Content-Type": "application/json", 76 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 77 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 78 | }, 79 | body: JSON.stringify({ 80 | user_id: user.ID 81 | }) 82 | }) 83 | 84 | 85 | if (!res.ok) { 86 | const errorRes = await res.json(); 87 | const errorMessage = errorRes.error; 88 | setFormError(errorMessage) 89 | setFormLoading(false); 90 | return 91 | } 92 | 93 | // Parse Data 94 | let lockedOrder = await res.json(); 95 | 96 | // If order is already locked, navigate there 97 | if (lockedOrder && lockedOrder.ID) { 98 | navigate(`/order?order_id=${lockedOrder.ID}`); 99 | } 100 | 101 | // Turn loading off 102 | setFormLoading(false); 103 | } 104 | 105 | // Lock order and then go to order page 106 | async function lockOrder(orderID){ 107 | 108 | // clear form error 109 | setFormError("") 110 | 111 | // set form loading 112 | setFormLoading(true); 113 | 114 | // Data validation 115 | if (!orderID) { 116 | setFormError("Order Number is a required field and must be a whole number.") 117 | setFormLoading(false); 118 | return 119 | } 120 | 121 | // Lock Order 122 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/lock-order`, { 123 | method: "POST", 124 | headers: { 125 | "Content-Type": "application/json", 126 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 127 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 128 | }, 129 | body: JSON.stringify({ 130 | order_search_value: orderID.toString(), 131 | user_id: user.ID 132 | }) 133 | }) 134 | 135 | if (!res.ok) { 136 | const errorRes = await res.json(); 137 | const errorMessage = errorRes.error; 138 | setFormError(errorMessage) 139 | setFormLoading(false); 140 | return 141 | } 142 | 143 | navigate(`/order?order_id=${orderID}`) 144 | 145 | } 146 | 147 | return ( 148 |
    149 |

    Scan a tote

    150 |

    or

    151 | { inputRef.current.focus() }} trigger={} position="center"> 152 | {close => ( 153 |
    154 |
    155 | 156 |
    157 |
    158 |
    159 |

    Enter an order

    160 | 163 |
    164 |
    { 165 | event.preventDefault(); 166 | lockOrder(event.target.order_id.value); 167 | }}> 168 |
    {formError}
    169 |
    170 | 171 | 172 |
    173 |
    174 |
    175 | Cancel 176 |
    177 | 178 |
    179 |
    180 |
    181 |
    182 | )} 183 |
    184 | 185 |
    186 |
    187 | ) 188 | } 189 | 190 | export default ScanATotePage -------------------------------------------------------------------------------- /src/pages/order.jsx: -------------------------------------------------------------------------------- 1 | // React imports 2 | import React from 'react' 3 | import { useSearchParams, useNavigate } from "react-router-dom"; 4 | import { useEffect, useState, useRef } from 'react'; 5 | 6 | // Component Import 7 | import Dropdown from "../components/Dropdown" 8 | import Card from "../components/Card" 9 | import Error from '../components/Error'; 10 | import Loading from '../components/Loading'; 11 | import Popup from 'reactjs-popup'; 12 | 13 | // Img imports 14 | import placeholderImage from "../images/placeholderItemImage.png" 15 | import loader from "../images/loading.gif" 16 | 17 | // Electron imports 18 | const { ipcRenderer, shell } = window.require("electron"); 19 | 20 | 21 | const OrderPage = ({user}) => { 22 | 23 | // Init Navigate Function 24 | const navigate = useNavigate(); 25 | 26 | // Component States 27 | const [order, setOrder] = useState(null); 28 | const [orderError, setOrderError] = useState(""); 29 | const [loading, setLoading] = useState(true); 30 | const [orderWeight, setOrderWeight] = useState(0); 31 | const [orderBoxID, setOrderBoxID] = useState(null); 32 | const [orderItemsPackedStatus, setOrderItemsPackedStatus] = useState([]); 33 | const [printers, setPrinters] = useState([]); 34 | const [selectedPrinter, setSelectedPrinter] = useState(""); 35 | let barcodeScan = ""; 36 | 37 | // Shipping To Form States 38 | const [shipToFormLoading, setShipToFormLoading] = useState(false); 39 | const [shippingFormError, setShippingFormError] = useState(""); 40 | 41 | // Shipping Rates Popup States 42 | const [shippingRates, setShippingRates] = useState(null); 43 | const [shippingRatesError, setShippingRatesError] = useState(""); 44 | const [selectedRate, setSelectedRate] = useState(""); 45 | const [shippingRatesPopupLoading, setShippingRatesPopupLoading] = useState(false); 46 | const [shippingRatesPopupOpen, setShippingRatesPopupOpen] = useState(false); 47 | 48 | // Shipments States 49 | const [shipmentsLoading, setShipmentsLoading] = useState(false); 50 | const [shipments, setShipments] = useState(null); 51 | 52 | // Flag for error state 53 | const [flaggedError, setFlaggedError] = useState(""); 54 | 55 | // Use Search Params 56 | const [searchParams] = useSearchParams(); 57 | 58 | // Refs 59 | const weightInputRef = useRef(); 60 | const boxContainerRef = useRef(); 61 | 62 | // useEffect Hooks for page 63 | 64 | // Get Initial Data On Page Load Function 65 | useEffect(() => { 66 | 67 | 68 | 69 | // Get Initial Data On Page Load Function 70 | async function getInitialPageData () { 71 | 72 | // Set Page To Loading 73 | setLoading(true); 74 | 75 | // Request all available printers 76 | fetchPrinters(); 77 | 78 | // FETCH DATA 79 | 80 | /* 81 | I'm fetching first, then setting the data here 82 | as a way to make multiple requests at once and speed up the requests 83 | */ 84 | 85 | // Get Order ID 86 | const orderID = searchParams.get("order_id") 87 | 88 | // Fetch Order 89 | const fetchedOrder = await fetchOrder(orderID) 90 | // Fetch Shipments 91 | const fetchedShipments = await fetchShipments(fetchedOrder.ID); 92 | 93 | 94 | // Set Order into state 95 | if (!fetchedOrder && !orderError) { 96 | setOrderError("An error has occured while fetching the order. Try again later.") 97 | return; 98 | } 99 | setOrder(fetchedOrder); 100 | 101 | 102 | // Set shipments into state 103 | setShipments(fetchedShipments); 104 | 105 | // Weight Validation 106 | if (!fetchedOrder.EstimatedWeight || fetchedOrder.EstimatedWeight === "0") { 107 | setOrderError("This order doesn't have a weight. Please set in the error bin, and notify Brian.") 108 | return 109 | } 110 | 111 | // SET DATA INTO STATES AFTER FETCHING 112 | 113 | 114 | // Set Order Weight 115 | setOrderWeight(fetchedOrder.EstimatedWeight); 116 | 117 | 118 | // Set Order items Packed Status 119 | if (orderItemsPackedStatus.length < 1) { 120 | const initialOrderItemsPackedStatus = []; 121 | let orderItemScanned = false; 122 | for (let i = 0; i < fetchedOrder.Items.length; i++) { 123 | 124 | // set packed true if order Item ID was scanned 125 | if (fetchedOrder.Items[i].ID.toString() === orderID && !orderItemScanned) { 126 | 127 | 128 | initialOrderItemsPackedStatus.push({ 129 | index: `${i}`, 130 | id: fetchedOrder.Items[i].ID, 131 | packed: true 132 | }) 133 | 134 | for (let j = 0; j < parseInt(fetchedOrder.Items[i].Quantity) - 1; j++) { 135 | initialOrderItemsPackedStatus.push({ 136 | index: `${i}-${j}`, 137 | id: fetchedOrder.Items[i].ID, 138 | packed: false 139 | }) 140 | } 141 | 142 | orderItemScanned = true; 143 | 144 | continue; 145 | } 146 | 147 | for (let j = 0; j < parseInt(fetchedOrder.Items[i].Quantity); j++) { 148 | initialOrderItemsPackedStatus.push({ 149 | index: `${i}-${j}`, 150 | id: fetchedOrder.Items[i].ID, 151 | packed: false 152 | }) 153 | } 154 | } 155 | 156 | setOrderItemsPackedStatus(initialOrderItemsPackedStatus) 157 | } 158 | 159 | // Set Order Box ID 160 | if (!orderBoxID) { 161 | for (let i = 0; i < fetchedOrder.Boxes.length; i++) { 162 | if (fetchedOrder.Boxes[i].SuggestedBox) { 163 | setOrderBoxID(fetchedOrder.Boxes[i].ID); 164 | break; 165 | } 166 | } 167 | } 168 | 169 | // Turn off loading screen 170 | setLoading(false); 171 | 172 | } 173 | 174 | // Run Function 175 | getInitialPageData(); 176 | 177 | }, []); 178 | 179 | // Listens for and handles barcode scan 180 | useEffect(() => { 181 | 182 | // Handles Keydown 183 | function handleKeyDown (e) { 184 | 185 | // If keyCode is 13 (enter) then check if there are barcode scan keys and if there are handle barcode scan 186 | if (e.keyCode === 13 && barcodeScan.length > 3) { 187 | handleBarcodeScan(barcodeScan, orderItemsPackedStatus) 188 | return 189 | } 190 | 191 | // Skip if pressed key is shift key 192 | if (e.keyCode === 16) { 193 | return 194 | } 195 | 196 | // Push Keycode to barcode scan variable 197 | barcodeScan += e.key; 198 | 199 | // Set Timeout to clear state 200 | setTimeout(() => { 201 | barcodeScan = "" 202 | }, 110) 203 | 204 | } 205 | 206 | // Adds event listener to page for keydown 207 | document.addEventListener('keydown', handleKeyDown) 208 | 209 | // Don't forget to clean up 210 | return function cleanup() { 211 | document.removeEventListener('keydown', handleKeyDown); 212 | } 213 | }, [orderWeight, orderItemsPackedStatus, orderBoxID, selectedPrinter, shipments]) 214 | 215 | 216 | // API CALL FUNCTIONS 217 | 218 | // Fetches an order by Order ID 219 | const fetchOrder = async (orderID) => { 220 | 221 | // Error Handle validation 222 | if (!orderID) { 223 | setOrderError("You cannot fetch an order without the parameter orderID.") 224 | return; 225 | } 226 | 227 | // Fetch Order 228 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/order/${orderID}`, { 229 | headers: { 230 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 231 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 232 | } 233 | }) 234 | 235 | // Error handle response 236 | if (!res.ok) { 237 | const errorRes = await res.json(); 238 | const errorMessage = errorRes.error; 239 | setOrderError(errorMessage) 240 | 241 | // unlock order 242 | postUnlockOrder(orderID) 243 | 244 | return 245 | } 246 | 247 | 248 | // Parse data 249 | const orderData = await res.json(); 250 | 251 | // Return Data 252 | return orderData; 253 | 254 | } 255 | 256 | // Fetches Shipments By Order ID 257 | const fetchShipments = async (orderID) => { 258 | 259 | // Validate orderID parameter 260 | if (!orderID) { 261 | setOrderError("Unable to fetch shipments without an orderID parameter"); 262 | return; 263 | } 264 | 265 | // Fetch Shipments from HQ 266 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/order/${orderID}/shipments`, { 267 | headers: { 268 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 269 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 270 | } 271 | }) 272 | 273 | // Handle Errors in response 274 | if (!res.ok) { 275 | const errorRes = await res.json(); 276 | const errorMessage = errorRes.error; 277 | setOrderError(errorMessage) 278 | return 279 | } 280 | 281 | // return Shipments 282 | const fetchedShipments = await res.json() 283 | return fetchedShipments; 284 | 285 | } 286 | 287 | // Fetches Printers from electron 288 | const fetchPrinters = () => { 289 | 290 | // Send Request for printers 291 | ipcRenderer.send("get-printer-list"); 292 | 293 | // Set listener for receiving printer list 294 | ipcRenderer.on("get-printer-list-reply", (event, printerList) => { 295 | 296 | // Set the printer list 297 | setPrinters(printerList) 298 | 299 | // Set default printer as selected for first time fetching printers 300 | if (!selectedPrinter) { 301 | for (let i = 0; i < printerList.length; i++) { 302 | if (printerList[i].isDefault) { 303 | setSelectedPrinter(printerList[i]) 304 | } 305 | } 306 | } 307 | 308 | }) 309 | 310 | return; 311 | 312 | } 313 | 314 | // Fetches Shipping Rates Based on Current Info 315 | const fetchShippingRates = async (orderID, orderWeightToFetch, orderBoxIDToFetch) => { 316 | 317 | // Validate Params 318 | 319 | // orderID 320 | if (!orderID) { 321 | alert("Unable to fetch shipping rates without orderID parameter."); 322 | return; 323 | } 324 | 325 | // orderWeightToFetch 326 | if (!orderWeightToFetch) { 327 | alert("Unable to fetch shipping rates without orderWeightToFetch parameter."); 328 | return; 329 | } 330 | 331 | // orderBoxIDToFetch 332 | if (!orderBoxIDToFetch) { 333 | alert("Unable to fetch shipping rates without orderBoxIDToFetch parameter."); 334 | return; 335 | } 336 | 337 | // Fetch Shipping Rates 338 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/get-rates`, { 339 | method: "POST", 340 | headers: { 341 | "Content-Type": "application/json", 342 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 343 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 344 | }, 345 | body: JSON.stringify({ 346 | order_id: orderID, 347 | weight: parseFloat(orderWeightToFetch), 348 | box_id: parseInt(orderBoxIDToFetch) 349 | }) 350 | }) 351 | 352 | // Handle Response Error 353 | if (!res.ok) { 354 | const errorRes = await res.json(); 355 | const errorMessage = errorRes.error; 356 | setShippingRatesError(errorMessage) 357 | return 358 | } 359 | 360 | // Parse and Return Shipping Rates 361 | const fetchedShippingRatesRes = await res.json() 362 | return fetchedShippingRatesRes.Rates; 363 | } 364 | 365 | // Makes request to HQ to unlock order 366 | const postUnlockOrder = async (orderID) => { 367 | 368 | // Order ID parameter validation 369 | if (!orderID) { 370 | setOrderError("Unable to unlock order without orderID parameter."); 371 | return; 372 | } 373 | 374 | // Unlock Order Request to HQ 375 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/unlock-order`, { 376 | method: "POST", 377 | headers: { 378 | "Content-Type": "application/json", 379 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 380 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 381 | }, 382 | body: JSON.stringify({ 383 | order_id: parseInt(orderID), 384 | user_id: user.ID 385 | }) 386 | }) 387 | 388 | // Error Handle in response 389 | if (!res.ok) { 390 | const errorRes = await res.json(); 391 | const errorMessage = errorRes.error; 392 | setOrderError(errorMessage) 393 | return 394 | } 395 | 396 | } 397 | 398 | // Fetches an existing label based on a shipment ID 399 | const fetchExistingLabel = async (shipmentID) => { 400 | 401 | // Parameter Validation 402 | if (!shipmentID) { 403 | setOrderError("Unable to view fetch an existing label without a shipmentID"); 404 | return; 405 | } 406 | 407 | // Fetch Label 408 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/view-label/${shipmentID}`, { 409 | headers: { 410 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 411 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 412 | } 413 | }) 414 | 415 | // Handle response error 416 | if (!res.ok) { 417 | const errorRes = await res.json(); 418 | const errorMessage = errorRes.error; 419 | setOrderError(errorMessage) 420 | return 421 | } 422 | 423 | const labelRes = await res.json() 424 | return labelRes.URL; 425 | 426 | } 427 | 428 | // Makes Request to HQ to update shipping address 429 | const postUpdateShippingAddress = async (newAddressData) => { 430 | 431 | // Make Request to change address 432 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/edit-address`, { 433 | method: "POST", 434 | headers: { 435 | "Content-Type": "application/json", 436 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 437 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 438 | }, 439 | body: JSON.stringify(newAddressData) 440 | }); 441 | 442 | // Error Handle 443 | if (!res.ok) { 444 | const errorRes = await res.json(); 445 | const errorMessage = errorRes.error; 446 | setShippingFormError(errorMessage) 447 | return 448 | } 449 | 450 | } 451 | 452 | // Makes Request to update Carrier Method 453 | const postUpdateCarrierMethod = async (orderID, selectedCarrier, selectedMethod) => { 454 | 455 | // Validate Parameters 456 | 457 | // orderID 458 | if (!orderID) { 459 | setShippingRatesError("Unable to update carrier method without orderID parameter."); 460 | return; 461 | } 462 | 463 | // selectedCarrier 464 | if (!selectedCarrier) { 465 | setShippingRatesError("Unable to update carrier method without selectedCarrier parameter."); 466 | return; 467 | } 468 | 469 | // selectedMethod 470 | if (!selectedMethod) { 471 | setShippingRatesError("Unable to update carrier method without selectedMethod parameter."); 472 | return; 473 | } 474 | 475 | // Make Request to Update Carrier Method 476 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/update-carrier-method`, { 477 | method: 'POST', 478 | headers: { 479 | "Content-Type": "application/json", 480 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 481 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 482 | }, 483 | body: JSON.stringify({ 484 | order_id: orderID, 485 | carrier: selectedCarrier, 486 | method: selectedMethod 487 | }) 488 | }) 489 | 490 | // Error Handle Response 491 | if (!res.ok) { 492 | const errorRes = await res.json(); 493 | const errorMessage = errorRes.error; 494 | setShippingRatesError(errorMessage) 495 | return 496 | } 497 | 498 | 499 | } 500 | 501 | // Makes Request To Void Label by ShipmentID 502 | const postVoidLabel = async (shipmentID) => { 503 | 504 | // Parameter Validation 505 | if (!shipmentID) { 506 | alert("Unable to void label without parameter shipmentID"); 507 | return; 508 | } 509 | 510 | // Make request to void label 511 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/get-refund`, { 512 | method: "POST", 513 | headers: { 514 | "Content-Type": "application/json", 515 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 516 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 517 | }, 518 | body: JSON.stringify({ 519 | shipment_id: shipmentID 520 | }) 521 | }) 522 | 523 | // Handle Errors in response 524 | if (!res.ok) { 525 | const errorRes = await res.json(); 526 | const errorMessage = errorRes.error; 527 | setOrderError(errorMessage) 528 | } 529 | 530 | } 531 | 532 | // Makes Request to purchase new label 533 | const postPurchaseLabel = async (orderID, weight, boxID, userID) => { 534 | 535 | // Parameter Validation 536 | 537 | // orderID 538 | if (!orderID) { 539 | alert("Unable to purchase label without orderID parameter."); 540 | return; 541 | } 542 | 543 | // weight 544 | if (!weight) { 545 | alert("Unable to purchase label without weight parameter."); 546 | return; 547 | } 548 | 549 | // boxID 550 | if (!boxID) { 551 | alert("Unable to purchase label without boxID parameter."); 552 | return; 553 | } 554 | 555 | // userID 556 | if (!userID) { 557 | alert("Unable to purchase label without userID parameter."); 558 | return; 559 | } 560 | 561 | // Make Request to purchase label 562 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/get-label`, { 563 | method: 'POST', 564 | headers: { 565 | "Content-Type": "application/json", 566 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 567 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 568 | }, 569 | body: JSON.stringify({ 570 | order_id: parseInt(orderID), 571 | weight: parseFloat(weight), 572 | box_id: parseInt(boxID), 573 | user_id: parseInt(userID) 574 | }) 575 | }) 576 | 577 | // Error Handle Response 578 | if (!res.ok) { 579 | const errorRes = await res.json(); 580 | const errorMessage = errorRes.error; 581 | setOrderError(errorMessage) 582 | return 583 | } 584 | 585 | 586 | // Parse and return Label Response 587 | const labelRes = await res.json() 588 | return labelRes.label_url; 589 | 590 | } 591 | 592 | // Make Request to flag for error 593 | const postFlagError = async (orderID, errorMessage) => { 594 | 595 | // Parameter Validation 596 | if (!orderID) { 597 | alert("Unable to flag error without orderID parameter."); 598 | return; 599 | } 600 | 601 | if (!errorMessage) { 602 | alert("Unable to flag error without errorMessage parameter."); 603 | return; 604 | } 605 | 606 | // Make Request to flag error 607 | const res = await fetch(`${process.env.REACT_APP_HQ_SHIPPING_ADDRESS}/report-error`, { 608 | method: 'POST', 609 | headers: { 610 | "Content-Type": "application/json", 611 | "X-API": process.env.REACT_APP_HQ_SHIPPING_X_API_KEY, 612 | "X-Real-IP": process.env.REACT_APP_HQ_SHIPPING_X_REAL_IP 613 | }, 614 | body: JSON.stringify({ 615 | order_id: parseInt(orderID), 616 | user_id: parseInt(user.ID), 617 | error_note: errorMessage 618 | }) 619 | 620 | }) 621 | 622 | // Error Handle Response 623 | if (!res.ok) { 624 | const errorRes = await res.json(); 625 | const errorMessage = errorRes.error; 626 | setOrderError(errorMessage) 627 | return 628 | } 629 | 630 | } 631 | 632 | // ACTION HANDLE FUNCTIONS 633 | 634 | 635 | // Selects Box 636 | const handleBoxSelect = (event) => { 637 | 638 | // Set new Box 639 | setOrderBoxID(event.target.value); 640 | 641 | // Scroll to top of box container 642 | boxContainerRef.current.scrollTop = 0; 643 | 644 | } 645 | 646 | // Print Label 647 | const handlePrintLabel = async () => { 648 | 649 | // Loop through shipments -- if one is not voided, ship that 650 | if (shipments) { 651 | for (let i = 0; i < shipments.length; i++) { 652 | if (!shipments[i].Voided) { 653 | 654 | // If Shipment isn't voided fetch label and print 655 | const existingLabelURL = await fetchExistingLabel(shipments[i].ID); 656 | 657 | // print label if it exists 658 | if (existingLabelURL) { 659 | ipcRenderer.send("print-label", JSON.stringify({URL: existingLabelURL, printerName: selectedPrinter.name})) 660 | } 661 | 662 | // return so other label doesn't get printed 663 | return; 664 | 665 | } 666 | } 667 | } 668 | 669 | // If all existing shipments are voided, purchase label (which creates shipment), print label, and update shipments 670 | 671 | // Set shipments loading 672 | setLoading(true); 673 | 674 | // Purchase Label 675 | const newLabelURL = await postPurchaseLabel(order.ID, orderWeight, orderBoxID, user.ID); 676 | 677 | // print label if it exists 678 | if (newLabelURL) { 679 | ipcRenderer.send("print-label", JSON.stringify({URL: newLabelURL, printerName: selectedPrinter.name})) 680 | } 681 | 682 | // Update Shipments 683 | const fetchedShipments = await fetchShipments(order.ID); 684 | setShipments(fetchedShipments) 685 | 686 | // Set shipments to not loading 687 | setLoading(false); 688 | } 689 | 690 | // Unlocks order when user goes back to tote page 691 | const handleBackToTotePageClick = async () => { 692 | 693 | // Set Screen Loading 694 | setLoading(true); 695 | 696 | // Unlock Order 697 | if (order && order.ID) { 698 | await postUnlockOrder(order.ID); 699 | } 700 | 701 | // Navigate back to tote page 702 | navigate("/scan") 703 | 704 | // Turn of loading in case 705 | setLoading(false); 706 | 707 | } 708 | 709 | // Updates Shipping Info 710 | const handleSaveShipToSubmit = async (event, closePopup) => { 711 | 712 | // Prevent Default Submit action 713 | event.preventDefault(); 714 | 715 | // Clear shipping form error 716 | setShippingFormError("") 717 | 718 | // Set Shipping Form Loading 719 | setShipToFormLoading(true) 720 | 721 | // Get Data and create post data 722 | const newAddressData = { 723 | order_id: order.ID, 724 | name: event.target.name.value, 725 | company: event.target.company.value, 726 | street1: event.target.street1.value, 727 | street2: event.target.street2.value, 728 | city: event.target.city.value, 729 | state: event.target.state.value, 730 | postal_code: event.target.zip.value, 731 | country: event.target.country.value, 732 | phone: event.target.phone.value 733 | } 734 | 735 | // Make call to update ship to in HQ 736 | await postUpdateShippingAddress(newAddressData); 737 | 738 | // Set ShipTo Address and order 739 | const newOrder = order; 740 | newOrder.ShipTo = { 741 | Name: newAddressData.name, 742 | Company: newAddressData.company, 743 | Street1: newAddressData.street1, 744 | Street2: newAddressData.street2, 745 | City: newAddressData.city, 746 | State: newAddressData.state, 747 | PostalCode: newAddressData.postal_code, 748 | Country: newAddressData.country, 749 | Phone: newAddressData.phone 750 | }; 751 | setOrder(newOrder); 752 | 753 | // Turn off loading 754 | setShipToFormLoading(false) 755 | 756 | // Close popup 757 | closePopup(); 758 | 759 | } 760 | 761 | // Edits Weight 762 | const handleEditWeightSubmit = (event, closePopup) => { 763 | 764 | // Prevent Default Action 765 | event.preventDefault(); 766 | 767 | // Set new order weight 768 | setOrderWeight(parseFloat(event.target.weight.value)) 769 | 770 | // Closes Popup 771 | closePopup(); 772 | 773 | } 774 | 775 | // Pack Item 776 | const handlePackItem = (orderItemID) => { 777 | 778 | //set new array to work with 779 | let updatedOrderItemPackedStatus = (orderItemsPackedStatus) 780 | 781 | // Find index of item in array to update 782 | let indexToUpdate = updatedOrderItemPackedStatus.findIndex((item) => (item.id === orderItemID && item.packed === false)) 783 | updatedOrderItemPackedStatus[indexToUpdate].packed = true; 784 | 785 | // Set array 786 | setOrderItemsPackedStatus([...updatedOrderItemPackedStatus]); 787 | 788 | } 789 | 790 | // Unpack Item 791 | const handleUnpackItem = (orderItemID) => { 792 | 793 | //set new array to work with 794 | let updatedOrderItemPackedStatus = (orderItemsPackedStatus) 795 | 796 | // Find index of item in array to update 797 | let indexToUpdate = updatedOrderItemPackedStatus.findIndex((item) => (item.id === orderItemID && item.packed === true)) 798 | updatedOrderItemPackedStatus[indexToUpdate].packed = false; 799 | 800 | // Set array 801 | setOrderItemsPackedStatus([...updatedOrderItemPackedStatus]); 802 | 803 | } 804 | 805 | // Populates Shipping Rates Popup 806 | const handlePopulateShippingRates = async () => { 807 | 808 | // Validation 809 | if (!orderWeight) { 810 | alert("Order Weight is required to get shipping rates") 811 | setShippingRatesPopupOpen(false) 812 | return 813 | } 814 | 815 | if (!orderBoxID) { 816 | alert("Box is required to get shipping rates") 817 | setShippingRatesPopupOpen(false) 818 | return 819 | } 820 | 821 | 822 | setShippingRatesPopupOpen(true) 823 | 824 | // set loading to true 825 | setShippingRatesPopupLoading(true) 826 | 827 | // Fetch Shipping Rates 828 | const fetchedShippingRates = await fetchShippingRates(order.ID, orderWeight, orderBoxID); 829 | 830 | 831 | // Parse Shipping Rates into object 832 | const shippingRatesParsed = {} 833 | 834 | // Sort Rates into provider 835 | for (let i = 0; i < fetchedShippingRates.length; i++) { 836 | 837 | // Create Provider if it doesn't exist 838 | if (!shippingRatesParsed[fetchedShippingRates[i].provider]) { 839 | shippingRatesParsed[fetchedShippingRates[i].provider] = []; 840 | } 841 | 842 | // Push Rate to provider 843 | shippingRatesParsed[fetchedShippingRates[i].provider].push(fetchedShippingRates[i]); 844 | } 845 | 846 | // Set Shipping Rates State 847 | setShippingRates(shippingRatesParsed); 848 | 849 | // Turn off loading screen 850 | setShippingRatesPopupLoading(false); 851 | } 852 | 853 | // Handles a barcode scan 854 | const handleBarcodeScan = async (scannedCode, currentOrderItemsPackedStatus) => { 855 | 856 | // handle if barcode scan is bx_ 857 | if (scannedCode.startsWith("bx_")) { 858 | 859 | // Get Box ID 860 | const boxID = scannedCode.split("_")[1]; 861 | 862 | setOrderBoxID(boxID); 863 | 864 | // Scroll to top of box container 865 | boxContainerRef.current.scrollTop = 0; 866 | 867 | return; 868 | } 869 | 870 | // handle if barcode scan is hs_ 871 | if (scannedCode.startsWith("hs_")) { 872 | 873 | // get scan code 874 | const scanCode = scannedCode.split("_")[1]; 875 | 876 | if (scanCode === "000001") { 877 | 878 | // validate order weight 879 | if (!orderWeight || orderWeight === "0") { 880 | // alert order weight is required to print label 881 | alert("Order Weight is required to print label"); 882 | return; 883 | } 884 | 885 | // validate selected printer 886 | if (!selectedPrinter.name) { 887 | // alert printer is required to print label 888 | alert("Selected Printer is required to print label"); 889 | return; 890 | } 891 | 892 | // validate order items packed status 893 | if ((orderItemsPackedStatus.filter(item => !item.packed)).length) { 894 | // alert order items are required to be packed to print label 895 | alert("Order Items are required to be packed to print label"); 896 | return; 897 | } 898 | 899 | // print label 900 | handlePrintLabel(); 901 | 902 | } 903 | 904 | if (scanCode === "000002") { 905 | 906 | // validate order item packed status 907 | if ((orderItemsPackedStatus.filter(item => !item.packed)).length) { 908 | // alert order items are required to be packed to complete order 909 | alert("Order Items are required to be packed to complete order"); 910 | return; 911 | } 912 | 913 | // validate shipments 914 | if (!shipments || (!shipments.filter(shipment => !shipment.Voided).length)) { 915 | // alert shipments are required to complete order 916 | alert("at least 1 non-Voided shipment is required to complete order"); 917 | return; 918 | } 919 | 920 | handleCompleteOrder(); 921 | 922 | } 923 | 924 | } 925 | 926 | // handle if barcode scan is order item 927 | for (let i = 0; i < currentOrderItemsPackedStatus.length; i++) { 928 | 929 | if (currentOrderItemsPackedStatus[i].id.toString() !== scannedCode) { 930 | continue; 931 | } 932 | 933 | if(currentOrderItemsPackedStatus[i].packed) { 934 | // handleUnpackItem(currentOrderItemsPackedStatus[i].id) 935 | } else { 936 | handlePackItem(currentOrderItemsPackedStatus[i].id) 937 | } 938 | } 939 | 940 | } 941 | 942 | // Handles click on rate selection 943 | const handleRateClick = (rateSelected) => { 944 | setSelectedRate(rateSelected); 945 | } 946 | 947 | // Updates Carrier Method On Save inside Shipping Method popup 948 | const handleUpdateCarrierMethodSubmit = async (closePopup) => { 949 | 950 | // Clear Error Message 951 | setShippingRatesError(""); 952 | 953 | // Set to loading 954 | setShippingRatesPopupLoading(true) 955 | 956 | // Make call to update carrier method 957 | await postUpdateCarrierMethod(order.ID, selectedRate.carrier, selectedRate.method); 958 | 959 | // Re-Fetch updated order and set it in state so that shipping method aligns 960 | const updatedOrder = await fetchOrder(order.ID); 961 | setOrder(updatedOrder); 962 | 963 | // Turn off loading 964 | setShippingRatesPopupLoading(false); 965 | 966 | // Close popup 967 | closePopup(); 968 | 969 | } 970 | 971 | // Handles Voids a label click on shipment 972 | const handleVoidLabel = async (shipmentID) => { 973 | 974 | // set loading to true 975 | setShipmentsLoading(true) 976 | 977 | // Void Label 978 | await postVoidLabel(shipmentID); 979 | 980 | // Re-Fetch shipments and set to state to update shipments 981 | const updatedShipments = await fetchShipments(order.ID); 982 | setShipments(updatedShipments) 983 | 984 | // Turn off loading screen 985 | setShipmentsLoading(false) 986 | 987 | } 988 | 989 | // Handles View Label Button Click 990 | const handleViewLabelClick = async (shipmentID) => { 991 | 992 | // set loading to true 993 | setShipmentsLoading(true) 994 | 995 | // Fetch label 996 | const existingLabelURL = await fetchExistingLabel(shipmentID); 997 | 998 | // Open window with label if it exists 999 | if (existingLabelURL) { 1000 | window.open(existingLabelURL) 1001 | } 1002 | 1003 | // Turn off loading screen 1004 | setShipmentsLoading(false) 1005 | 1006 | } 1007 | 1008 | // Handles Complete Order (Just unlocks order) 1009 | const handleCompleteOrder = async () => { 1010 | 1011 | // Set Screen Loading 1012 | setLoading(true); 1013 | 1014 | // Unlock Order 1015 | await postUnlockOrder(order.ID); 1016 | 1017 | // Navigate back to tote page 1018 | navigate("/scan") 1019 | 1020 | // Turn of loading in case 1021 | setLoading(false); 1022 | 1023 | } 1024 | 1025 | // Handles Flag Order for error 1026 | const handleFlagForError = async (orderID, flaggedError) => { 1027 | 1028 | // Set Screen Loading 1029 | setLoading(true); 1030 | 1031 | // Flag Order 1032 | await postFlagError(orderID, flaggedError); 1033 | 1034 | // unlock order 1035 | await postUnlockOrder(order.ID); 1036 | 1037 | // Navigate back to tote page 1038 | navigate("/scan") 1039 | 1040 | // Turn of loading in case 1041 | setLoading(false); 1042 | 1043 | 1044 | } 1045 | 1046 | 1047 | 1048 | 1049 | // RENDER PROPER PAGE 1050 | 1051 | // Show order error screen 1052 | if (orderError) { 1053 | return ( 1054 | 1055 | ) 1056 | } 1057 | 1058 | // Show Loading Screen 1059 | if (loading) { 1060 | return ( 1061 | 1062 | ) 1063 | } 1064 | 1065 | 1066 | // Show Order Page 1067 | return ( 1068 |
    1069 |
    1070 |

    Go back to Tote page

    1071 |
    1072 |

    Order #{order.Number}

    1073 |
    1074 |
    {shell.openExternal( `https://whsrv.hoopswagg.com/hq/orders/view/${order.ID}` )}} className="text-white rounded font-light bg-blue-500 hover:bg-blue-400 py-2 px-3 flex items-center justify-center transition cursor-pointer">View in Dashboard
    1075 | 1076 | { 1077 | printers.map(printer => ( 1078 |
  • 1079 | 1080 |
  • 1081 | )) 1082 | } 1083 |
    1084 |
    1085 |
    1086 |
    1087 |
    1088 | 1089 |
    1090 | 1091 | {/* Non-Packed Order Items Card */} 1092 | 1093 |
    1094 |

    Order Items

    1095 |

    {(orderItemsPackedStatus.filter(item => !item.packed)).length} Items

    1096 |
    1097 |
    1098 |

    Product

    1099 |
    1100 |
    1101 | { 1102 | orderItemsPackedStatus.map(function(orderItemStatus){ 1103 | 1104 | // don't render if item is packed 1105 | if (orderItemStatus.packed) { 1106 | return <> 1107 | } 1108 | 1109 | // Get Item 1110 | const itemArray = order.Items.filter(orderItem => orderItem.ID === orderItemStatus.id); 1111 | const item = itemArray[0] 1112 | return ( 1113 |
    {handlePackItem(item.ID)}}> 1114 | 1115 |

    {item.RenderID}: {item.ProductName}

    1116 |
    1117 | ) 1118 | 1119 | }) 1120 | } 1121 |
    1122 |
    1123 | 1124 | {/* Packed Order Items Card */} 1125 | 1126 |
    1127 |

    Packed Items

    1128 |

    {(orderItemsPackedStatus.filter(item => item.packed)).length} Items

    1129 |
    1130 |
    1131 |

    Product

    1132 |
    1133 |
    1134 | { 1135 | orderItemsPackedStatus.map(function(orderItemStatus){ 1136 | 1137 | // Don't render if the item isn't packed 1138 | if (!orderItemStatus.packed) { 1139 | return <> 1140 | } 1141 | 1142 | // Get Item 1143 | const itemArray = order.Items.filter(orderItem => orderItem.ID === orderItemStatus.id); 1144 | const item = itemArray[0] 1145 | 1146 | return ( 1147 |
    1148 | 1149 |

    {item.RenderID}: {item.ProductName}

    1150 | 1151 |
    1152 | ) 1153 | 1154 | 1155 | }) 1156 | } 1157 |
    1158 |
    1159 |
    1160 | 1161 |
    1162 |
    1163 | 1164 | {/* Ship To Card */} 1165 | 1166 |
    1167 |

    Ship to

    1168 | Edit

    } position="center"> 1169 | {close => ( 1170 |
    1171 |
    1172 | 1173 |
    1174 |
    1175 |
    1176 |

    Shipping Information

    1177 | 1180 |
    1181 |
    handleSaveShipToSubmit(event, close)}> 1182 |
    {shippingFormError}
    1183 |
    1184 |
    1185 | 1186 | 1187 |
    1188 |
    1189 |
    1190 |
    1191 | 1192 | 1193 |
    1194 |
    1195 | 1196 | 1197 |
    1198 |
    1199 |
    1200 |
    1201 | 1202 | 1203 |
    1204 |
    1205 |
    1206 |
    1207 | 1208 | 1209 |
    1210 |
    1211 |
    1212 |
    1213 | 1214 | 1215 |
    1216 |
    1217 | 1218 | 1219 |
    1220 |
    1221 |
    1222 |
    1223 | 1224 | 1225 |
    1226 |
    1227 | 1228 | 1229 |
    1230 |
    1231 |
    1232 | 1235 | 1236 |
    1237 |
    1238 |
    1239 |
    1240 | )} 1241 |
    1242 |
    1243 |

    {order.ShipTo.Name}

    1244 |

    {order.ShipTo.Street1}

    1245 | {order.ShipTo.Street2 && (

    {order.ShipTo.Street2}

    )} 1246 | {order.ShipTo.Street3 && (

    {order.ShipTo.Street3}

    )} 1247 |

    {order.ShipTo.City}, {order.ShipTo.State}, {order.ShipTo.PostalCode}

    1248 |
    1249 | 1250 | {/* Shipping Method Card */} 1251 | 1252 |
    1253 |

    Shipping Method

    1254 |

    View Rates

    1255 | {setShippingRatesPopupOpen(false)}} position="center"> 1256 | {close => ( 1257 |
    1258 |
    1259 | 1260 |
    1261 | 1262 |
    1263 |

    Shipping Rates

    1264 | 1267 |
    1268 |
    {shippingRatesError}
    1269 | {shippingRates && ( 1270 |
    1271 | { 1272 | Object.entries(shippingRates).map((rates, key) => { 1273 | return ( 1274 | <> 1275 |
    1276 |

    {rates[0]}

    1277 |
    1278 | { 1279 | rates[1].map(rate => { 1280 | return ( 1281 |
    handleRateClick({object_id: rate.object_id, carrier: rate.provider, method: rate.servicelevel.token})} className={`w-full py-4 border-b border-gray-300 flex items-center justify-between hover:bg-blue-100 transition duration-300 px-2 ${selectedRate.object_id === rate.object_id ? "bg-blue-100" : "cursor-pointer"}`}> 1282 |
    1283 |
    {rate.servicelevel.name}
    1284 |
    1285 |

    ${rate.amount}

    1286 |
    1287 | ) 1288 | }) 1289 | } 1290 | 1291 | 1292 | ) 1293 | }) 1294 | } 1295 |
    1296 | )} 1297 |
    1298 |
    Cancel
    1299 |
    handleUpdateCarrierMethodSubmit(close)}>Save
    1300 |
    1301 | 1302 |
    1303 | )} 1304 |
    1305 |
    1306 |
    1307 |

    Carrier Method: {order.CarrierToken.toUpperCase()} / {order.MethodToken}

    1308 |

    Shipping Service: {order.RequestedShippingService}

    1309 |
    1310 |
    1311 | 1312 | {/* Internal Notes Card */} 1313 | { order.InternalNotes && ( 1314 | 1315 |

    Internal Notes

    1316 |

    1317 | {order.InternalNotes} 1318 |

    1319 |
    1320 | )} 1321 | 1322 | {/* Edit Weight Card */} 1323 | 1324 |
    1325 |

    Weight

    1326 | {weightInputRef.current.focus()}} trigger={

    Edit

    } position="center"> 1327 | {close => ( 1328 |
    1329 |
    1330 |
    1331 |

    Edit Weight

    1332 | 1335 |
    1336 |
    { handleEditWeightSubmit(event, close); }}> 1337 |
    1338 | 1339 | 1340 |
    1341 |
    1342 | 1345 | 1346 |
    1347 |
    1348 |
    1349 |
    1350 | )} 1351 |
    1352 |
    1353 |
    {orderWeight}oz
    1354 |
    1355 |
    1356 | 1357 | {/* Box Card */} 1358 | 1359 |
    1360 |

    Suggested Box

    1361 |
    1362 |
    1363 | { 1364 | order.Boxes.map(function(box){ 1365 | 1366 | if (orderBoxID) { 1367 | 1368 | if (box.ID !== parseInt(orderBoxID)) { 1369 | return <>; 1370 | } 1371 | 1372 | return ( 1373 | 1377 | ) 1378 | 1379 | } 1380 | 1381 | if (!box.SuggestedBox) { 1382 | return <> 1383 | } 1384 | 1385 | return ( 1386 | 1390 | ) 1391 | }) 1392 | } 1393 | { 1394 | order.Boxes.map(function(box, i){ 1395 | 1396 | if (box.ID === parseInt(orderBoxID)) { 1397 | return <> 1398 | } 1399 | 1400 | 1401 | if (box.SuggestedBox && !orderBoxID) { 1402 | return <> 1403 | } 1404 | 1405 | return ( 1406 | 1410 | ) 1411 | }) 1412 | } 1413 |
    1414 |
    1415 | 1416 | {/* Shipments Card */} 1417 | 1418 |
    1419 |

    Shipments

    1420 |
    1421 |
    1422 |
    1423 | 1424 |
    1425 | 1426 | 1427 | 1428 | 1429 | 1430 | 1431 | 1432 | 1433 | 1434 | { 1435 | shipments && shipments.map(shipment => { 1436 | return ( 1437 | 1438 | 1439 | 1440 | 1441 | 1442 | 1443 | 1455 | 1456 | ) 1457 | }) 1458 | } 1459 |
    CarrierCostTracking NumberServiceStatusActions
    {shipment.CarrierCode}{shipment.ShipmentCost}{shipment.TrackingNumber}{shipment.ServiceCode}
    {shipment.Voided ? "Voided" : "Shipped"}
    1444 | {!shipment.Voided && ( 1445 | 1446 |
  • 1447 | 1448 |
  • 1449 |
  • 1450 | 1451 |
  • 1452 |
    1453 | )} 1454 |
    1460 |
    1461 |
    1462 |
    1463 |
    1464 | 1465 | {/* Complete Order, Print Label, and Report Error Buttons */} 1466 |
    1467 | {setFlaggedError("")}} trigger={} modal position="center" > 1468 | {close => ( 1469 |
    1470 |

    Once order is flagged for error, please set it aside

    1471 | 1472 |
    1473 | 1474 | { setFlaggedError(event.target.value) }} type="text" id="reason" className="w-full p-2 border-2 border-gray-300 rounded-lg" /> 1475 |
    1476 | 1477 | 1478 |
    1479 | 1480 | 1481 |
    1482 | 1483 |
    1484 | )} 1485 |
    1486 | 1487 | 1488 |
    1489 |
    1490 | ) 1491 | } 1492 | 1493 | export default OrderPage --------------------------------------------------------------------------------