diff --git a/.gitignore b/.gitignore index d75edea..becda0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ venv -__pycache__ \ No newline at end of file +__pycache__ +node_modules \ No newline at end of file diff --git a/Makefile b/Makefile index c1b4ad6..a4b8778 100644 --- a/Makefile +++ b/Makefile @@ -6,15 +6,17 @@ COMPOSE=docker-compose -p $(PROJECT_NAME) check-docker: @docker info >nul 2>&1 || (echo. && echo ERROR: Docker is not running. Please start Docker Desktop and try again. && echo. && exit /b 1) -## up: Start the mysql, api (port 5000), and api-test (port 5001) containers and seed default locations +## up: Start mysql, api (5000), and api-test (5001). Waits briefly and verifies mysql_api is running. Seeding runs inside the API when src.api.app loads (seed_data). .PHONY: up up: check-docker $(COMPOSE) up -d @echo "Waiting for services to be ready..." @timeout /t 5 /nobreak >nul 2>&1 || sleep 5 2>/dev/null || true - @$(COMPOSE) exec api python src/scripts/seed_locations.py + @echo Checking API container status... + @docker inspect mysql_api --format "{{.State.Status}}" 2>nul >nul || (echo. && echo ======================================== && echo ERROR: API container 'mysql_api' does not exist! && echo ======================================== && echo. && echo Try running: make build && echo. && exit /b 1) + @docker inspect mysql_api --format "{{.State.Status}}" 2>nul | findstr /C:"running" >nul 2>&1 || (echo. && echo ======================================== && echo ERROR: API container is not running! && echo ======================================== && echo. && echo Container status: && docker inspect mysql_api --format "Status: {{.State.Status}} (Exit Code: {{.State.ExitCode}})" 2>nul && echo. && echo Container logs: && echo. && docker logs mysql_api 2>&1 && echo. && echo ======================================== && echo. && exit /b 1) -## up-empty: Start the mysql, api (port 5000), and api-test (port 5001) containers without seeding data +## up-empty: docker compose up -d only (no wait or API checks). Seeding still runs when each API process starts (same as up). .PHONY: up-empty up-empty: check-docker $(COMPOSE) up -d @@ -34,9 +36,9 @@ status: check-docker logs: check-docker $(COMPOSE) logs -f -## mysql: Open a mysql shell into the mysql container -.PHONY: mysql -mysql: check-docker +## mysql-shell: Open a mysql shell into the mysql container +.PHONY: mysql-shell +mysql-shell: check-docker $(COMPOSE) exec db mysql -u mysqluser -pmysqlpassword mydb ## build: Build or rebuild services @@ -57,17 +59,7 @@ install: sudo apt-get install -y docker.io docker-compose make python3-pip pip3 install -r requirements.txt -## locations: Generate inventory-locations.json from generate-locations.js -.PHONY: locations -locations: - @cd milventory && node generate-locations.js - ## milventory: Start the milventory React app .PHONY: milventory milventory: @cd milventory && npm install && npm start - -## help: Show this help menu -help: - @echo "Available commands:" - @grep -E '^##' Makefile | sed 's/## //' diff --git a/README.md b/README.md index 9ccc2d5..107309e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Inventory management system for MIL with database-backed storage. ```bash make up ``` - This starts MySQL and the Flask API, and seeds initial data. + This starts MySQL and the Flask API. Initial data is seeded when the API process loads (`src/scripts/seed_data.py` via `src.api.app`). 2. **Start frontend** (in a new terminal): ```bash @@ -32,23 +32,21 @@ Inventory management system for MIL with database-backed storage. - Email: `test@ufl.edu` - Password: `test` -## Available Commands +## Available Commands (Ordered by importance) -- `make up` - Start all services and seed data -- `make up-empty` - Start services without seeding -- `make down` - Stop services (keeps data) -- `make clean` - Stop services and remove all data +- `make up` - Start all backend services - `make milventory` - Start React frontend -- `make mysql` - Open MySQL shell +- `make down` - Stop backend services (keeps data) +- `make clean` - Stop backend services and remove all data - `make logs` - View service logs - `make status` - Check service status -- `make help` - Show all commands +- `make mysql-shell` - Open MySQL shell ## Project Structure - `milventory/` - React frontend application - `src/api/` - Flask backend API -- `src/sql/` - Database schema definitions +- `src/tables/` - Database schema definitions (`table_*.sql` per domain) - `src/scripts/` - Database seeding and utility scripts ## Development diff --git a/milventory/generate-locations.js b/milventory/generate-locations.js deleted file mode 100644 index 2eca059..0000000 --- a/milventory/generate-locations.js +++ /dev/null @@ -1,361 +0,0 @@ -// Script to generate inventory-locations.json -// Run with: node generate-locations.js > public/inventory-locations.json -// Or: node generate-locations.js (and redirect output manually) - -const fs = require('fs'); -const path = require('path'); - -// Inventory bounds configuration -const inventoryBounds = { - "viewBox": { - "x": 0, - "y": 0, - "width": 4000, - "height": 4000 - }, - "room": { - "x": 80, - "y": 80, - "width": 3600, - "height": 3840, - "rx": 18, - "ry": 18 - } -}; - -// Configuration constants -const drawerSize = 150; -const drawerSpacing = 5; -const topStartX = 720; -const topY = 80; - -// Top drawers A-K (11 drawers) -// Every 3rd drawer (C, F, I) is double width -const drawerLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K']; -const topDrawers = []; -let currentX = topStartX; - -drawerLabels.forEach((label, index) => { - const isThird = (index + 1) % 3 === 0; // C, F, I (indices 2, 5, 8) - const width = isThird ? drawerSize * 2 : drawerSize; - - topDrawers.push({ - title: `Drawer ${label}`, - x: currentX, - y: topY, - width: width, - height: drawerSize, - fill: 'var(--drawer)', - inventory: [] - }); - - // Move X position for next drawer - currentX += width + drawerSpacing; -}); - -// Top cabinets 1-4 (each is 2 drawers wide, positioned below drawers) -// 2 drawers of space between each cabinet -const cabinetWidth = drawerSize * 2 + drawerSpacing; -const cabinetHeight = drawerSize + drawerSpacing; -const cabinetSpacing = drawerSize * 2 + drawerSpacing*2; // 2 drawers of space between cabinets -const cabinetY = topY + drawerSize + 20; - -const topCabinets = []; -let cabinetX = topStartX; -for (let i = 0; i < 4; i++) { - topCabinets.push({ - title: `Cabinet ${i + 1}`, - x: cabinetX, - y: cabinetY, - width: cabinetWidth, - height: cabinetHeight, - fill: 'var(--table)', - inventory: [] - }); - // Move X position for next cabinet (cabinet width + 2 drawers of space) - cabinetX += cabinetWidth + cabinetSpacing; -} - -// Right side drawers L-AA -// Note: Drawer N is taller (height 205), which affects spacing -const rightX = 3500; -const rightDrawerLabels = ['L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA']; -const rightDrawers = []; -let rightYStart = 840; -let rightY = rightYStart; // Starting Y position - -rightDrawerLabels.forEach((label, index) => { - const height = label === 'N' ? 2*drawerSize + drawerSpacing : drawerSize; // Drawer N is taller - rightDrawers.push({ - title: `Drawer ${label}`, - x: rightX, - y: rightY, - width: drawerSize, - height: height, - fill: 'var(--drawer)', - inventory: [] - }); - // Move Y position for next drawer (accounting for drawer height and spacing) - rightY += height + drawerSpacing; -}); - -// Right side cabinets 5-12 (positioned to the left of drawers, aligned with drawer pairs) -const rightCabinetWidth = drawerSize + drawerSpacing; // 105 -const rightCabinetHeight = drawerSize * 2 + drawerSpacing; // 205 -const rightCabinetX = rightX - rightCabinetWidth - 20; // 1825 - -const rightCabinets = []; -// Cabinets align with drawer positions (every 2 drawers, starting with L-M) -let cabinetYPos = rightYStart; // Start aligned with Drawer L -for (let i = 0; i < 8; i++) { - rightCabinets.push({ - title: `Cabinet ${i + 5}`, - x: rightCabinetX, - y: cabinetYPos, - width: rightCabinetWidth, - height: rightCabinetHeight, - fill: 'var(--table)', - inventory: [] - }); - // Move to next cabinet position (every 2 drawers) - // Account for Drawer N being taller - if (i === 0) { - // After Cabinet 5 (L-M), skip to after N - cabinetYPos = rightYStart + drawerSize*2 + drawerSpacing*2; // After Drawer N - } else { - cabinetYPos += (drawerSize + drawerSpacing) * 2; // Normal spacing - } -} - -// Workbench -const workbench = { - title: 'Workbench', - x: 140, - y: 1680, - width: 350, - height: 520, - fill: '#e7ebf3', - isWorkbench: true, - inventory: [] -}; - -// Tall Cabinets 100-103 (File Cabinets) -const tallCabinetX = 140; -const tallCabinetWidth = 240; -const tallCabinetHeight = 300; -const tallCabinetSpacing = 305; // Vertical spacing -const tallCabinetStartY = 2260; - -const tallCabinets = []; -for (let i = 0; i < 4; i++) { - tallCabinets.push({ - title: `Tall Cabinet ${103 - i}`, // 103, 102, 101, 100 - x: tallCabinetX, - y: tallCabinetStartY + i * tallCabinetSpacing, - width: tallCabinetWidth, - height: tallCabinetHeight, - fill: 'var(--files)', - inventory: [] - }); -} - -// Tall Cabinet 104 (above right drawer section) -// Calculate x position from room right border -const roomRightBorder = inventoryBounds.room.x + inventoryBounds.room.width; // 80 + 3600 = 3680 -const tallCabinet104X = roomRightBorder - tallCabinetWidth - 30; // 3680 - 240 - 20 = 3420 - -const tallCabinet104 = { - title: 'Tall Cabinet 104', - x: tallCabinet104X, - y: rightYStart - tallCabinetHeight - 20, // Above the right drawers - width: tallCabinetWidth, - height: tallCabinetHeight, - fill: 'var(--files)', - inventory: [] -}; - -// Tables A-H (2 columns x 4 rows) -// Pattern: A,B in col1 rows 0,1; C,D in col2 rows 0,1; E,F in col1 rows 2,3; G,H in col2 rows 2,3 -const tableWidth = 720; -const tableHeight = 300; -const tableCol1X = 800; -const tableCol2X = 2100; -const tableRows = [1080, 1385, 2160, 2465]; // 4 rows - -const tables = []; -const tableLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; - -tableLabels.forEach((label, index) => { - // Pattern: pairs alternate columns, then move to next row pair - // A,B (0,1): col0 rows 0,1 - // C,D (2,3): col1 rows 0,1 - // E,F (4,5): col0 rows 2,3 - // G,H (6,7): col1 rows 2,3 - const pairIndex = Math.floor(index / 2); // 0,0,1,1,2,2,3,3 - const colIndex = pairIndex % 2; // 0,0,1,1,0,0,1,1 - const rowInPair = index % 2; // 0,1,0,1,0,1,0,1 - const rowIndex = Math.floor(pairIndex / 2) * 2 + rowInPair; // 0,1,0,1,2,3,2,3 - - tables.push({ - title: `Table ${label}`, - x: colIndex === 0 ? tableCol1X : tableCol2X, - y: tableRows[rowIndex], - width: tableWidth, - height: tableHeight, - fill: 'var(--table)', - inventory: [] - }); -}); - -// Tables I-J (2 tables horizontally at bottom of room) -const roomBottomY = 80 + 3840 - 300 - 20; // Room height - table height - margin -const bottomTableY = roomBottomY; // ~3600 -const bottomTableSpacing = 50; // Space between the two tables -const bottomTableStartX = 1400; // Start position for first table - -const bottomTables = [ - { - title: 'Table I', - x: bottomTableStartX, - y: bottomTableY, - width: tableWidth, - height: tableHeight, - fill: 'var(--table)', - inventory: [] - }, - { - title: 'Table J', - x: bottomTableStartX + tableWidth + bottomTableSpacing, - y: bottomTableY, - width: tableWidth, - height: tableHeight, - fill: 'var(--table)', - inventory: [] - } -]; - -// Combine all boxes in the desired order -const allBoxes = [ - ...topDrawers, - ...topCabinets, - ...rightDrawers, - ...rightCabinets, - workbench, - ...tallCabinets, - tallCabinet104, - ...tables, - ...bottomTables -]; - -// Create the full JSON structure -const output = { - "inventory-bounds": inventoryBounds, - "boxes": allBoxes -}; - -// Format function to align columns -function formatInventoryData(data) { - // Define attribute order - const attributeOrder = ['title', 'x', 'y', 'width', 'height', 'fill', 'isWorkbench', 'inventory']; - - // Find the maximum width for each attribute's value across all boxes - const maxValueWidths = {}; - - attributeOrder.forEach(attr => { - let maxWidth = 0; - data.boxes.forEach(box => { - if (box[attr] !== undefined) { - const value = box[attr]; - let valueStr; - - if (Array.isArray(value)) { - valueStr = JSON.stringify(value); - } else if (typeof value === 'string') { - valueStr = `"${value}"`; - } else if (typeof value === 'boolean') { - valueStr = String(value); - } else { - valueStr = String(value); - } - - maxWidth = Math.max(maxWidth, valueStr.length); - } - }); - maxValueWidths[attr] = maxWidth; - }); - - // Format each box with aligned columns (all attributes on one line) - const formatBox = (box) => { - const parts = []; - - attributeOrder.forEach(attr => { - if (box[attr] !== undefined) { - const value = box[attr]; - let valueStr; - - if (Array.isArray(value)) { - valueStr = JSON.stringify(value); - } else if (typeof value === 'string') { - valueStr = `"${value}"`; - } else if (typeof value === 'boolean') { - valueStr = String(value); - } else { - valueStr = String(value); - } - - // Pad the value to align columns - const paddedValue = valueStr.padEnd(maxValueWidths[attr]); - parts.push(`"${attr}": ${paddedValue}`); - } - }); - - return ` { ${parts.join(', ')} }`; - }; - - // Format the entire JSON structure - const formattedBoxes = data.boxes.map(formatBox); - - // Preserve inventory-bounds if it exists - let formattedOutput = ''; - if (data['inventory-bounds']) { - const bounds = JSON.stringify(data['inventory-bounds'], null, 2); - // Indent the bounds object properly - const indentedBounds = bounds.split('\n').map((line, idx) => { - if (idx === 0) return line; // First line - return ' ' + line; - }).join('\n'); - - formattedOutput = `{ - "inventory-bounds": ${indentedBounds}, - "boxes": [ -${formattedBoxes.join(',\n')} - ] -} -`; - } else { - formattedOutput = `{ - "boxes": [ -${formattedBoxes.join(',\n')} - ] -} -`; - } - - return formattedOutput; -} - -// Write to file -const outputPath = path.join(__dirname, 'public', 'inventory-locations.json'); - -// First write the raw JSON -const jsonString = JSON.stringify(output, null, 2); -fs.writeFileSync(outputPath, jsonString, 'utf8'); - -// Then format it with aligned columns -const formattedOutput = formatInventoryData(output); -fs.writeFileSync(outputPath, formattedOutput, 'utf8'); - -console.log(`✓ Generated ${outputPath}`); -console.log(` - Inventory bounds configured`); -console.log(` - ${allBoxes.length} boxes generated`); -console.log(` - Formatted with aligned columns`); diff --git a/milventory/package-lock.json b/milventory/package-lock.json index 362388e..70cb302 100644 --- a/milventory/package-lock.json +++ b/milventory/package-lock.json @@ -10,8 +10,10 @@ "dependencies": { "d3": "^7.8.5", "http-proxy-middleware": "^2.0.6", + "mysql2": "^3.18.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-router-dom": "^6.8.0", "react-scripts": "5.0.1" } }, @@ -51,6 +53,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -654,6 +657,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1479,6 +1483,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -2863,6 +2868,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3395,6 +3408,7 @@ "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3528,6 +3542,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -3579,6 +3594,7 @@ "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -3918,6 +3934,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4007,6 +4024,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4433,6 +4451,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/axe-core": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", @@ -4848,6 +4875,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6085,6 +6113,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -6335,6 +6364,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6890,6 +6928,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8088,6 +8127,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -9143,6 +9191,12 @@ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9464,6 +9518,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -10309,6 +10364,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10633,6 +10689,12 @@ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10660,6 +10722,21 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -10888,6 +10965,44 @@ "multicast-dns": "cli.js" } }, + "node_modules/mysql2": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.18.2.tgz", + "integrity": "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -10898,6 +11013,18 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -11546,6 +11673,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -12612,6 +12740,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12931,6 +13060,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13057,6 +13187,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -13079,10 +13210,41 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", + "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -13485,6 +13647,7 @@ "version": "2.79.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -13719,6 +13882,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14181,6 +14345,21 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, "node_modules/stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", @@ -14934,19 +15113,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -15256,6 +15422,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "peer": true, "engines": { "node": ">=10" }, @@ -15642,6 +15809,7 @@ "version": "5.102.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -15711,6 +15879,7 @@ "version": "4.15.2", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -16098,6 +16267,7 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/milventory/package.json b/milventory/package.json index a362cec..6ebb753 100644 --- a/milventory/package.json +++ b/milventory/package.json @@ -3,11 +3,13 @@ "version": "1.0.0", "private": true, "dependencies": { + "d3": "^7.8.5", + "http-proxy-middleware": "^2.0.6", + "mysql2": "^3.18.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-scripts": "5.0.1", - "d3": "^7.8.5", - "http-proxy-middleware": "^2.0.6" + "react-router-dom": "^6.8.0", + "react-scripts": "5.0.1" }, "scripts": { "start": "react-scripts start", @@ -33,4 +35,3 @@ ] } } - diff --git a/milventory/public/index.html b/milventory/public/index.html index 0bfce82..7017897 100644 --- a/milventory/public/index.html +++ b/milventory/public/index.html @@ -3,7 +3,7 @@ - Robotics Room — Zoomable Map with Randomized Tooltips + Milventory diff --git a/milventory/public/inventory-locations.json b/milventory/public/inventory-locations.json deleted file mode 100644 index 76c9996..0000000 --- a/milventory/public/inventory-locations.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "inventory-bounds": { - "viewBox": { - "x": 0, - "y": 0, - "width": 4000, - "height": 4000 - }, - "room": { - "x": 80, - "y": 80, - "width": 3600, - "height": 3840, - "rx": 18, - "ry": 18 - } - }, - "boxes": [ - { "title": "Drawer A" , "x": 720 , "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer B" , "x": 875 , "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer C" , "x": 1030, "y": 80 , "width": 300, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer D" , "x": 1335, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer E" , "x": 1490, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer F" , "x": 1645, "y": 80 , "width": 300, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer G" , "x": 1950, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer H" , "x": 2105, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer I" , "x": 2260, "y": 80 , "width": 300, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer J" , "x": 2565, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer K" , "x": 2720, "y": 80 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Cabinet 1" , "x": 720 , "y": 250 , "width": 305, "height": 155, "fill": "var(--table)" }, - { "title": "Cabinet 2" , "x": 1335, "y": 250 , "width": 305, "height": 155, "fill": "var(--table)" }, - { "title": "Cabinet 3" , "x": 1950, "y": 250 , "width": 305, "height": 155, "fill": "var(--table)" }, - { "title": "Cabinet 4" , "x": 2565, "y": 250 , "width": 305, "height": 155, "fill": "var(--table)" }, - { "title": "Drawer L" , "x": 3500, "y": 840 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer M" , "x": 3500, "y": 995 , "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer N" , "x": 3500, "y": 1150, "width": 150, "height": 305, "fill": "var(--drawer)" }, - { "title": "Drawer O" , "x": 3500, "y": 1460, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer P" , "x": 3500, "y": 1615, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer R" , "x": 3500, "y": 1770, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer S" , "x": 3500, "y": 1925, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer T" , "x": 3500, "y": 2080, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer U" , "x": 3500, "y": 2235, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer V" , "x": 3500, "y": 2390, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer W" , "x": 3500, "y": 2545, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer X" , "x": 3500, "y": 2700, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer Y" , "x": 3500, "y": 2855, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer Z" , "x": 3500, "y": 3010, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Drawer AA" , "x": 3500, "y": 3165, "width": 150, "height": 150, "fill": "var(--drawer)" }, - { "title": "Cabinet 5" , "x": 3325, "y": 840 , "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 6" , "x": 3325, "y": 1150, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 7" , "x": 3325, "y": 1460, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 8" , "x": 3325, "y": 1770, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 9" , "x": 3325, "y": 2080, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 10" , "x": 3325, "y": 2390, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 11" , "x": 3325, "y": 2700, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Cabinet 12" , "x": 3325, "y": 3010, "width": 155, "height": 305, "fill": "var(--table)" }, - { "title": "Workbench" , "x": 140 , "y": 1680, "width": 350, "height": 520, "fill": "#e7ebf3" , "isWorkbench": true }, - { "title": "Tall Cabinet 103", "x": 140 , "y": 2260, "width": 240, "height": 300, "fill": "var(--files)" }, - { "title": "Tall Cabinet 102", "x": 140 , "y": 2565, "width": 240, "height": 300, "fill": "var(--files)" }, - { "title": "Tall Cabinet 101", "x": 140 , "y": 2870, "width": 240, "height": 300, "fill": "var(--files)" }, - { "title": "Tall Cabinet 100", "x": 140 , "y": 3175, "width": 240, "height": 300, "fill": "var(--files)" }, - { "title": "Tall Cabinet 104", "x": 3410, "y": 520 , "width": 240, "height": 300, "fill": "var(--files)" }, - { "title": "Table A" , "x": 800 , "y": 1080, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table B" , "x": 800 , "y": 1385, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table C" , "x": 2100, "y": 1080, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table D" , "x": 2100, "y": 1385, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table E" , "x": 800 , "y": 2160, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table F" , "x": 800 , "y": 2465, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table G" , "x": 2100, "y": 2160, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table H" , "x": 2100, "y": 2465, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table I" , "x": 1400, "y": 3600, "width": 720, "height": 300, "fill": "var(--table)" }, - { "title": "Table J" , "x": 2170, "y": 3600, "width": 720, "height": 300, "fill": "var(--table)" } - ] -} diff --git a/milventory/public/master-inventory-items.json b/milventory/public/master-inventory-items.json deleted file mode 100644 index 942e6cf..0000000 --- a/milventory/public/master-inventory-items.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "items": [] -} - - - - - - diff --git a/milventory/src/App.js b/milventory/src/App.js index 725adb3..495a624 100644 --- a/milventory/src/App.js +++ b/milventory/src/App.js @@ -1,13 +1,20 @@ import React, { useState, useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate, useNavigate } from 'react-router-dom'; import { InventoryProvider, useInventory } from './context/InventoryContext'; -import MapComponent from './components/Map'; -import LeftPanel from './components/LeftPanel'; -import Tooltip from './components/Tooltip'; -import AddModal from './components/AddModal'; -import EditModal from './components/EditForm'; -import AddModePreview from './components/AddModePreview'; -import Login from './components/Login'; -import ErrorToast from './components/ErrorToast'; +import MapComponent from './components/Map/Map'; +import LeftPanel from './components/Layout/LeftPanel'; +import Tooltip from './components/Map/Tooltip'; +import AddModal from './components/Box/AddModal'; +import EditModal from './components/Box/EditForm'; +import AddModePreview from './components/Map/AddModePreview'; +import Login from './components/Auth/Login'; +import SignUp from './components/Auth/SignUp'; +import ErrorToast from './components/Common/ErrorToast'; +import ConflictErrorModal from './components/Common/ConflictErrorModal'; +import { BlockingDialogProvider } from './components/Common/BlockingDialogContext'; +import HistoryModal from './components/History/HistoryModal'; +import UserItemTypesModal from './components/Master/UserItemTypesModal'; +import AdminDashboard from './components/Admin/AdminDashboard'; import { auth } from './api'; function App() { @@ -49,19 +56,104 @@ function App() { ); } + return ( + + + + + ) : ( + + ) + } + /> + + ) : ( + + + + ) + } + /> + + + + + + } + /> + } /> + + + + ); +} + +// Protected Route component +function ProtectedRoute({ children, requireLeader = false }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + auth.getCurrentUser() + .then((userData) => { + setUser(userData); + setLoading(false); + }) + .catch(() => { + setUser(null); + setLoading(false); + }); + }, []); + + if (loading) { + return ( +
+
Loading...
+
+ ); + } + if (!user) { - return ; + return ; } - return ( - - - - ); + if (requireLeader && !user.is_leader) { + return ; + } + + return children; } function AppContent({ user, onLogout }) { - const { wrapRef, svgRef, isLoading, error, setError } = useInventory(); + const { + wrapRef, + svgRef, + isLoading, + error, + setError, + conflictError, + setConflictError, + dismissMasterWorkbenchUI, + } = useInventory(); + const navigate = useNavigate(); + const [showHistoryModal, setShowHistoryModal] = useState(false); + const [showUserTypesModal, setShowUserTypesModal] = useState(false); // Handle 401 errors by logging out useEffect(() => { @@ -92,16 +184,82 @@ function AppContent({ user, onLogout }) {
- Zoom: wheel · Pan: drag · Hover for name · Click for details {user && ( Logged in as {user.first_name} {user.last_name} ({user.email}) + {user.is_leader && ( + + )} + +