├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
1387 |
1388 | {box.Name}
1389 |
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 |
1407 |
1408 | {box.Name}
1409 |
1410 | )
1411 | })
1412 | }
1413 |
1414 |
1415 |
1416 | {/* Shipments Card */}
1417 |
1418 |
1419 |
Shipments
1420 |
1421 |
1422 |
1423 |

1424 |
1425 |
1426 |
1427 | | Carrier |
1428 | Cost |
1429 | Tracking Number |
1430 | Service |
1431 | Status |
1432 | Actions |
1433 |
1434 | {
1435 | shipments && shipments.map(shipment => {
1436 | return (
1437 |
1438 | | {shipment.CarrierCode} |
1439 | {shipment.ShipmentCost} |
1440 | {shipment.TrackingNumber} |
1441 | {shipment.ServiceCode} |
1442 | {shipment.Voided ? "Voided" : "Shipped"} |
1443 |
1444 | {!shipment.Voided && (
1445 |
1446 |
1447 |
1448 |
1449 |
1450 |
1451 |
1452 |
1453 | )}
1454 | |
1455 |
1456 | )
1457 | })
1458 | }
1459 |
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 | Reason flagged for error
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
--------------------------------------------------------------------------------