From 428e336553d8ab8dcf7bff7072aec38590a8de3e Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Thu, 23 Jul 2026 18:35:49 +0530 Subject: [PATCH] changes regarding the mui to astryx tempalate based design --- package-lock.json | 1508 ++++++++++-------- package.json | 13 +- public/navbarLogo.png | Bin 0 -> 83551 bytes src/App.jsx | 11 +- src/components/Button.jsx | 16 + src/components/DateRangePicker.jsx | 202 ++- src/components/Logo.jsx | 73 +- src/components/PageHeader.jsx | 100 +- src/components/Panel.jsx | 33 + src/components/StatCard.jsx | 181 ++- src/index.css | 112 ++ src/layout/MainLayout/Header.jsx | 539 ++----- src/layout/MainLayout/Sidebar.jsx | 412 ++--- src/layout/MainLayout/index.jsx | 69 +- src/layout/MinimalLayout.jsx | 5 +- src/main.jsx | 28 +- src/menu/navItems.jsx | 27 +- src/pages/Dashboard.jsx | 904 ++++------- src/pages/auth/Login.jsx | 259 ++- src/pages/auth/Signup.jsx | 308 ++-- src/pages/operations/Dispatch.jsx | 552 +++---- src/pages/operations/HubSettings.jsx | 407 ++--- src/pages/operations/Inbound.jsx | 537 +++---- src/pages/operations/OrderAssignment.jsx | 670 ++++---- src/pages/operations/RiderRoutes.jsx | 704 ++++----- src/pages/operations/Riders.jsx | 1822 ++++++++-------------- src/pages/operations/Routing.jsx | 420 ++--- src/pages/operations/TrackingMap.jsx | 286 ++-- src/theme/componentsOverride.js | 191 --- src/theme/index.js | 20 - src/theme/palette.js | 102 -- src/theme/shadows.js | 15 - src/theme/typography.js | 28 - 33 files changed, 4679 insertions(+), 5875 deletions(-) create mode 100644 public/navbarLogo.png create mode 100644 src/components/Button.jsx create mode 100644 src/components/Panel.jsx create mode 100644 src/index.css delete mode 100644 src/theme/componentsOverride.js delete mode 100644 src/theme/index.js delete mode 100644 src/theme/palette.js delete mode 100644 src/theme/shadows.js delete mode 100644 src/theme/typography.js diff --git a/package-lock.json b/package-lock.json index 7eb4ac7..7c3f1ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,14 +8,15 @@ "name": "doormile-hub", "version": "0.0.0", "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.1.1", - "@mui/material": "^9.1.1", + "@astryxdesign/cli": "^0.1.8", + "@astryxdesign/core": "^0.1.8", + "@astryxdesign/theme-neutral": "^0.1.8", + "@stylexjs/stylex": "^0.19.0", "dayjs": "^1.11.21", "leaflet": "^1.9.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "lucide-react": "^1.25.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-leaflet": "^4.2.1", "react-router-dom": "^6.30.4", "xlsx": "^0.18.5" @@ -34,6 +35,72 @@ "vite": "^5.4.0" } }, + "node_modules/@astryxdesign/cli": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.1.8.tgz", + "integrity": "sha512-8q3o415RGZ+UvZ4HVfXJFHB9ehNk6iw3akyvXOe7QFfu3fkKPjRzZZLJaxiiTPUdbxMaf16jOzSjfFaETqEouQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "jiti": "^2.7.0", + "jscodeshift": "^17.3.0", + "zod": "^4.4.3" + }, + "bin": { + "astryx": "bin/astryx.mjs", + "cli": "bin/astryx.mjs" + }, + "peerDependencies": { + "@astryxdesign/charts": "*", + "@astryxdesign/core": "*", + "@astryxdesign/lab": "*", + "@astryxdesign/theme-neutral": "*", + "gpt-tokenizer": "^3.4.0" + }, + "peerDependenciesMeta": { + "@astryxdesign/charts": { + "optional": true + }, + "@astryxdesign/core": { + "optional": true + }, + "@astryxdesign/lab": { + "optional": true + }, + "@astryxdesign/theme-neutral": { + "optional": true + } + } + }, + "node_modules/@astryxdesign/core": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.1.8.tgz", + "integrity": "sha512-RviCsKJNPb/mlhn+G9Q+eoFksbTE5Nfay526Kiqjm7I7avxUIsvhNuOmMjybAAseM1o16vZvW1C+hfKAmMd3SA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "intl-messageformat": "^11.2.9" + }, + "peerDependencies": { + "@stylexjs/stylex": "^0.19.0", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + } + }, + "node_modules/@astryxdesign/theme-neutral": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.1.8.tgz", + "integrity": "sha512-ByagXqwTVijRm8aoTJXG2EirCrfx+qs1kn68X0VvPeEsI3yR650aZKsL8DYsvD8kLEEtKVprLCiSVxupHPJ+3w==", + "license": "MIT", + "dependencies": { + "lucide-react": "^1.18.0" + }, + "peerDependencies": { + "@astryxdesign/core": "0.1.8", + "react": ">=19" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -52,7 +119,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -62,7 +128,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -93,7 +158,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/@babel/generator": { @@ -112,11 +176,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.7", @@ -129,6 +204,27 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -138,6 +234,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -155,7 +264,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -169,16 +277,57 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -201,7 +350,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -211,7 +359,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", @@ -236,6 +383,146 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", @@ -268,13 +555,78 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { + "node_modules/@babel/plugin-transform-typescript": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/template": { @@ -322,152 +674,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emotion/babel-plugin": { - "version": "11.13.5", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", - "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.3.3", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/cache": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", - "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", - "license": "MIT" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", - "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", - "license": "MIT", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", - "license": "MIT" - }, - "node_modules/@emotion/react": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", - "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", - "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", - "license": "MIT", - "dependencies": { - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/unitless": "^0.10.0", - "@emotion/utils": "^1.4.2", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", - "license": "MIT" - }, - "node_modules/@emotion/styled": { - "version": "11.14.1", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", - "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.13.5", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.3", - "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", - "@emotion/utils": "^1.4.2" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", - "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", - "license": "MIT" - }, - "node_modules/@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", - "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", - "license": "MIT", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", - "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", - "license": "MIT" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1016,6 +1222,27 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.15", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.15.tgz", + "integrity": "sha512-5o4grXKotAB3JqQuisLApHG43g17N+paoRTa92Jiz35Zvfemq0cVf4EDvuxyHAzmsJji7igaEowicLO/VmfJ8Q==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1096,7 +1323,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1128,249 +1354,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mui/core-downloads-tracker": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.1.1.tgz", - "integrity": "sha512-AupmMICbdJHqAh6FfOMaaiiIr7dfEgZJn5DFfiPuGNrbs+ZZy9cD1APwO0TSVBz5j08MJEEY6n7iC76/2wjMEA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.1.1.tgz", - "integrity": "sha512-OXhm9DajemStb58AumM06DuPhHTa3XD36TFD4yf6WtJyNRO5DfEZbbnHlBg/US2Y2oOXwM/XurMTBOD6L/YYZw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^9.1.1", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.1.1.tgz", - "integrity": "sha512-Wv+gInjrpf99l1Q0oHe0eOWGTnlbkzs5nowClX65KCT/2fyPMwcbFEEkUsOHdpcHhB5UAbz/d7jlwt5ajWVvlA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/core-downloads-tracker": "^9.1.1", - "@mui/system": "^9.1.1", - "@mui/types": "^9.1.1", - "@mui/utils": "^9.1.1", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1", - "react-is": "^19.2.6", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^9.1.1", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.1.1.tgz", - "integrity": "sha512-oH6c+d6sJ1CZT0Vg2/fHdUQ5zvo9Pn+f+WWk0tlQliHqqIRdN32DZ7UxjalW3LUj4OkHbdWR31biWuLxK9i7Cg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/utils": "^9.1.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.1.1.tgz", - "integrity": "sha512-neaYKdJfvEG54q8efHLJR7swpHG/gfSv9xGqW5iTSMsubD7yPCPFrhVBt284j1DOF3uZaaDJSHQL7gz6jGF21Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@emotion/cache": "^11.14.0", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.1.1.tgz", - "integrity": "sha512-q+aqNa58QZUwmmyUvJKKrStrej+4BcWFw4M0Ug+zRylPIQgR64cqvBnE3QTfLZm4OXulydp8Hl3zwKxMayrdsA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/private-theming": "^9.1.1", - "@mui/styled-engine": "^9.1.1", - "@mui/types": "^9.1.1", - "@mui/utils": "^9.1.1", - "clsx": "^2.1.1", - "csstype": "^3.2.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.1.1.tgz", - "integrity": "sha512-Zjt7u8wNvDg40rPTGoL+TnfkpuSKjwubsNSFRH1KAVZLcaV4I3AFNHIFbvH7p4F3alEibSbdd90xAgn5Rnfndg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.1.1.tgz", - "integrity": "sha512-qSNfnkzZMptaaWFFklpDf4NPJztgwsMDVfM/sSDt+wq4ssYSBhLYwwjuB6eS/+p2IUYbeRzHluzXbw0Zn7aI4A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.29.2", - "@mui/types": "^9.1.1", - "@types/prop-types": "^15.7.15", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.2.6" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/@react-leaflet/core": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", @@ -1748,6 +1731,17 @@ "win32" ] }, + "node_modules/@stylexjs/stylex": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.0.tgz", + "integrity": "sha512-CnUFp7YMaDLDeemsWOfJgoC/gKM5P/yBNMcpJaE6ChJmXr7s0DJwSeGTTlHJcqqwN9OW1qGtmARWLFhGZN1pTA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2", + "invariant": "^2.2.4", + "styleq": "0.2.1" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1807,22 +1801,18 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.31", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1839,15 +1829,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2079,6 +2060,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2105,21 +2098,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2131,7 +2109,6 @@ "version": "2.10.38", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -2155,7 +2132,6 @@ "version": "4.28.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2185,6 +2161,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2239,6 +2221,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2248,7 +2231,6 @@ "version": "1.0.30001799", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2295,11 +2277,16 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, "engines": { "node": ">=6" } @@ -2333,6 +2320,21 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2340,28 +2342,6 @@ "dev": true, "license": "MIT" }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "license": "MIT", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -2389,10 +2369,17 @@ "node": ">= 8" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -2528,16 +2515,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2557,18 +2534,8 @@ "version": "1.5.377", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.377.tgz", "integrity": "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==", - "dev": true, "license": "ISC" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -2671,6 +2638,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2809,7 +2777,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2819,6 +2786,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -3015,6 +2983,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3095,11 +3076,19 @@ "node": ">=16.0.0" } }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "license": "MIT" + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, "node_modules/find-up": { "version": "5.0.0", @@ -3139,6 +3128,27 @@ "dev": true, "license": "ISC" }, + "node_modules/flow-estree": { + "version": "0.323.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.323.0.tgz", + "integrity": "sha512-VuCvqb38ueB7u9Xnfuj/uV9xWaqRM1XX8fUbch3Q1perZp1BZHAcGKcSkc57RdVXaDrjsfgNOiSEmlvx8Yhk4Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.323.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.323.0.tgz", + "integrity": "sha512-uTw5ffLYxQDnSe4Ch5mo1Sp7kFOrhOglz3nqgpQRrYpt4ucQTxrQRCLdC0F1hyhcIK7j5GjdxzV1d4DN68eDzA==", + "license": "MIT", + "dependencies": { + "flow-estree": "0.323.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3183,6 +3193,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3236,7 +3247,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3355,6 +3365,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3440,6 +3456,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -3448,21 +3465,6 @@ "node": ">= 0.4" } }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3477,6 +3479,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -3493,7 +3496,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -3514,6 +3516,25 @@ "node": ">= 0.4" } }, + "node_modules/intl-messageformat": { + "version": "11.2.12", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.12.tgz", + "integrity": "sha512-KW70Xxfcvy7vV3qODfvShWkFDPMqKDAa4N+hSyVBWGNtVhTUFYaqlD/l88DaYPKiVcPP4rPQ3qnH7i5K82Mg7g==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.15" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -3532,12 +3553,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -3608,6 +3623,7 @@ "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -3772,6 +3788,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -3931,6 +3959,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -3949,6 +3986,15 @@ "node": ">= 0.4" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3978,6 +4024,46 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jscodeshift": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.4.0.tgz", + "integrity": "sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "picomatch": "^4.0.2", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3997,12 +4083,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4021,7 +4101,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -4056,6 +4135,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/leaflet": { "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", @@ -4076,12 +4164,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4121,12 +4203,42 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz", + "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4182,6 +4294,12 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/node-exports-info": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.2.tgz", @@ -4205,7 +4323,6 @@ "version": "2.0.48", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4215,6 +4332,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4386,10 +4504,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -4398,24 +4526,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4440,23 +4550,118 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -4510,6 +4715,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -4521,6 +4727,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/punycode": { @@ -4534,36 +4741,26 @@ } }, "node_modules/react": { - "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==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.8" } }, - "node_modules/react-is": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", - "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", - "license": "MIT" - }, "node_modules/react-leaflet": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", @@ -4620,20 +4817,20 @@ "react-dom": ">=16.8" } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", + "node_modules/recast": { + "version": "0.23.12", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.12.tgz", + "integrity": "sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" + "engines": { + "node": ">= 4" } }, "node_modules/reflect.getprototypeof": { @@ -4680,31 +4877,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -4811,19 +4988,15 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4878,6 +5051,18 @@ "node": ">= 0.4" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4977,10 +5162,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -4996,6 +5193,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/ssf": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", @@ -5134,10 +5341,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "node_modules/styleq": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", + "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==", "license": "MIT" }, "node_modules/supports-color": { @@ -5157,6 +5364,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5165,6 +5373,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5279,7 +5508,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -5509,6 +5737,19 @@ "node": ">=0.10.0" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", @@ -5534,18 +5775,8 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5558,6 +5789,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index b187278..f6d0010 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,15 @@ "preview": "vite preview" }, "dependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mui/icons-material": "^9.1.1", - "@mui/material": "^9.1.1", + "@astryxdesign/cli": "^0.1.8", + "@astryxdesign/core": "^0.1.8", + "@astryxdesign/theme-neutral": "^0.1.8", + "@stylexjs/stylex": "^0.19.0", "dayjs": "^1.11.21", "leaflet": "^1.9.4", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "lucide-react": "^1.25.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-leaflet": "^4.2.1", "react-router-dom": "^6.30.4", "xlsx": "^0.18.5" diff --git a/public/navbarLogo.png b/public/navbarLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..f64351844eb8674eeada1cad25774313d5258af3 GIT binary patch literal 83551 zcmeFZ^;=Zm7dA|Tgn%F+4T>TSQj!V?NC`+u!%)&79fKfU(hZVIcgN5T1JWQ}(hN1^ z%zMVq^FDvXbA9H_}KMDL#0{@f1|0MALrUXtDm(WN_N#|ddPZ7;Uvi*ybpy3y2B&4KlQMJmO4b%00 z4EPU$=;_AyFGiYGsI$UFe*; zH`^Y1d8Wn#MBG!tf6>+|*T|Zj5D2hhn5gO0?Ox4S7(&l$v|j+vXiB4E??*o6%dP7d znUIww=@@bLdhla|_s-x7f^`6XcK2;FutHB8TWiE$$D}g}y>X|Y*9`Mk- z8tNhIf(MWUGHeWH<~T2WG#QzvoUhiB-{(pJ6RQ9BMjY$CI;SAQK${A9{Mo-xxd|%Y z6fCe20^g4N7yU)kSnrsjDtg#5GN7OT<`oTKy=Pv0R{|gkP{_X#Gue2**s1~^VCKFA zlsM+;)6OFfXz@{xw6FoPj4dlrfXG|_o7XNEwWs=Tz%0A@sCVj9Q15thrCmiN#Ki7v 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ℑLH^vV6!ltU%*7=%^zH&!eaFX1 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 V!zj{wQHMGNsHWkAiu;al{txy&3PS(@ literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx index 1e51028..6829533 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,5 @@ import { Suspense, lazy } from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; -import { Box, CircularProgress } from '@mui/material'; - import MainLayout from '@/layout/MainLayout'; import MinimalLayout from '@/layout/MinimalLayout'; import ProtectedRoute from '@/auth/ProtectedRoute'; @@ -11,9 +9,10 @@ const load = (factory) => { return ( - - +

+
+ +
} > @@ -34,6 +33,8 @@ export default function App() { > import('@/pages/Dashboard'))} /> import('@/pages/operations/Inbound'))} /> + {/* TEMPORARILY DISABLED: These pages still contain MUI imports which crashes Vite. + They will be re-enabled once they are migrated to Astryx. */} import('@/pages/operations/Routing'))} /> import('@/pages/operations/Dispatch'))} /> import('@/pages/operations/TrackingMap'))} /> diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 0000000..e6f3234 --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,16 @@ +import { Button as AstryxButton } from '@astryxdesign/core/Button'; + +// Wraps Astryx's Button so `variant="primary"` (the default) renders in the +// Doormile brand red instead of the theme's neutral accent — primary CTAs +// stand out as "the one thing to click" while every other accent-driven +// control (focus rings, selected nav, ghost/outline buttons) keeps the +// theme's neutral dark. Scoped via a CSS custom property on this element only, +// not a global token flip, so nothing else on the page is affected. +export default function Button({ variant = 'primary', style, ...props }) { + const brandStyle = + variant === 'primary' + ? { '--color-accent': 'var(--color-brand)', '--color-on-accent': 'var(--color-on-brand)' } + : null; + + return ; +} diff --git a/src/components/DateRangePicker.jsx b/src/components/DateRangePicker.jsx index db1b202..ae3bd70 100644 --- a/src/components/DateRangePicker.jsx +++ b/src/components/DateRangePicker.jsx @@ -1,41 +1,30 @@ import { useState } from 'react'; -import { Box, Stack, Typography, IconButton, Button, Popover, Divider, Chip, useMediaQuery } from '@mui/material'; -import { alpha } from '@mui/material/styles'; import dayjs from 'dayjs'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded'; -import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; -import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react'; -// ───────────────────────────────────────────────────────────────────────────── -// Shared date-range picker — the same two-month brand calendar used on the -// Dashboard, packaged for reuse. Controlled: pass `value={{ from, to }}` (both -// 'YYYY-MM-DD') and an `onChange({ from, to })` handler. Optionally cap the -// selectable range with `maxDate` (defaults to today, so no future dates). -// ───────────────────────────────────────────────────────────────────────────── +import Button from '@/components/Button'; export const DATE_FMT = 'YYYY-MM-DD'; -const BRAND = '#C01227'; +const BRAND = 'var(--color-brand)'; const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; -// A single compact month grid. function MonthGrid({ view, start, end, max, onDay }) { const gridStart = view.startOf('month').subtract(view.startOf('month').day(), 'day'); const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day')); return ( - - +
+
{view.format('MMMM YYYY')} - - +
+
{DAY_LABELS.map((d, i) => ( - +
{d} - +
))} - - +
+
{cells.map((d) => { const inMonth = d.month() === view.month(); const isStart = start && d.isSame(start, 'day'); @@ -44,37 +33,40 @@ function MonthGrid({ view, start, end, max, onDay }) { const inRange = start && end && d.isAfter(start, 'day') && d.isBefore(end, 'day'); const isToday = d.isSame(dayjs(), 'day'); const disabled = max && d.isAfter(max, 'day'); + return ( - - + +
); })} - - +
+
); } -// Click a day to set the start, click again to set the end (auto-swaps if reversed). function RangeCalendar({ from, to, maxDate, onSelect }) { const [view, setView] = useState(dayjs(to || from || undefined).startOf('month')); const [anchorDate, setAnchorDate] = useState(null); @@ -97,22 +89,27 @@ function RangeCalendar({ from, to, maxDate, onSelect }) { }; return ( - - - setView((v) => v.subtract(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}> - - - setView((v) => v.add(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}> - - - - +
+
+ + +
+
- +
- - - +
+
+ +
); } @@ -125,8 +122,7 @@ const PRESETS = [ export default function DateRangePicker({ value, onChange, maxDate }) { const today = dayjs().format(DATE_FMT); const max = maxDate ?? today; - const [anchor, setAnchor] = useState(null); - const isMobile = useMediaQuery('(max-width:600px)'); + const [open, setOpen] = useState(false); const from = value?.from || today; const to = value?.to || today; @@ -143,72 +139,70 @@ export default function DateRangePicker({ value, onChange, maxDate }) { const isPreset = (days) => to === today && from === dayjs().subtract(days - 1, 'day').format(DATE_FMT); return ( - <> - +
+
{PRESETS.map((p) => { const active = isPreset(p.days); return ( - applyPreset(p.days)} - sx={{ - height: 36, fontSize: '0.825rem', fontWeight: 600, borderRadius: 2, - bgcolor: active ? BRAND : '#F1F5F9', color: active ? '#fff' : '#475569', + style={{ + height: 36, fontSize: '0.825rem', fontWeight: 600, borderRadius: 8, padding: '0 12px', + backgroundColor: active ? BRAND : '#F1F5F9', color: active ? '#fff' : '#475569', + border: 'none', cursor: 'pointer', boxShadow: active ? '0 4px 10px rgba(192,18,39,0.15)' : 'none', - '& .MuiChip-label': { px: 1.5 }, - '&:hover': { bgcolor: active ? '#9E0E20' : '#E2E8F0' } }} - /> + > + {p.label} + ); })} - +
- setAnchor(null)} - anchorOrigin={{ vertical: 'bottom', horizontal: isMobile ? 'left' : 'right' }} - transformOrigin={{ vertical: 'top', horizontal: isMobile ? 'left' : 'right' }} - marginThreshold={12} - PaperProps={{ sx: { mt: 1, borderRadius: 2.5, border: '1px solid #EEF0F2', boxShadow: '0 8px 28px rgba(0,0,0,0.10)', overflow: 'hidden', maxWidth: 'calc(100vw - 24px)' } }} - > - - Select date range - - {dayjs(from).format('DD MMM')} – {dayjs(to).format('DD MMM YYYY')} · {dayCount} {dayCount === 1 ? 'day' : 'days'} - - - onChange({ from: f, to: t })} /> - - - - - - - + {open && ( + <> +
setOpen(false)} /> +
+
+
Select date range
+
+ {dayjs(from).format('DD MMM')} – {dayjs(to).format('DD MMM YYYY')} · {dayCount} {dayCount === 1 ? 'day' : 'days'} +
+
+ onChange({ from: f, to: t })} /> +
+ + +
+
+ + )} +
); } diff --git a/src/components/Logo.jsx b/src/components/Logo.jsx index 98d00da..cf4842e 100644 --- a/src/components/Logo.jsx +++ b/src/components/Logo.jsx @@ -1,44 +1,67 @@ -import { Box, Typography } from '@mui/material'; - // ==============================|| DOORMILE WORDMARK LOGO ||============================== // // Uses the brand wordmark asset (white PNG). `onDark` shows it as-is on dark/red // surfaces; on light surfaces it is recoloured to near-black. `compact` (e.g. the -// collapsed sidebar) renders just the square "D" badge, since the wordmark won't fit. +// collapsed navbar rail) pairs the round navbar mark with a "Doormile" text +// wordmark instead, since the full wordmark image is too wide to fit there. const LOGO_SRC = '/Doormile-logo.png'; +const NAVBAR_MARK_SRC = '/navbarLogo.png'; -export default function Logo({ onDark = false, compact = false, height = 26, sx }) { +export default function Logo({ onDark = false, compact = false, height = 26, size = 64, style = {} }) { if (compact) { - const mark = onDark ? '#FFFFFF' : '#C01227'; - const markText = onDark ? '#C01227' : '#FFFFFF'; return ( - - + + - D - - + Doormile + +
); } return ( - - + Doormile - + ); } diff --git a/src/components/PageHeader.jsx b/src/components/PageHeader.jsx index 85ea55a..ee5859d 100644 --- a/src/components/PageHeader.jsx +++ b/src/components/PageHeader.jsx @@ -1,81 +1,51 @@ -import { Box, Stack, Typography, Avatar } from '@mui/material'; - -// ==============================|| SHARED PAGE HEADER ||============================== // +import { Heading, Text } from '@astryxdesign/core/Text'; export default function PageHeader({ icon: Icon, title, subtitle, action }) { return ( - - +
{Icon && ( - - - + +
)} - - +
+ {title} - + {subtitle && ( - + {subtitle} - + )} - - +
+ - {action && ( - - {action} - - )} -
+ {action &&
{action}
} + ); -} \ No newline at end of file +} diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx new file mode 100644 index 0000000..7fd6b5d --- /dev/null +++ b/src/components/Panel.jsx @@ -0,0 +1,33 @@ +import { Card } from '@astryxdesign/core/Card'; +import { Heading } from '@astryxdesign/core/Text'; + +// Shared "section card": an optional titled header strip over a body slot. +// Every page that shows a titled block of content (tables, forms, feeds) +// renders it through this so cards share one radius/border/shadow/header +// treatment instead of each page re-declaring its own — that drift (16px vs +// 12px radius, different border colors) was the main source of visual +// inconsistency between screens. +export default function Panel({ title, action, children, bodyPadding = 0, style }) { + return ( + + {title && ( +
+ {title} + {action} +
+ )} +
{children}
+
+ ); +} diff --git a/src/components/StatCard.jsx b/src/components/StatCard.jsx index 6fd6afe..315ee70 100644 --- a/src/components/StatCard.jsx +++ b/src/components/StatCard.jsx @@ -1,70 +1,147 @@ -import { Card, CardContent, Stack, Avatar, Typography, Skeleton, Box } from '@mui/material'; -import TrendingUpOutlinedIcon from '@mui/icons-material/TrendingUpOutlined'; +import { Card } from '@astryxdesign/core/Card'; -// ───────────────────────────────────────────────────────────────────────────── -// Shared KPI / stat card — the single reference design used across every page so -// the stat strips read as one system: -// • icon tile + uppercase label on a top row, label centered on the icon's height -// • big number centered on the icon's vertical axis (a 40px column under the icon) -// • optional caption below (with an optional green trend arrow) -// `bg` defaults to a soft tint of `color`. `hover` adds the lift-on-hover effect. -// ───────────────────────────────────────────────────────────────────────────── +// ==============================|| STAT / KPI CARD ||============================== // +// `tone` maps to Astryx's own categorical tokens (same palette Badge/StatusDot +// use), so KPI cards read as part of the same status language as the rest of +// the app instead of a one-off set of hand-picked hex values. + +const TONE_VARS = { + blue: { icon: 'var(--color-icon-blue)', bg: 'var(--color-background-blue)' }, + green: { icon: 'var(--color-icon-green)', bg: 'var(--color-background-green)' }, + orange: { icon: 'var(--color-icon-orange)', bg: 'var(--color-background-orange)' }, + purple: { icon: 'var(--color-icon-purple)', bg: 'var(--color-background-purple)' }, + red: { icon: 'var(--color-icon-red)', bg: 'var(--color-background-red)' }, + teal: { icon: 'var(--color-icon-teal)', bg: 'var(--color-background-teal)' }, + cyan: { icon: 'var(--color-icon-cyan)', bg: 'var(--color-background-cyan)' }, + gray: { icon: 'var(--color-icon-gray)', bg: 'var(--color-background-gray)' } +}; export default function StatCard({ - icon: Icon, label, value, + icon: Icon, + tone = 'blue', sub, - color = '#1A73E8', - bg, - trend = false, + size = 'md', loading = false, - hover = false + hover = true }) { + const isCompact = size === 'sm'; + const { icon: iconColor, bg: tintBg } = TONE_VARS[tone] || TONE_VARS.blue; + return ( - - {/* Icon + label on a clean top row; label stretches to the icon height and - centers its text so the caption is optically centered against the icon. */} - - - {Icon && } - - - {label} - - + {Icon && ( +
+ +
+ )} - {/* Value sits under the icon: a box that is the icon's width (40px) so a short - number centers on the icon's axis, but grows with wider values ("0.0 km", - "₹1,234") so they left-align under the icon instead of overflowing the card. */} - - {loading ? ( - - ) : ( - - {value} - - )} - - - {sub && ( - - {trend && } - {sub} - +
+ + {label} + + {Icon && ( +
+ +
)} - +
+ +
+ {loading ? ( +
+ ) : ( + + {value} + + )} +
+ + {sub && ( +
+ {sub} +
+ )} + + ); } diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..20afe9e --- /dev/null +++ b/src/index.css @@ -0,0 +1,112 @@ +/* ─── Force light mode + brand color: override every Astryx light-dark() token ─── */ +:root, +[data-astryx-theme] { + color-scheme: light; + + /* Surface / background tokens */ + --color-background-surface: #ffffff; + --color-background-body: #f1f4f7; + --color-background-card: #ffffff; + --color-background-popover: #ffffff; + --color-background-muted: rgba(5, 54, 89, 0.047); + --color-background-inverted: #0A1317; + --color-background-error-inverted: #AA071E; + + /* Text tokens */ + --color-text-primary: #0A1317; + --color-text-secondary: #4E606F; + --color-text-disabled: #A4B0BC; + --color-text-accent: #0A1317; + + /* Icon tokens */ + --color-icon-primary: #0A1317; + --color-icon-secondary: #4E606F; + --color-icon-disabled: #A4B0BC; + --color-icon-accent: #0A1317; + + /* Border tokens */ + --color-border: rgba(5, 54, 89, 0.1); + --color-border-emphasized: #CCD3DB; + + /* Interactive overlay tokens */ + --color-neutral: rgba(5, 54, 89, 0.1); + --color-overlay: rgba(1, 18, 40, 0.4); + --color-overlay-hover: rgba(5, 54, 89, 0.047); + --color-overlay-pressed: rgba(5, 54, 89, 0.098); + + /* Misc */ + --color-skeleton: #CCD3DB; + --color-track: #CCD3DB; + --color-shadow: rgba(5, 54, 89, 0.1); + + /* Structural accent (nav selection, focus rings, secondary controls) — stays neutral/dark */ + --color-accent: #0A1317; + --color-accent-muted: rgba(10, 19, 23, 0.12); + --color-on-accent: #ffffff; + + /* Doormile brand red — reserved for primary CTAs only. Do not use for + structural chrome (nav, focus rings, secondary buttons); use + src/components/Button.jsx (variant="primary") to apply it consistently. */ + --color-brand: #C01227; + --color-brand-hover: #A20F20; + --color-brand-muted: rgba(192, 18, 39, 0.1); + --color-on-brand: #ffffff; + + /* "red" Badge variant */ + --color-background-red: rgba(5, 54, 89, 0.1); + --color-text-red: #0A1317; + + /* App-level overrides */ + background-color: #f8fafc; + color: #0f172a; +} + +*, *::before, *::after { + font-family: var(--font-family-body, 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background-color: #f8fafc; + color: #334155; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + color: #0f172a; + letter-spacing: -0.02em; +} + +input, textarea { + font-family: inherit !important; + background-color: #ffffff !important; + color: #0f172a !important; +} + +input::placeholder, textarea::placeholder { + color: #94a3b8 !important; + opacity: 1 !important; +} + +input:not(.search-bar-input):not([class]), select:not([class]), textarea:not([class]) { + background-color: #ffffff !important; + color: #0f172a !important; + border: 1px solid rgba(5, 54, 89, 0.12) !important; + border-radius: 8px !important; + padding: 10px 14px !important; + font-size: 0.875rem !important; + outline: none !important; + transition: border-color 0.2s, box-shadow 0.2s !important; + box-sizing: border-box !important; + width: 100% !important; +} + +input:not(.search-bar-input):not([class]), select:not([class]) { + height: 38px !important; +} + +input:not(.search-bar-input):not([class]):focus, select:not([class]):focus, textarea:not([class]):focus { + border-color: #0A1317 !important; + box-shadow: 0 0 0 3px rgba(10, 19, 23, 0.12) !important; +} diff --git a/src/layout/MainLayout/Header.jsx b/src/layout/MainLayout/Header.jsx index 8c41744..8a18837 100644 --- a/src/layout/MainLayout/Header.jsx +++ b/src/layout/MainLayout/Header.jsx @@ -1,43 +1,12 @@ import { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { - AppBar, - Toolbar, - IconButton, - Box, - InputBase, - Badge, - Avatar, - Typography, - Menu, - MenuItem, - Divider, - ListItemIcon, - ListItemText, - Tooltip, - Button, - Stack, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, - Grid, - alpha, - InputAdornment -} from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import SearchIcon from '@mui/icons-material/Search'; -import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; -import ChatIcon from '@mui/icons-material/Chat'; -import LogoutIcon from '@mui/icons-material/Logout'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import DoneAllIcon from '@mui/icons-material/DoneAll'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import SendIcon from '@mui/icons-material/Send'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'; +import { Settings, LogOut, Bell, MessageSquare, Search, Truck, AlertTriangle, ArrowRight, Send, CheckCircle2 } from 'lucide-react'; +import { TopNav, TopNavHeading } from '@astryxdesign/core/TopNav'; +import { DropdownMenu } from '@astryxdesign/core/DropdownMenu'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Avatar } from '@astryxdesign/core/Avatar'; +import Button from '@/components/Button'; import Logo from '@/components/Logo'; import { getStaff, getHubContext, clearSession } from '@/auth/session'; import { @@ -49,18 +18,16 @@ import { markConversationRead } from '@/api/hub'; -const RED = '#C01227'; +const RED = 'var(--color-brand)'; -// Map a notification `type` to an icon component (real API sends type, not an icon). const NOTIF_ICON = { - exception: WarningAmberIcon, - inbound: LocalShippingOutlinedIcon, - dispatch: LocalShippingOutlinedIcon, - warning: WarningAmberIcon, - alert: NotificationsActiveIcon + exception: AlertTriangle, + inbound: Truck, + dispatch: Truck, + warning: AlertTriangle, + alert: Bell }; -// Build initials from a display name — falls back to a sensible default. const toInitials = (name) => (name || '') .split(' ') @@ -70,8 +37,6 @@ const toInitials = (name) => .join('') .toUpperCase() || 'HB'; -// Normalise a conversation summary from GET /hub/messages. Be tolerant of a few -// backend field-name variants so the list still renders if a key is named differently. const toConversation = (c) => { const name = c.name || c.milername || c.displayname || `Miler ${c.mileruserid ?? c.id}`; return { @@ -84,14 +49,13 @@ const toConversation = (c) => { }; }; -// Normalise one message in a thread from GET /hub/messages/:id. const toMessage = (m) => ({ sender: m.sender === 'me' ? 'me' : 'them', text: m.text ?? m.body ?? m.message ?? '', time: m.time ?? m.createdat ?? m.sentat ?? '' }); -export default function Header({ onToggle }) { +export default function Header({ isSidebarCollapsed }) { const navigate = useNavigate(); const staff = getStaff(); const hub = getHubContext(); @@ -103,14 +67,9 @@ export default function Header({ onToggle }) { navigate('/login'); }; - const [account, setAccount] = useState(null); - const [notifAnchor, setNotifAnchor] = useState(null); - const [msgAnchor, setMsgAnchor] = useState(null); - - // Dialog State const [selectedNotif, setSelectedNotif] = useState(null); const [conversations, setConversations] = useState([]); - const [activeChat, setActiveChat] = useState(null); // { id, name, initials, messages: [] } + const [activeChat, setActiveChat] = useState(null); const [chatLoading, setChatLoading] = useState(false); const [sending, setSending] = useState(false); const [typedMessage, setTypedMessage] = useState(''); @@ -122,7 +81,6 @@ export default function Header({ onToggle }) { const unread = notifications.filter((n) => !n.read).length; - // Load real notifications from the API (map `type` → an icon component). const loadNotifications = useCallback(async () => { try { const res = await getNotifications(); @@ -133,43 +91,39 @@ export default function Header({ onToggle }) { time: n.time, read: Boolean(n.read), type: n.type, - icon: NOTIF_ICON[n.type] || NotificationsNoneIcon + icon: NOTIF_ICON[n.type] || Bell, + desc: n.desc, + stats: n.stats || [], + to: n.to, + actionText: n.actionText })) ); } catch { - // Non-fatal: leave the bell empty if it can't load. setNotifications([]); } }, []); useEffect(() => { loadNotifications(); - const t = setInterval(loadNotifications, 60000); // refresh every 60s + const t = setInterval(loadNotifications, 60000); return () => clearInterval(t); }, [loadNotifications]); - // Load conversations (one per miler at the hub) for the messages dropdown. const loadConversations = useCallback(async () => { try { const res = await getConversations(); setConversations((res?.data || []).map(toConversation)); } catch { - // Non-fatal: leave the messages list empty if it can't load. setConversations([]); } }, []); useEffect(() => { loadConversations(); - const t = setInterval(loadConversations, 60000); // refresh every 60s + const t = setInterval(loadConversations, 60000); return () => clearInterval(t); }, [loadConversations]); - const openNotif = (e) => { setNotifAnchor(e.currentTarget); loadNotifications(); }; - const closeNotif = () => setNotifAnchor(null); - - const openMessages = (e) => { setMsgAnchor(e.currentTarget); loadConversations(); }; - const markAllRead = async () => { const unreadIds = notifications.filter((n) => !n.read).map((n) => n.id); setNotifications((prev) => prev.map((n) => ({ ...n, read: true }))); @@ -178,18 +132,14 @@ export default function Header({ onToggle }) { const onNotifClick = async (n) => { setNotifications((prev) => prev.map((x) => (x.id === n.id ? { ...x, read: true } : x))); - closeNotif(); + if (n.desc || n.stats) setSelectedNotif(n); + else if (n.to) navigate(n.to); try { await markNotificationRead(n.id); - } catch { - /* best effort */ - } + } catch {} }; - // Open a conversation: show the header immediately, then load the thread and - // mark the other party's messages as read (which clears the unread badge). const onMessageClick = async (conv) => { - setMsgAnchor(null); setTypedMessage(''); setActiveChat({ id: conv.id, name: conv.name, initials: conv.initials, messages: [] }); setChatLoading(true); @@ -207,35 +157,30 @@ export default function Header({ onToggle }) { setConversations((prev) => prev.map((c) => (c.id === conv.id ? { ...c, unread: 0 } : c))); } } catch { - // Leave the (empty) thread open; the header still shows who it's with. } finally { setChatLoading(false); } }; - const closeChat = () => { setActiveChat(null); setTypedMessage(''); }; + const closeChat = () => { setActiveChat(null); setTypedMessage(''); }; const handleSendMessage = async () => { const text = typedMessage.trim(); if (!text || !activeChat || sending) return; setSending(true); - // Optimistically append; reconcile with the server's stored copy on success. const now = new Date(); const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); setActiveChat((c) => ({ ...c, messages: [...c.messages, { sender: 'me', text, time: timeStr }] })); setTypedMessage(''); try { await sendMessage(activeChat.id, text); - // Refresh the thread so the persisted message (and its real timestamp) shows. const res = await getConversation(activeChat.id); const thread = res?.data || {}; setActiveChat((c) => c && { ...c, messages: (thread.messages || thread.chat || []).map(toMessage) }); - // Keep the dropdown preview in sync. setConversations((prev) => prev.map((c) => (c.id === activeChat.id ? { ...c, lastMessage: text, time: timeStr } : c)) ); } catch { - // On failure, drop the optimistic bubble and restore the draft to retry. setActiveChat((c) => c && { ...c, messages: c.messages.filter((m) => !(m.sender === 'me' && m.text === text && m.time === timeStr)) }); setTypedMessage(text); } finally { @@ -250,336 +195,118 @@ export default function Header({ onToggle }) { }; return ( - t.zIndex.drawer + 1, - borderBottom: '1px solid', - borderColor: 'grey.200' - }} - > - - - - - - {/* Brand wordmark — left side */} - navigate('/dashboard')} - sx={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }} - > - - - - - - {/* Search */} - - - setSearch(e.target.value)} - placeholder="Scan package, check destination…" - sx={{ fontSize: '0.875rem', flex: 1 }} - inputProps={{ 'aria-label': 'search' }} + <> + } + headingHref="/dashboard" /> - + } + endContent={ +
+ {unreadMessages > 0 && {unreadMessages}}
}} + items={[ + { label: 'Messages', type: 'label' }, + ...conversations.map(m => ({ + label: m.name, + description: m.lastMessage, + onClick: () => onMessageClick(m) + })) + ]} + /> + + {unread > 0 && {unread}}
}} + items={[ + { label: 'Notifications', type: 'label' }, + { label: 'Mark all read', icon: , onClick: markAllRead, disabled: unread === 0 }, + { type: 'divider' }, + ...(notifications.length === 0 ? [{ label: 'No notifications', disabled: true }] : notifications.map((n, i) => ({ + label: n.title + '\u200B'.repeat(i), + description: n.time, + icon: n.icon ? (() => { const Icon = n.icon; return ; })() : undefined, + onClick: () => onNotifClick(n) + }))) + ]} + /> - - - - - - - - - - - - - - + , + label: staffName + }} + items={[ + { label: 'Settings', icon: , onClick: () => navigate('/hub-settings') }, + { type: 'divider' }, + { label: 'Logout', icon: , onClick: handleLogout } + ]} + /> + + } + /> - setAccount(e.currentTarget)} - sx={{ - display: 'flex', - alignItems: 'center', - gap: 1, - ml: 0.5, - cursor: 'pointer', - py: 0.5, - px: 1, - borderRadius: 2, - '&:hover': { bgcolor: 'grey.100' } - }} - > - {toInitials(staffName)} - - - {staffName} - - - {hubName} - - - - - {/* Notifications dropdown */} - - - - Notifications - - - - - {notifications.length === 0 && ( - - - - )} - {notifications.map((n) => { - const Icon = n.icon; - return ( - onNotifClick(n)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - - - - - - - {!n.read && } - - ); - })} - - - {/* Messages dropdown */} - setMsgAnchor(null)} - transformOrigin={{ horizontal: 'right', vertical: 'top' }} - anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} - PaperProps={{ sx: { mt: 1, width: 340, maxWidth: '90vw' } }} - > - - Messages - - - {conversations.length === 0 && ( - - - - )} - {conversations.map((m) => ( - onMessageClick(m)} sx={{ py: 1.25, whiteSpace: 'normal', alignItems: 'flex-start' }}> - - - {m.initials} - - - 0 ? 700 : 600 }} - secondaryTypographyProps={{ fontSize: '0.8rem', noWrap: true }} - /> - - - {m.time} - - {m.unread > 0 && ( - - )} - - - ))} - - - {/* Account dropdown */} - setAccount(null)} - transformOrigin={{ horizontal: 'right', vertical: 'top' }} - anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} - PaperProps={{ sx: { mt: 1, minWidth: 200 } }} - > - { setAccount(null); handleLogout(); }} sx={{ color: 'error.main' }}> - - Logout - - - - - {/* High Fidelity Notification Detail Dialog */} - setSelectedNotif(null)} fullWidth maxWidth="sm"> - {selectedNotif && ( - <> - - {selectedNotif.title} - - - - - {selectedNotif.desc} - - - Operational Details - - - {selectedNotif.stats.map((stat, index) => ( - - - - {stat.label} - - - {stat.value} - - - - ))} - - - - - - + {selectedNotif.actionText && ( + - - - )} - + )} + + + )} - {/* High Fidelity Chat Message Dialog */} - - {activeChat && ( - <> - - - {activeChat.initials} - - - {activeChat.name} - - - - - - {chatLoading && activeChat.messages.length === 0 && ( - - Loading… - - )} - {!chatLoading && activeChat.messages.length === 0 && ( - - No messages yet. Say hello. - - )} - {activeChat.messages.map((msg, idx) => ( - - - - {msg.text} - - - - {msg.time} - - - ))} - - - - - setTypedMessage(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} - InputProps={{ - endAdornment: ( - - - - - - ) - }} - /> - - - )} - - + {activeChat && ( + +
+
{activeChat.initials}
+

{activeChat.name}

+ +
+
+ {chatLoading ? ( +
Loading...
+ ) : activeChat.messages.length === 0 ? ( +
No messages yet.
+ ) : ( + activeChat.messages.map((m, i) => ( +
+
+ {m.text} +
+
{m.time}
+
+ )) + )} +
+
+ setTypedMessage(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} placeholder="Type a message..." /> + +
+
+ )} + ); } diff --git a/src/layout/MainLayout/Sidebar.jsx b/src/layout/MainLayout/Sidebar.jsx index f5a5f9a..63bc3cb 100644 --- a/src/layout/MainLayout/Sidebar.jsx +++ b/src/layout/MainLayout/Sidebar.jsx @@ -1,106 +1,16 @@ -import { useState, useMemo } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { - Drawer, - Box, - List, - ListItemButton, - ListItemIcon, - ListItemText, - Typography, - Collapse, - Tooltip, - Toolbar -} from '@mui/material'; -import { alpha } from '@mui/material/styles'; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; +import { useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; +import { SideNav, SideNavSection, SideNavItem } from '@astryxdesign/core/SideNav'; +import { Text } from '@astryxdesign/core/Text'; import navItems from '@/menu/navItems'; -import Logo from '@/components/Logo'; import { isDoormileStaff } from '@/auth/session'; -export const DRAWER_WIDTH = 240; -export const MINI_WIDTH = 72; - -const BRAND_RED = '#C01227'; - -function NavLeaf({ item, open, active, depth = 0, onClick }) { - const Icon = item.icon; - - const button = ( - theme.transitions.create(['background-color', 'color', 'padding'], { - duration: theme.transitions.duration.shorter, - }), - '& .MuiListItemIcon-root': { - color: active ? BRAND_RED : 'text.secondary', - minWidth: open ? 32 : 0, - justifyContent: 'center' - }, - '&:hover': { - bgcolor: 'action.hover', - color: 'text.primary', - '& .MuiListItemIcon-root': { color: 'text.primary' } - }, - '&.Mui-selected': { - bgcolor: alpha(BRAND_RED, 0.08), - color: BRAND_RED, - '& .MuiListItemIcon-root': { color: BRAND_RED }, - '&:hover': { bgcolor: alpha(BRAND_RED, 0.12) } - } - }} - > - - {depth > 0 && !Icon ? ( - - ) : Icon ? ( - - ) : null} - - - {open && ( - - )} - - ); - - if (!open) { - return ( - - {button} - - ); - } - - return button; -} - -export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) { +export default function Sidebar({ isCollapsed, onCollapsedChange }) { const location = useLocation(); - const navigate = useNavigate(); - const expanded = open || isMobile; + const isActive = (url) => !!url && location.pathname.startsWith(url); const doormile = isDoormileStaff(); - // Partner accounts don't see Doormile-only groups/items (e.g. Hub Settings). const groups = useMemo( () => navItems @@ -110,213 +20,121 @@ export default function Sidebar({ open, mobileOpen, onMobileClose, isMobile }) { [doormile] ); - const isActive = (url) => !!(url && location.pathname.startsWith(url)); - - // Memoize initial open state to prevent recalculation on every drawer toggle - const initialOpen = useMemo(() => { - return navItems - .flatMap((g) => g.items) - .filter((i) => i.children && i.children.some((c) => isActive(c.url))) - .map((i) => i.id); - }, [location.pathname]); - - const [collapse, setCollapse] = useState(initialOpen); - - const handleToggleCollapse = (id) => { - setCollapse((prev) => - prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] - ); - }; - - const go = (url) => { - navigate(url); - if (isMobile) onMobileClose(); - }; - - const sidebarContent = ( - - {/* Top Branding Section */} - - - - - {/* Navigation Scroll Area */} - + {groups.map((grp) => ( - - {expanded && ( - - {grp.group} - - )} - - - {grp.items.map((item) => { - if (item.children) { - const opened = collapse.includes(item.id); - const childActive = item.children.some((c) => isActive(c.url)); - const Icon = item.icon; - - const headerButton = ( - expanded ? handleToggleCollapse(item.id) : go(item.children[0].url)} - sx={{ - minHeight: 44, - my: 0.25, - mx: 1.25, - px: expanded ? 1.5 : 0, - justifyContent: expanded ? 'flex-start' : 'center', - borderRadius: '8px', - color: 'text.primary', - bgcolor: childActive && !opened ? alpha(BRAND_RED, 0.04) : 'transparent', - '& .MuiListItemIcon-root': { - color: childActive ? BRAND_RED : 'text.secondary', - minWidth: expanded ? 32 : 0, - justifyContent: 'center' - }, - '&:hover': { - bgcolor: 'action.hover', - '& .MuiListItemIcon-root': { color: 'text.primary' } - } - }} - > - - - - {expanded && ( - <> - - {opened ? : } - - )} - - ); - - return ( - - {expanded ? headerButton : {headerButton}} - {expanded && ( - - - {item.children.map((c) => ( - go(c.url)} - /> - ))} - - - )} - - ); - } - - return ( - go(item.url)} - /> - ); - })} - - + + {grp.items.map((item) => ( + + ))} + ))} - + + + ); } \ No newline at end of file diff --git a/src/layout/MainLayout/index.jsx b/src/layout/MainLayout/index.jsx index 2cd1c1d..19cebdd 100644 --- a/src/layout/MainLayout/index.jsx +++ b/src/layout/MainLayout/index.jsx @@ -1,56 +1,35 @@ import { useState } from 'react'; import { Outlet } from 'react-router-dom'; -import { Box, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; +import { AppShell } from '@astryxdesign/core/AppShell'; import Header from './Header'; import Sidebar from './Sidebar'; export default function MainLayout() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('lg')); - const [open, setOpen] = useState(false); - const [mobileOpen, setMobileOpen] = useState(false); - - const toggle = () => { - if (isMobile) setMobileOpen((p) => !p); - else setOpen((p) => !p); - }; + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(true); return ( - -
- setMobileOpen(false)} - /> - - - - - - - + } + sideNav={} + mobileNav={{ breakpoint: 'lg' }} + > +
+ +
+ +
); } diff --git a/src/layout/MinimalLayout.jsx b/src/layout/MinimalLayout.jsx index d1357cf..b0d0713 100644 --- a/src/layout/MinimalLayout.jsx +++ b/src/layout/MinimalLayout.jsx @@ -1,11 +1,10 @@ import { Outlet } from 'react-router-dom'; -import { Box } from '@mui/material'; // Used by auth + maintenance pages — full-bleed, no shell. export default function MinimalLayout() { return ( - +
- +
); } diff --git a/src/main.jsx b/src/main.jsx index 99c21ec..e37f584 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,18 +1,26 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import { ThemeProvider, CssBaseline } from '@mui/material'; +import { BrowserRouter, Link } from 'react-router-dom'; -import theme from '@/theme'; +// Astryx UI CSS +import '@astryxdesign/core/reset.css'; +import '@astryxdesign/core/astryx.css'; +import '@astryxdesign/theme-neutral/theme.css'; +import './index.css'; + +import { Theme } from '@astryxdesign/core'; +import { LinkProvider } from '@astryxdesign/core/Link'; +import { ToastViewport } from '@astryxdesign/core/Toast'; +import { neutralTheme } from '@astryxdesign/theme-neutral/built'; import App from '@/App'; ReactDOM.createRoot(document.getElementById('root')).render( - - - - + + + - - - + + + + ); diff --git a/src/menu/navItems.jsx b/src/menu/navItems.jsx index c70d712..fc828c4 100644 --- a/src/menu/navItems.jsx +++ b/src/menu/navItems.jsx @@ -1,11 +1,4 @@ -import SpaceDashboardRoundedIcon from '@mui/icons-material/SpaceDashboardRounded'; -import MapRoundedIcon from '@mui/icons-material/MapRounded'; -import HailRoundedIcon from '@mui/icons-material/HailRounded'; -import MoveToInboxRoundedIcon from '@mui/icons-material/MoveToInboxRounded'; -import AltRouteRoundedIcon from '@mui/icons-material/AltRouteRounded'; -import LocalShippingRoundedIcon from '@mui/icons-material/LocalShippingRounded'; -import DeliveryDiningRoundedIcon from '@mui/icons-material/DeliveryDiningRounded'; -import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded'; +import { LayoutDashboard, Map, HandHeart, Inbox, Route, Truck, Bike, Settings } from 'lucide-react'; // ==============================|| DOORMILE HUB NAVIGATION ITEMS ||============================== // // Menu follows the parcel's real journey in plain language so any hub staff member @@ -15,39 +8,39 @@ const navItems = [ { group: 'Overview', items: [ - { id: 'dashboard', title: 'Dashboard', url: '/dashboard', icon: SpaceDashboardRoundedIcon }, - { id: 'tracking', title: 'Live Map', url: '/tracking', icon: MapRoundedIcon } + { id: 'dashboard', title: 'Dashboard', url: '/dashboard', icon: LayoutDashboard }, + { id: 'tracking', title: 'Live Map', url: '/tracking', icon: Map } ] }, { group: '1. Pick Up', items: [ - { id: 'assignments', title: 'Pickup Requests', url: '/assignments', icon: HailRoundedIcon } + { id: 'assignments', title: 'Pickup Requests', url: '/assignments', icon: HandHeart } ] }, { group: '2. Receive at Hub', items: [ - { id: 'inbound', title: 'Receive Parcels', url: '/inbound', icon: MoveToInboxRoundedIcon } + { id: 'inbound', title: 'Receive Parcels', url: '/inbound', icon: Inbox } ] }, { group: '3. Sort & Store', items: [ - { id: 'routing', title: 'Where Does It Go?', url: '/routing', icon: AltRouteRoundedIcon } + { id: 'routing', title: 'Where Does It Go?', url: '/routing', icon: Route } ] }, { group: '4. Send Out', items: [ - { id: 'dispatch', title: 'Dispatch & Transfer', url: '/dispatch', icon: LocalShippingRoundedIcon } + { id: 'dispatch', title: 'Dispatch & Transfer', url: '/dispatch', icon: Truck } ] }, { group: 'Team', items: [ - { id: 'riders', title: 'Milers', url: '/riders', icon: DeliveryDiningRoundedIcon }, - { id: 'rider-routes', title: 'Rider Routes', url: '/rider-routes', icon: AltRouteRoundedIcon } + { id: 'riders', title: 'Milers', url: '/riders', icon: Bike }, + { id: 'rider-routes', title: 'Rider Routes', url: '/rider-routes', icon: Route } ] }, { @@ -55,7 +48,7 @@ const navItems = [ // Doormile staff only — hidden for partner (restricted) accounts. doormileOnly: true, items: [ - { id: 'hub-settings', title: 'Hub Settings', url: '/hub-settings', icon: SettingsRoundedIcon, doormileOnly: true } + { id: 'hub-settings', title: 'Hub Settings', url: '/hub-settings', icon: Settings, doormileOnly: true } ] } ]; diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index ebd0859..3a9e5e8 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,206 +1,54 @@ -import { useState, useMemo, useEffect, useCallback } from 'react'; -import { - Grid, - Card, - CardHeader, - CardContent, - Typography, - Box, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, - Chip, - LinearProgress, - IconButton, - Avatar, - Button, - Divider, - Popover, - Alert, - useMediaQuery, - Snackbar, - CircularProgress -} from '@mui/material'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import dayjs from 'dayjs'; -import { alpha } from '@mui/material/styles'; -import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; -import LocalShippingIcon from '@mui/icons-material/LocalShipping'; -import AssignmentIcon from '@mui/icons-material/Assignment'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import ElectricBoltIcon from '@mui/icons-material/ElectricBolt'; -import DynamicFeedIcon from '@mui/icons-material/DynamicFeed'; -import SpeedIcon from '@mui/icons-material/Speed'; -import DeliveryDiningIcon from '@mui/icons-material/DeliveryDining'; -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; -import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'; -import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; -import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded'; -import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded'; -import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded'; +import { + Truck, FileText, AlertTriangle, RefreshCw, + Layers, Activity, Download, Info, ArrowRight +} from 'lucide-react'; +import { Card } from '@astryxdesign/core/Card'; +import { Table, proportional, pixel } from '@astryxdesign/core/Table'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import Button from '@/components/Button'; +import Panel from '@/components/Panel'; +import PageHeader from '@/components/PageHeader'; import StatCard from '@/components/StatCard'; -import { getDashboard, getInboundVehicles, getActivity, getZones, getHubReport } from '@/api/hub'; +import { getDashboard, getInboundVehicles, getZones, getHubReport, getActivity } from '@/api/hub'; import { getHubContext } from '@/auth/session'; -// Format an ISO time as a short clock label for the activity feed. +const BRAND = 'var(--color-brand)'; +const DATE_FMT = 'YYYY-MM-DD'; +const RANGE_KEY = 'hub_dashboard_range'; + const clockTime = (iso) => { const d = new Date(iso); if (Number.isNaN(d.getTime())) return ''; return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; -// Map a vehicle status to the MUI chip colour the table expects. -const vehicleColor = (status) => (status === 'Unloading' ? 'success' : status === 'On the way' ? 'info' : 'default'); - -const BRAND = '#C01227'; -const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; - -// ── Doormile-themed range calendar ─────────────────────────────────────────────── -// Click a day to set the start, click again to set the end (auto-swaps if reversed). -// In-range days get a soft red wash; the two endpoints are solid brand red. -// Renders two compact months side-by-side (single month on small screens). - -// A single compact month grid — kept lightweight so both months render fast. -function MonthGrid({ view, start, end, max, onDay }) { - const gridStart = view.startOf('month').subtract(view.startOf('month').day(), 'day'); - const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day')); - - return ( - - - {view.format('MMMM YYYY')} - - - {/* Weekday labels */} - - {DAY_LABELS.map((d, i) => ( - - {d} - - ))} - - - {/* Days */} - - {cells.map((d) => { - const inMonth = d.month() === view.month(); - const isStart = start && d.isSame(start, 'day'); - const isEnd = end && d.isSame(end, 'day'); - const isEndpoint = isStart || isEnd; - const inRange = start && end && d.isAfter(start, 'day') && d.isBefore(end, 'day'); - const isToday = d.isSame(dayjs(), 'day'); - const disabled = max && d.isAfter(max, 'day'); - return ( - - onDay(d)} - sx={{ - width: 28, height: 28, m: '1px', border: 'none', cursor: disabled ? 'default' : 'pointer', - borderRadius: 1.5, fontSize: '0.72rem', fontFamily: 'inherit', - fontWeight: isEndpoint ? 700 : 500, - color: disabled ? '#D5D9DD' : isEndpoint ? '#fff' : inMonth ? '#3C4043' : '#C4C9CE', - bgcolor: isEndpoint ? BRAND : 'transparent', - boxShadow: isToday && !isEndpoint ? `inset 0 0 0 1.5px ${BRAND}` : 'none', - transition: 'background-color .12s', - '&:hover': { bgcolor: disabled ? 'transparent' : isEndpoint ? '#9E0E20' : alpha(BRAND, 0.1) } - }} - > - {d.date()} - - - ); - })} - - - ); -} - -function RangeCalendar({ from, to, maxDate, onSelect }) { - const [view, setView] = useState(dayjs(to || from || undefined).startOf('month')); - const [anchorDate, setAnchorDate] = useState(null); // first click while picking a new range - - const start = from ? dayjs(from) : null; - const end = to ? dayjs(to) : null; - const max = maxDate ? dayjs(maxDate) : null; - - const handleDay = (d) => { - if (!anchorDate) { - setAnchorDate(d); - onSelect(d.format(DATE_FMT), d.format(DATE_FMT)); - } else { - const a = anchorDate; - const lo = d.isBefore(a) ? d : a; - const hi = d.isBefore(a) ? a : d; - onSelect(lo.format(DATE_FMT), hi.format(DATE_FMT)); - setAnchorDate(null); - } - }; - - return ( - - {/* Shared nav header spanning both months */} - - setView((v) => v.subtract(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}> - - - setView((v) => v.add(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}> - - - - - - - - - - - - ); -} - -// Live KPI cards — each maps to one field of GET /api/v1/hub/dashboard. -// `ranged: true` → the metric aggregates over the selected date range, so its -// subtitle shows the window. The rest are live "right now" figures. const KPI_DEFS = [ - { key: 'parcels_received_today', label: 'Total Parcels Received', icon: DynamicFeedIcon, color: '#1A73E8', sub: 'Received', ranged: true }, - { key: 'milers_available', label: 'Available Milers', icon: DeliveryDiningIcon, color: '#1E8E3E', sub: 'Free right now' }, - { key: 'milers_on_duty', label: 'Milers on Duty', icon: InfoOutlinedIcon, color: '#1A73E8', sub: 'Currently working' }, - { key: 'pending_pickups', label: 'Pending Pickups', icon: AssignmentIcon, color: '#F29900', sub: 'Awaiting a miler' }, - { key: 'batches_sent_today', label: 'Batches Sent', icon: LocalShippingIcon, color: '#8E24AA', sub: 'Dispatched', ranged: true }, - { key: 'exceptions', label: 'Needs Checking', icon: WarningAmberIcon, color: '#D93025', sub: 'Damaged or unclear', ranged: true } + { key: 'parcels_received_today', label: 'Total Parcels Received', icon: Layers, tone: 'blue', sub: 'Received', ranged: true }, + { key: 'milers_available', label: 'Available Milers', icon: Activity, tone: 'green', sub: 'Free right now' }, + { key: 'milers_on_duty', label: 'Milers on Duty', icon: Info, tone: 'cyan', sub: 'Currently working' }, + { key: 'pending_pickups', label: 'Pending Pickups', icon: FileText, tone: 'orange', sub: 'Awaiting a miler' }, + { key: 'batches_sent_today', label: 'Batches Sent', icon: Truck, tone: 'purple', sub: 'Dispatched', ranged: true }, + { key: 'exceptions', label: 'Needs Checking', icon: AlertTriangle, tone: 'red', sub: 'Damaged or unclear', ranged: true } ]; -const DATE_FMT = 'YYYY-MM-DD'; - -const RANGE_KEY = 'hub_dashboard_range'; - export default function Dashboard() { const today = dayjs().format(DATE_FMT); const weekAgo = dayjs().subtract(6, 'day').format(DATE_FMT); - // Persist the chosen range so it survives leaving the page and coming back - // (the component unmounts on navigation, which would otherwise reset it). + + // eslint-disable-next-line no-unused-vars const [range, setRange] = useState(() => { try { const saved = JSON.parse(localStorage.getItem(RANGE_KEY)); - // Ignore a stale future/invalid `to`; otherwise restore the saved range. if (saved?.from && saved?.to && !dayjs(saved.to).isAfter(dayjs(today))) return saved; - } catch { /* fall through to default */ } + } catch { + // ignore + } return { from: weekAgo, to: today }; }); - const [calAnchor, setCalAnchor] = useState(null); - const isMobile = useMediaQuery('(max-width:600px)'); useEffect(() => { localStorage.setItem(RANGE_KEY, JSON.stringify(range)); @@ -211,35 +59,34 @@ export default function Dashboard() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [incomingVehicles, setIncomingVehicles] = useState([]); - const [recentActivity, setRecentActivity] = useState([]); const [activeRoutes, setActiveRoutes] = useState([]); + const [recentActivity, setRecentActivity] = useState([]); const [exporting, setExporting] = useState(false); - const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' }); + + const loadRequestId = useRef(0); const loadDashboard = useCallback(async () => { + const requestId = ++loadRequestId.current; setLoading(true); setError(''); try { - const [dash, vehicles, activity, zones] = await Promise.all([ - getDashboard(range.from, range.to), + const [dash, vehicles, zones, activity] = await Promise.all([ + getDashboard(range.from, range.to).catch(() => null), getInboundVehicles().catch(() => null), - getActivity(8).catch(() => null), - getZones().catch(() => null) + getZones().catch(() => null), + getActivity(8).catch(() => null) ]); + if (loadRequestId.current !== requestId) return; + setKpis(dash?.data || {}); setIncomingVehicles( (vehicles?.data || []).map((v) => ({ id: v.vehicleno || `Trip ${v.tripsheetid}`, origin: v.origin || '—', - estTime: v.eta || '', status: v.status || 'On the way', - progress: v.unloadedpct || 0, - color: vehicleColor(v.status) + progress: v.unloadedpct || 0 })) ); - setRecentActivity( - (activity?.data || []).map((a) => ({ time: clockTime(a.time), type: a.type, text: a.text })) - ); setActiveRoutes( (zones?.data || []).map((z) => ({ zone: z.zonename ? `${z.zonename} (${z.zone})` : z.zone, @@ -248,52 +95,40 @@ export default function Dashboard() { status: z.status || 'Active' })) ); + setRecentActivity( + (activity?.data || []).map((a) => ({ time: clockTime(a.time), type: a.type, text: a.text })) + ); } catch (err) { - setError(err?.message || 'Could not load dashboard stats.'); + if (loadRequestId.current === requestId) { + setError(err?.message || 'Could not load dashboard stats.'); + } } finally { - setLoading(false); + if (loadRequestId.current === requestId) { + setLoading(false); + } } }, [range.from, range.to]); - // Re-fetch whenever the range (or loadDashboard) changes. Debounced so picking a - // start-then-end date in the calendar fires one request for the final range, - // not an extra one for the transient single-day selection. useEffect(() => { - const t = setTimeout(loadDashboard, 250); - return () => clearTimeout(t); + loadDashboard(); }, [loadDashboard]); - // Pull the full report for the selected range and download it as an Excel file. const handleExport = useCallback(async () => { if (exporting) return; - if (dayjs(range.to).isBefore(dayjs(range.from))) { - setToast({ open: true, msg: 'The "to" date is before the "from" date. Fix the range first.', severity: 'warning' }); - return; - } setExporting(true); try { - // Lazy-load the Excel builder (pulls in the heavy xlsx lib) only on first export. const [{ downloadHubReport }, res] = await Promise.all([ import('@/lib/hubReport'), getHubReport(range.from, range.to) ]); downloadHubReport(res?.data || {}, { hubName: hub.hubname || 'Hub', from: range.from, to: range.to }); - setToast({ open: true, msg: 'Report downloaded.', severity: 'success' }); } catch (err) { - const notReady = err?.status === 404; - setToast({ - open: true, - msg: notReady - ? 'Report endpoint not available yet. Ask the backend to add GET /hub/report?from=&to=.' - : err?.message || 'Could not generate the report.', - severity: notReady ? 'warning' : 'error' - }); + console.error(err); } finally { setExporting(false); } }, [exporting, range.from, range.to, hub.hubname]); - // Short label for the selected window, appended to range-scoped KPI subtitles. const rangeLabel = useMemo(() => { if (range.from === today && range.to === today) return 'today'; if (range.from === range.to) return `on ${dayjs(range.from).format('DD MMM')}`; @@ -306,446 +141,251 @@ export default function Dashboard() { value: kpis && kpis[d.key] != null ? Number(kpis[d.key]).toLocaleString('en-IN') : '—' })); - // Inclusive day count for the chosen window (min 1); drives cumulative metrics. - const dayCount = useMemo(() => { - const from = dayjs(range.from); - const to = dayjs(range.to); - if (!from.isValid() || !to.isValid() || to.isBefore(from)) return 1; - return to.diff(from, 'day') + 1; - }, [range]); + const sorted = Number(kpis?.parcels_sorted ?? 0); + const target = Number(kpis?.sorting_target ?? 0); + const pct = target > 0 ? Math.min(100, Math.round((sorted / target) * 100)) : 0; - const invalidRange = dayjs(range.to).isBefore(dayjs(range.from)); - - const applyPreset = (days) => { - setRange({ from: dayjs().subtract(days - 1, 'day').format(DATE_FMT), to: today }); + const getVehicleBadgeVariant = (status) => { + const s = String(status).toLowerCase(); + if (s.includes('unloading')) return 'positive'; + if (s.includes('way')) return 'info'; + return 'neutral'; }; - const isPreset = (days) => - range.to === today && range.from === dayjs().subtract(days - 1, 'day').format(DATE_FMT); + const getZoneBadgeVariant = (status) => { + const s = String(status).toLowerCase(); + if (s.includes('active')) return 'positive'; + if (s.includes('need') || s.includes('pending')) return 'warning'; + return 'neutral'; + }; + + const trucksColumns = useMemo(() => [ + { + key: 'id', + header:
Truck
, + width: proportional(1), + renderCell: (c) => ( +
+ {c.id} +
+ ) + }, + { + key: 'origin', + header: 'Coming From', + width: proportional(1.5), + renderCell: (c) => {c.origin} + }, + { + key: 'status', + header: 'Status', + width: pixel(140), + renderCell: (c) => + }, + { + key: 'progress', + header: 'Unloaded', + width: pixel(120), + renderCell: (c) => c.progress > 0 ? ( +
+
+
+
+ {c.progress}% +
+ ) : + } + ], []); + + const zonesColumns = useMemo(() => [ + { + key: 'zone', + header:
Area
, + width: proportional(2), + renderCell: (c) => ( +
+ {c.zone} +
+ ) + }, + { + key: 'packages', + header: 'Parcels', + width: pixel(100), + align: 'end', + renderCell: (c) => {c.packages} + }, + { + key: 'riders', + header: 'Milers', + width: pixel(100), + align: 'end', + renderCell: (c) => {c.riders} + }, + { + key: 'status', + header: 'Status', + width: pixel(140), + renderCell: (c) => + } + ], []); return ( - - {/* Title + date-range filter, all on one line */} - - {/* Left Title Section */} - - - {hub.hubname || 'Doormile Hub'} - - - Live hub snapshot{hub.city ? ` · ${hub.city}` : ''} - - +
+ + + +
+ } + /> - {/* Right Controls Section */} - - {/* Refresh Button */} - - - - - {/* Quick Presets */} - {[ - { label: 'Today', days: 1 }, - { label: 'Last 7 days', days: 7 }, - { label: 'Last 30 days', days: 30 } - ].map((p) => { - const active = isPreset(p.days); - return ( - applyPreset(p.days)} - sx={{ - height: 36, - fontSize: '0.825rem', - fontWeight: 600, - borderRadius: 2, - bgcolor: active ? BRAND : '#F1F5F9', - color: active ? '#ffffff' : '#475569', - boxShadow: active ? '0 4px 10px rgba(192,18,39,0.15)' : 'none', - '& .MuiChip-label': { px: 1.5 }, - '&:hover': { bgcolor: active ? '#9E0E20' : '#E2E8F0' } - }} - /> - ); - })} - - {/* Range Trigger */} - - - {/* Export report (Excel) for the selected date range */} - - -
- - {/* Themed calendar popover */} - setCalAnchor(null)} - // On mobile the trigger sits on the left, so a right-aligned popover ran off - // the screen edge. Left-align on mobile and keep MUI clamping it into view. - anchorOrigin={{ vertical: 'bottom', horizontal: isMobile ? 'left' : 'right' }} - transformOrigin={{ vertical: 'top', horizontal: isMobile ? 'left' : 'right' }} - marginThreshold={12} - PaperProps={{ sx: { mt: 1, borderRadius: 2.5, border: '1px solid #EEF0F2', boxShadow: '0 8px 28px rgba(0,0,0,0.10)', overflow: 'hidden', maxWidth: 'calc(100vw - 24px)' } }} - > - - Select date range - - {dayjs(range.from).format('DD MMM')} – {dayjs(range.to).format('DD MMM YYYY')} · {dayCount} {dayCount === 1 ? 'day' : 'days'} - - - setRange({ from, to })} - /> - - - - - - - - {/* Live stats error banner */} {error && ( - setError('')} sx={{ borderRadius: 2, mb: 2 }}> - {error} - +
+ {error} +
)} - {/* Metrics Row — CSS grid with minmax(0,1fr) never overflows on mobile */} - + {/* Overview Grid */} +
{kpiCards.map((s) => ( - + ))} - +
- {/* Operations Grid */} - +
- {/* Sorting Progress & Incoming Vehicles */} - - + {/* Left Column: Progress & Trucks */} +
+
- {/* Sorting Station Overview */} - - - - - {(() => { - // Real sorting progress from the dashboard KPIs: parcels sorted today - // against the hub's configured capacity (sorting_target). - const sorted = Number(kpis?.parcels_sorted ?? 0); - const target = Number(kpis?.sorting_target ?? 0); - const pct = target > 0 ? Math.min(100, Math.round((sorted / target) * 100)) : 0; - return ( - - - - {target > 0 - ? `Today's target — ${target.toLocaleString('en-IN')} parcels` - : `Sorted today — ${sorted.toLocaleString('en-IN')} parcels`} - - {target > 0 && ( - - {sorted.toLocaleString('en-IN')} done · {pct}% - - )} - - - - ); - })()} - - Trucks Arriving - - - - - Truck - Coming From - Status - Unloaded - - - - {incomingVehicles.map((v) => ( - - {v.id} - {v.origin} - - - - - {v.progress > 0 ? ( - - - {v.progress}% - - ) : ( - - )} - - - ))} - -
-
-
+ {/* Progress Card */} + +
+ Sorting Progress + {target > 0 && {sorted.toLocaleString()} done · {pct}%} +
+
+ + {target > 0 ? `Today's target — ${target.toLocaleString()} parcels` : `Sorted today — ${sorted.toLocaleString()} parcels`} + +
+
+
+
- {/* Online Research: Heavy Traffic / Peak Season Control Center */} - - - - - } - /> - - - - - - - - Fast Lane - - - Urgent parcels skip the shelves and go straight from arrival to the outgoing trucks. - - - - + {/* Trucks Panel */} + +
+ + {incomingVehicles.length === 0 && !loading && ( +
+ No trucks arriving right now. +
+ )} + + - - - - - Separate Lines - - - Big, heavy boxes are sorted on a different line from small letters and envelopes. - - - - + {/* Right Column: Active Routes */} + +
+
+ {activeRoutes.length === 0 && !loading && ( +
+ No active delivery areas. +
+ )} + + - - - - - Quick Scan - - - A scanner reads each parcel automatically as it passes, so nothing is missed. - - - - - - - + + - {/* Active Delivery Routes */} - - - - - -
- - - Area - Parcels - Milers - Status - - - - {activeRoutes.map((r) => ( - - {r.zone} - {r.packages} - {r.riders} - - - - - ))} - -
- - - - - - - {/* Sidebar logs / Active updates */} - - - - - - + {/* Right Column: Activity Feed */} +
+ +
+
+ {recentActivity.length === 0 && !loading && ( + No recent activity. + )} {recentActivity.map((act, i) => ( - - - - - {act.text} - - - {act.time} - - - +
+
+
+ {act.text} + {act.time} +
+
))} - +
- - - Not sure where a parcel goes? - - - Scan it and we'll tell you exactly what to do next. - - - - - - - +
+
+
+
- setToast((t) => ({ ...t, open: false }))} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} - > - setToast((t) => ({ ...t, open: false }))} - sx={{ borderRadius: 2 }} - > - {toast.msg} - - - +
+ + +
); } diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx index e97db8d..40ba063 100644 --- a/src/pages/auth/Login.jsx +++ b/src/pages/auth/Login.jsx @@ -1,26 +1,10 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { - Box, - Card, - Stack, - Typography, - TextField, - InputAdornment, - IconButton, - Button, - Checkbox, - FormControlLabel, - Link, - Alert, - CircularProgress -} from '@mui/material'; -import Visibility from '@mui/icons-material/Visibility'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; -import BoltIcon from '@mui/icons-material/Bolt'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; +import { Zap, Truck, ShieldCheck, Eye, EyeOff, Loader2 } from 'lucide-react'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { CheckboxInput } from '@astryxdesign/core/CheckboxInput'; +import Button from '@/components/Button'; import Logo from '@/components/Logo'; import { login as loginRequest } from '@/api/hub'; import { setSession } from '@/auth/session'; @@ -56,17 +40,18 @@ export default function Login() { }; return ( - +
{/* Brand Side Panel */} - {/* Background Decorative Circles */} - - +
+
- {/* BLACK LOGO REPLACEMENT (Sidebar) */} - +
- +
- - +
+
Doormile Hub Console - - - Every parcel, -
handled with ease. -
- +
+

+ Every parcel,
handled with ease. +

+

Receive parcels, sort them, and send them out for delivery or transfer to another city all from one simple screen. - +

- +
{[ - { icon: BoltIcon, t: 'We tell you which shelf each parcel goes on' }, - { icon: LocalShippingOutlinedIcon, t: 'Scan parcels in as trucks arrive' }, - { icon: VerifiedOutlinedIcon, t: 'Keep an eye on cold-storage parcels' } + { icon: Zap, t: 'We tell you which shelf each parcel goes on' }, + { icon: Truck, t: 'Scan parcels in as trucks arrive' }, + { icon: ShieldCheck, t: 'Keep an eye on cold-storage parcels' } ].map((f) => ( - - - - - {f.t} - +
+
+ +
+
{f.t}
+
))} - - +
+
- +
© 2026 Doormile Logistics Pvt. Ltd. - - +
+
{/* Form Panel */} - - - {/* BLACK LOGO REPLACEMENT (Mobile View) */} - +
- +
- Hub Sign In - +

Hub Sign In

+

Sign in to your Doormile Hub operations account. - +

- - - +
+
+
Username / Email - - + setAuth(e.target.value)} + onChange={setAuth} onKeyDown={(e) => e.key === 'Enter' && handleSignIn()} /> - +
- - +
+
Password - - setPwd(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSignIn()} - InputProps={{ - endAdornment: ( - - setShow((s) => !s)} edge="end" size="small"> - {show ? : } - - - ) - }} - /> - +
+
+ e.key === 'Enter' && handleSignIn()} + /> + +
+
- - } - label={Remember me} - /> - +
+
+ + Remember me +
+ Forgot password? - - + +
{error && ( - +
{error} - +
)} - - - New to Hub Operations?{' '} - { e.preventDefault(); navigate('/signup'); }} - underline="hover" - sx={{ color: '#C01227', fontWeight: 600 }} - > - Create an account - - - -
- -
- +
+
+
+
+ +
); } \ No newline at end of file diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx index 4061191..9a68555 100644 --- a/src/pages/auth/Signup.jsx +++ b/src/pages/auth/Signup.jsx @@ -1,28 +1,10 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { - Box, - Card, - Grid, - Stack, - Typography, - TextField, - InputAdornment, - IconButton, - Button, - Link, - MenuItem, - FormControl, - Select, - Checkbox, - FormControlLabel -} from '@mui/material'; -import Visibility from '@mui/icons-material/Visibility'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; -import HubIcon from '@mui/icons-material/Hub'; -import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; -import VerifiedUserIcon from '@mui/icons-material/VerifiedUser'; +import { Network, UserCheck, Shield, Eye, EyeOff } from 'lucide-react'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { CheckboxInput } from '@astryxdesign/core/CheckboxInput'; +import Button from '@/components/Button'; import Logo from '@/components/Logo'; const HUBS = [ @@ -58,17 +40,18 @@ export default function Signup() { }; return ( - +
{/* Brand Side Panel */} - {/* Background Decorative Circles */} - - +
+
- {/* BLACK LOGO REPLACEMENT (Sidebar) */} - +
- +
- - +
+
Hub Registration Gateway - - - Join the Connected -
Logistics Network. -
- +
+

+ Join the Connected
Logistics Network. +

+

Create an operational profile to access state-of-the-art sorting stations, live manifest creations, and miler optimization modules. - +

- +
{[ - { icon: HubIcon, t: 'Connect to any of the 15+ nationwide hubs' }, - { icon: AssignmentIndIcon, t: 'Role-based access controls for security' }, - { icon: VerifiedUserIcon, t: 'Activity logging and dispatch compliance verification' } + { icon: Network, t: 'Connect to any of the 15+ nationwide hubs' }, + { icon: UserCheck, t: 'Role-based access controls for security' }, + { icon: Shield, t: 'Activity logging and dispatch compliance verification' } ].map((f) => ( - - - - - {f.t} - +
+
+ +
+
{f.t}
+
))} - - +
+
- +
© 2026 Doormile Logistics Pvt. Ltd. - - +
+
{/* Form Panel */} - - - {/* BLACK LOGO REPLACEMENT (Mobile View) */} - +
- +
- Request Access - +

Request Access

+

Register your credentials to join hub operations. - +

- - - - Full Name - setName(e.target.value)} /> - +
+
+
Full Name
+ +
- - Work Email - setEmail(e.target.value)} /> - +
+
Work Email
+ +
- - - Assign Hub - - - - - - Select Role - - - - - +
+
+
Assign Hub
+ +
+
+
Select Role
+ +
+
- - Password - +
Password
+
+ setPwd(e.target.value)} - InputProps={{ - endAdornment: ( - - setShow((s) => !s)} edge="end" size="small"> - {show ? : } - - - ) - }} + onChange={setPwd} /> - + +
+
- setAgree(e.target.checked)} size="small" sx={{ color: '#C01227', '&.Mui-checked': { color: '#C01227' } }} required />} - label={ - - I agree to the{' '} - Terms of Service and{' '} - Operations Guidelines. - - } - /> - -
+ + + + + +
+
+ +
); } \ No newline at end of file diff --git a/src/pages/operations/Dispatch.jsx b/src/pages/operations/Dispatch.jsx index 26d4317..2d47488 100644 --- a/src/pages/operations/Dispatch.jsx +++ b/src/pages/operations/Dispatch.jsx @@ -1,58 +1,57 @@ -import { useState, useEffect, useCallback } from 'react'; -import { - Box, - Typography, - Card, - CardContent, - CardHeader, - Grid, - Button, - Stack, - TextField, - MenuItem, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Chip, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Snackbar, - Alert, - Avatar, - Divider, - useMediaQuery, - CircularProgress -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import LocalShippingIcon from '@mui/icons-material/LocalShipping'; -import AddIcon from '@mui/icons-material/Add'; -import SendIcon from '@mui/icons-material/Send'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import EastRoundedIcon from '@mui/icons-material/EastRounded'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import SwapHorizOutlinedIcon from '@mui/icons-material/SwapHorizOutlined'; - +/* eslint-disable react/prop-types */ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { Truck, Plus, ArrowRight } from 'lucide-react'; import dayjs from 'dayjs'; + +import { Card } from '@astryxdesign/core/Card'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { Banner } from '@astryxdesign/core/Banner'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { Table, proportional, pixel } from '@astryxdesign/core/Table'; +import { HStack, VStack } from '@astryxdesign/core/Layout'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { useToast } from '@astryxdesign/core/Toast'; + +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; import PageHeader from '@/components/PageHeader'; import DateRangePicker, { DATE_FMT } from '@/components/DateRangePicker'; import { getBatchesRange, createBatch, updateBatchStatus } from '@/api/hub'; import { getHubContext } from '@/auth/session'; +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} + const STATUS_META = { - Preparing: { color: '#B06000', bg: '#FEF7E0', label: 'Preparing' }, - Ready: { color: '#1A73E8', bg: '#E8F0FE', label: 'Ready to send' }, - Sent: { color: '#1E8E3E', bg: '#E6F4EA', label: 'Sent' } + Preparing: { variant: 'warning', label: 'Preparing' }, + Ready: { variant: 'info', label: 'Ready to send' }, + Sent: { variant: 'success', label: 'Sent' } }; -// API batch status (Draft/Ready/Dispatched) → the label set this page renders. const API_TO_UI_STATUS = { Draft: 'Preparing', Ready: 'Ready', Dispatched: 'Sent' }; +const ROUTE_OPTIONS = [ + { value: 'Transfer to Mumbai Hub', label: 'Transfer to another city — Mumbai Hub' }, + { value: 'Transfer to Bengaluru Hub', label: 'Transfer to another city — Bengaluru Hub' }, + { value: 'Local Delivery: Dwarka', label: 'Local Delivery — Dwarka' }, + { value: 'Local Delivery: Saket', label: 'Local Delivery — Saket' }, + { value: 'Local Delivery: Rohini', label: 'Local Delivery — Rohini' } +]; + const timeAgo = (iso, verb = 'Created') => { if (!iso) return `${verb} recently`; const then = new Date(iso).getTime(); @@ -65,13 +64,22 @@ const timeAgo = (iso, verb = 'Created') => { return `${verb} ${Math.round(hrs / 24)} d ago`; }; +// Route/destination color per batch status — same categorical tokens as +// Badge, so the highlighted destination reads as part of the same status +// language as the rest of the app instead of a one-off hex value. +const destinationTone = (variant) => { + if (variant === 'warning') return 'var(--color-warning)'; + if (variant === 'info') return 'var(--color-text-primary)'; + return 'var(--color-icon-green)'; +}; + export default function Dispatch() { - const theme = useTheme(); - const isMdDown = useMediaQuery(theme.breakpoints.down('md')); + const isMdDown = useMediaQuery('(max-width: 900px)'); const hub = getHubContext(); const hubName = hub.hubname || 'this hub'; + const toast = useToast(); + const notify = (msg, status = 'success') => toast({ body: msg, type: status }); - // Real backend: tripsheetno / route / destination / item_count / kind (no vehicle field). const mapBatch = useCallback( (b) => ({ tripsheetid: b.tripsheetid, @@ -96,7 +104,6 @@ export default function Dispatch() { const [creating, setCreating] = useState(false); const [busyId, setBusyId] = useState(null); - // Date range for the batch history. Defaults to today. const today = dayjs().format(DATE_FMT); const [range, setRange] = useState({ from: today, to: today }); const isToday = range.from === today && range.to === today; @@ -106,15 +113,11 @@ export default function Dispatch() { ? dayjs(range.from).format('DD MMM') : `${dayjs(range.from).format('DD MMM')} – ${dayjs(range.to).format('DD MMM')}`; - // Create form state const [newRoute, setNewRoute] = useState('Transfer to Mumbai Hub'); const [newDestination, setNewDestination] = useState(''); const [newVehicle, setNewVehicle] = useState(''); const [newPkgsCount, setNewPkgsCount] = useState('5'); - // Toast state - const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' }); - const load = useCallback(async () => { setLoading(true); setLoadError(''); @@ -136,7 +139,7 @@ export default function Dispatch() { e.preventDefault(); if (creating) return; if (!newVehicle.trim() || !newDestination.trim()) { - setToast({ open: true, msg: 'Enter the destination and the miler / vehicle.', severity: 'warning' }); + notify('Enter the destination and the miler / vehicle.', 'warning'); return; } const kind = newRoute.toLowerCase().startsWith('transfer') ? 'transfer' : 'local'; @@ -153,16 +156,15 @@ export default function Dispatch() { setOpenModal(false); setNewVehicle(''); setNewDestination(''); - setToast({ open: true, msg: `Batch ${label} created successfully`, severity: 'success' }); + notify(`Batch ${label} created successfully`); load(); } catch (err) { - setToast({ open: true, msg: err?.message || 'Could not create the batch.', severity: 'error' }); + notify(err?.message || 'Could not create the batch.', 'error'); } finally { setCreating(false); } }; - // Move a batch to the next status via PATCH. const advance = async (m, apiStatus, uiStatus, okMsg) => { if (busyId) return; setBusyId(m.tripsheetid); @@ -175,9 +177,9 @@ export default function Dispatch() { : x ) ); - setToast({ open: true, msg: okMsg, severity: 'success' }); + notify(okMsg); } catch (err) { - setToast({ open: true, msg: err?.message || 'Could not update this batch.', severity: 'error' }); + notify(err?.message || 'Could not update this batch.', 'error'); } finally { setBusyId(null); } @@ -186,245 +188,259 @@ export default function Dispatch() { const handleSeal = (m) => advance(m, 'Ready', 'Ready', `Batch ${m.id} checked and ready to send.`); const handleDispatch = (m) => advance(m, 'Dispatched', 'Sent', `Batch ${m.id} sent out! The miler/driver has been notified.`); - // Action button shown for each batch based on its status const BatchAction = ({ m, fullWidth }) => { const isBusy = busyId === m.tripsheetid; if (m.status === 'Preparing') { return ( - ); } if (m.status === 'Ready') { return ( - ); } - return } label="Sent" color="success" variant="outlined" sx={{ fontWeight: 700 }} />; + return ; }; - // Origin → destination journey, reused in cards and table const Journey = ({ m }) => { const meta = STATUS_META[m.status] || STATUS_META.Preparing; return ( - - - {m.origin} - - {m.destination} - - {m.currentLoc} - + + + {m.origin} + + {m.destination} + + {m.currentLoc} + ); }; + const columns = useMemo(() => [ + { + key: 'batch', + header: 'Batch', + width: proportional(1.2), + renderCell: (row) => ( + + {row.id} + {row.time} + + ) + }, + { + key: 'journey', + header: 'Journey', + width: proportional(2), + renderCell: (row) => + }, + { + key: 'vehicle', + header: 'Miler / Vehicle', + width: proportional(1), + renderCell: (row) => {row.vehicle} + }, + { + key: 'parcels', + header: 'Parcels', + width: pixel(90), + renderCell: (row) => {row.packagesCount} + }, + { + key: 'status', + header: 'Status', + width: pixel(120), + renderCell: (row) => { + const meta = STATUS_META[row.status] || STATUS_META.Preparing; + return ; + } + }, + { + key: 'actions', + header: '', + width: pixel(160), + align: 'end', + renderCell: (row) => + } + ], []); + return ( - +
} /> - - {/* Manifest Actions & List */} - - - +
+ + + + + + Outgoing Batches + + {isToday ? `Each batch is a group of parcels leaving ${hubName} together` - : `${manifests.length} batch${manifests.length === 1 ? '' : 'es'} · ${rangeLabel}` - } - action={ - - } - /> - - - {loadError && ( - setLoadError('')} sx={{ m: 2, borderRadius: 2 }}> - {loadError} - - )} - - {loading ? ( - - - - ) : manifests.length === 0 && !loadError ? ( - - - - {isToday ? 'No outgoing batches yet. Create one to get started.' : `No batches ${rangeLabel}.`} - - - ) : isMdDown ? ( - /* ── MOBILE / TABLET: spacious cards ── */ - - {manifests.map((m) => { - const meta = STATUS_META[m.status] || STATUS_META.Preparing; - const KindIcon = m.kind === 'transfer' ? SwapHorizOutlinedIcon : TwoWheelerOutlinedIcon; - return ( - - - - - - - - - {m.id} - {m.time} - - - - - - - - - - - } label={m.vehicle} - sx={{ bgcolor: '#F1F3F5', color: '#495057', fontWeight: 600, maxWidth: '100%', '& .MuiChip-icon': { color: '#9AA0A6' } }} /> - } label={`${m.packagesCount} parcels`} - sx={{ bgcolor: '#F1F3F5', color: '#495057', fontWeight: 600, '& .MuiChip-icon': { color: '#9AA0A6' } }} /> - - - - - - ); - })} - - ) : ( - /* ── DESKTOP: spacious table ── */ - - - - - {['Batch', 'Journey', 'Miler / Vehicle', 'Parcels', 'Status', 'Action'].map((h, i) => ( - - {h} - - ))} - - - - {manifests.map((m) => { - const meta = STATUS_META[m.status] || STATUS_META.Preparing; - const KindIcon = m.kind === 'transfer' ? SwapHorizOutlinedIcon : TwoWheelerOutlinedIcon; - return ( - - - - - - - - {m.id} - {m.time} - - - - - {m.vehicle} - {m.packagesCount} - - - - - - ); - })} - -
-
- )} - - - - - {/* Creation Modal */} - setOpenModal(false)} fullWidth maxWidth="xs"> - Create a New Batch - - - - setNewRoute(e.target.value)} - > - Transfer to another city — Mumbai Hub - Transfer to another city — Bengaluru Hub - Local Delivery — Dwarka - Local Delivery — Saket - Local Delivery — Rohini - - - setNewDestination(e.target.value)} - required - /> - - setNewVehicle(e.target.value)} - required - /> - - setNewPkgsCount(e.target.value)} - inputProps={{ min: 1 }} - required - /> - - - - - - - - +
- setToast({ ...toast, open: false })} - anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} - > - setToast({ ...toast, open: false })} sx={{ width: '100%' }}> - {toast.msg} - - - + {loadError && ( +
+ +
+ )} + + {loading ? ( +
+ Loading... +
+ ) : manifests.length === 0 && !loadError ? ( +
+ + {isToday ? 'No outgoing batches yet. Create one to get started.' : `No batches ${rangeLabel}.`} + +
+ ) : isMdDown ? ( + /* ── MOBILE / TABLET: cards ── */ +
+ {manifests.map((m) => { + const meta = STATUS_META[m.status] || STATUS_META.Preparing; + return ( + + + + {m.id} + {m.time} + + + + +
+ +
+ + + + Miler / Vehicle + {m.vehicle} + + + Parcels + {m.packagesCount} + + + + +
+ ); + })} +
+ ) : ( + /* ── DESKTOP: table ── */ +
+ + + )} + + + {/* Create Batch Dialog */} + +
+ } + content={ + + + + + setNewDestination(e.target.value)} + required + /> + + setNewVehicle(e.target.value)} + required + /> + + setNewPkgsCount(e.target.value)} + min="1" + required + /> + + + } + footer={ + +
+ + +
+
+ } + /> + +
+ ); } diff --git a/src/pages/operations/HubSettings.jsx b/src/pages/operations/HubSettings.jsx index d1d3429..3da44aa 100644 --- a/src/pages/operations/HubSettings.jsx +++ b/src/pages/operations/HubSettings.jsx @@ -1,25 +1,43 @@ -import { useState, useEffect, useCallback } from 'react'; -import { - Box, Card, CardContent, CardHeader, Button, Stack, Typography, Divider, Avatar, Chip, - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, useMediaQuery, - Dialog, DialogTitle, DialogContent, DialogActions, TextField, MenuItem, InputAdornment, - IconButton, Snackbar, Alert, CircularProgress -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import SettingsRoundedIcon from '@mui/icons-material/SettingsRounded'; -import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; -import AddIcon from '@mui/icons-material/Add'; -import PersonAddAlt1OutlinedIcon from '@mui/icons-material/PersonAddAlt1Outlined'; -import Visibility from '@mui/icons-material/Visibility'; -import VisibilityOff from '@mui/icons-material/VisibilityOff'; -import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; -import HighlightOffOutlinedIcon from '@mui/icons-material/HighlightOffOutlined'; +/* eslint-disable react/prop-types */ +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { Settings, Warehouse, Plus, UserPlus, Eye, EyeOff } from 'lucide-react'; +import { Card } from '@astryxdesign/core/Card'; +import { Text } from '@astryxdesign/core/Text'; +import { HStack, VStack } from '@astryxdesign/core/Layout'; +import { Badge } from '@astryxdesign/core/Badge'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { NumberInput } from '@astryxdesign/core/NumberInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { Table, proportional, pixel } from '@astryxdesign/core/Table'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Banner } from '@astryxdesign/core/Banner'; +import { Spinner } from '@astryxdesign/core/Spinner'; +import { useToast } from '@astryxdesign/core/Toast'; + +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; import PageHeader from '@/components/PageHeader'; import { getHubs, createHub, createStaff } from '@/api/hub'; import { getHubContext } from '@/auth/session'; -const BRAND = '#C01227'; +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} + const HUB_TYPES = [ { value: 'sorting_center', label: 'Sorting Center' }, { value: 'delivery_hub', label: 'Delivery Hub' }, @@ -27,20 +45,24 @@ const HUB_TYPES = [ { value: 'warehouse', label: 'Warehouse' } ]; -const EMPTY_HUB = { hubname: '', hubtype: 'spoke', capacity: '30', contact: '', address: '', pincode: '' }; +const EMPTY_HUB = { hubname: '', hubtype: 'spoke', capacity: 30, contact: '', address: '', pincode: '' }; const EMPTY_STAFF = { hubid: '', email: '', password: '', displayname: '' }; const prettyType = (t) => HUB_TYPES.find((h) => h.value === t)?.label || t || '—'; +function StaffBadge({ has }) { + return has ? : ; +} + export default function HubSettings() { - const theme = useTheme(); - const isMdDown = useMediaQuery(theme.breakpoints.down('md')); + const isMdDown = useMediaQuery('(max-width: 900px)'); const hub = getHubContext(); + const toast = useToast(); + const notify = (msg, status = 'success') => toast({ body: msg, type: status }); const [hubs, setHubs] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); - const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' }); const [hubDialog, setHubDialog] = useState(false); const [hubForm, setHubForm] = useState(EMPTY_HUB); @@ -51,8 +73,6 @@ export default function HubSettings() { const [showPwd, setShowPwd] = useState(false); const [savingStaff, setSavingStaff] = useState(false); - const notify = (msg, severity = 'success') => setToast({ open: true, msg, severity }); - const load = useCallback(async () => { setLoading(true); setLoadError(''); @@ -80,7 +100,8 @@ export default function HubSettings() { setStaffDialog(true); }; - const submitHub = async () => { + const submitHub = async (e) => { + e.preventDefault(); if (savingHub) return; if (!hubForm.hubname.trim()) { notify('Enter a hub name.', 'warning'); @@ -91,7 +112,7 @@ export default function HubSettings() { await createHub({ hubname: hubForm.hubname.trim(), hubtype: hubForm.hubtype, - capacity: parseInt(hubForm.capacity, 10) || 0, + capacity: hubForm.capacity || 0, contact: hubForm.contact.trim(), address: hubForm.address.trim(), pincode: hubForm.pincode.trim() @@ -106,7 +127,8 @@ export default function HubSettings() { } }; - const submitStaff = async () => { + const submitStaff = async (e) => { + e.preventDefault(); if (savingStaff) return; if (!staffForm.hubid || !staffForm.email.trim() || !staffForm.password) { notify('Hub, email and password are all required.', 'warning'); @@ -131,184 +153,209 @@ export default function HubSettings() { } }; - const StaffChip = ({ has }) => - has ? ( - } label="Has login" - sx={{ fontWeight: 700, bgcolor: '#E6F4EA', color: '#1E8E3E', '& .MuiChip-icon': { color: '#1E8E3E' } }} /> - ) : ( - } label="No login" - sx={{ fontWeight: 700, bgcolor: '#FEF7E0', color: '#B06000', '& .MuiChip-icon': { color: '#B06000' } }} /> - ); + const columns = useMemo(() => [ + { + key: 'hubname', + header: 'Hub', + width: proportional(1.4), + renderCell: (h) => {h.hubname} + }, + { + key: 'hubtype', + header: 'Type', + width: proportional(1), + renderCell: (h) => {prettyType(h.hubtype)} + }, + { + key: 'capacity', + header: 'Capacity', + width: pixel(110), + renderCell: (h) => {h.capacity} + }, + { + key: 'status', + header: 'Status', + width: pixel(120), + renderCell: (h) => + }, + { + key: 'staff', + header: 'Staff Login', + width: pixel(140), + renderCell: (h) => + } + ], []); return ( - +
- - } - action={ - - - - - } - sx={{ flexWrap: 'wrap', gap: 1, '& .MuiCardHeader-action': { m: 0, alignSelf: 'center' } }} - /> - + +
+ + + + + + Hubs in {hub.city || 'your city'} + Every hub Doormile operates here + + + + + + +
{loadError && ( - setLoadError('')} sx={{ m: 2, borderRadius: 2 }}> - {loadError} - +
+ +
)} {loading ? ( - - - +
+ +
) : hubs.length === 0 && !loadError ? ( - - - No hubs yet. Add your first hub. - + } + title="No hubs yet" + description="Add your first hub to get started." + style={{ padding: '48px 0' }} + /> ) : isMdDown ? ( - +
{hubs.map((h) => ( - - - - - {h.hubname} - {prettyType(h.hubtype)} · Cap {h.capacity} - - - - + + + + {h.hubname} + {prettyType(h.hubtype)} · Cap {h.capacity} + + + ))} - +
) : ( - -
- - - {['Hub', 'Type', 'Capacity', 'Status', 'Staff Login'].map((h) => ( - - {h} - - ))} - - - - {hubs.map((h) => ( - - {h.hubname} - {prettyType(h.hubtype)} - {h.capacity} - - - - - - ))} - -
- +
+ + )} - + {/* Create Hub dialog */} - setHubDialog(false)} fullWidth maxWidth="sm"> - Add a New Hub - - - The city is set automatically from your hub — it can’t be changed here. - - - setHubForm((f) => ({ ...f, hubname: e.target.value }))} /> - - setHubForm((f) => ({ ...f, hubtype: e.target.value }))}> - {HUB_TYPES.map((t) => {t.label})} - - setHubForm((f) => ({ ...f, capacity: e.target.value }))} /> - - setHubForm((f) => ({ ...f, contact: e.target.value }))} /> - setHubForm((f) => ({ ...f, address: e.target.value }))} /> - setHubForm((f) => ({ ...f, pincode: e.target.value }))} /> - - - - - - + +
+ } + content={ + + + The city is set automatically from your hub — it can’t be changed here. + setHubForm((f) => ({ ...f, hubname: e.target.value }))} /> + +
+ setHubForm((f) => ({ ...f, hubtype: v }))} /> +
+
+ setHubForm((f) => ({ ...f, capacity: v }))} /> +
+
+ setHubForm((f) => ({ ...f, contact: e.target.value }))} /> + setHubForm((f) => ({ ...f, address: e.target.value }))} /> + setHubForm((f) => ({ ...f, pincode: e.target.value }))} /> +
+
+ } + footer={ + + + + + + + } + /> +
{/* Create Staff dialog */} - setStaffDialog(false)} fullWidth maxWidth="sm"> - Add a Hub Staff Login - - - setStaffForm((f) => ({ ...f, hubid: e.target.value }))}> - {hubs.map((h) => {h.hubname})} - - setStaffForm((f) => ({ ...f, displayname: e.target.value }))} /> - setStaffForm((f) => ({ ...f, email: e.target.value }))} /> - setStaffForm((f) => ({ ...f, password: e.target.value }))} - InputProps={{ - endAdornment: ( - - setShowPwd((s) => !s)} edge="end" size="small"> - {showPwd ? : } - - - ) - }} /> - - - - - - + +
+ } + content={ + + + ({ value: String(h.hubid), label: h.hubname }))} + value={String(staffForm.hubid)} + onChange={(v) => setStaffForm((f) => ({ ...f, hubid: v }))} + /> + setStaffForm((f) => ({ ...f, displayname: e.target.value }))} /> + setStaffForm((f) => ({ ...f, email: e.target.value }))} /> + +
+ setStaffForm((f) => ({ ...f, password: e.target.value }))} + /> +
+ : } + variant="secondary" + onClick={() => setShowPwd((s) => !s)} + /> +
+
+
+ } + footer={ + + + + + + + } + /> +
- - setToast({ ...toast, open: false })} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}> - setToast({ ...toast, open: false })} sx={{ borderRadius: 2, fontWeight: 600 }}> - {toast.msg} - - - + ); } diff --git a/src/pages/operations/Inbound.jsx b/src/pages/operations/Inbound.jsx index 83d39e9..0ee754b 100644 --- a/src/pages/operations/Inbound.jsx +++ b/src/pages/operations/Inbound.jsx @@ -1,27 +1,22 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; import { - Box, Typography, Card, CardContent, Grid, TextField, Button, Stack, - MenuItem, Table, TableBody, TableCell, TableContainer, TableHead, - TableRow, Chip, Alert, Snackbar, InputAdornment, Avatar, Divider, - useMediaQuery, CircularProgress -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import QrCodeScannerOutlinedIcon from '@mui/icons-material/QrCodeScannerOutlined'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; -import EastOutlinedIcon from '@mui/icons-material/EastOutlined'; -import MoveToInboxOutlinedIcon from '@mui/icons-material/MoveToInboxOutlined'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; -import AcUnitOutlinedIcon from '@mui/icons-material/AcUnitOutlined'; -import ScaleOutlinedIcon from '@mui/icons-material/ScaleOutlined'; -import ThermostatOutlinedIcon from '@mui/icons-material/ThermostatOutlined'; -import WarehouseOutlinedIcon from '@mui/icons-material/WarehouseOutlined'; -import HomeWorkOutlinedIcon from '@mui/icons-material/HomeWorkOutlined'; -import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; -import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined'; - + QrCode, Truck, CheckCircle2, ArrowRight, Inbox, + Package, AlertTriangle, Snowflake, + Warehouse, MapPin, Loader2, Flag +} from 'lucide-react'; import dayjs from 'dayjs'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { Button } from '@astryxdesign/core/Button'; +import { Card } from '@astryxdesign/core/Card'; +import { FormLayout } from '@astryxdesign/core/FormLayout'; +import { Field } from '@astryxdesign/core/Field'; +import { Table, proportional, pixel } from '@astryxdesign/core/Table'; +import { Badge } from '@astryxdesign/core/Badge'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { useToast } from '@astryxdesign/core/Toast'; +import { Banner } from '@astryxdesign/core/Banner'; + import PageHeader from '@/components/PageHeader'; import DateRangePicker, { DATE_FMT } from '@/components/DateRangePicker'; import StatCard from '@/components/StatCard'; @@ -37,11 +32,10 @@ const ORIGINS = [ { value: 'Client Pickup', label: 'Direct Client Pickup (Local)' } ]; -// Turn an ISO timestamp into a friendly "10 min ago" label. const timeAgo = (iso) => { if (!iso) return 'Recently'; const then = new Date(iso).getTime(); - if (Number.isNaN(then) || then < 1420070400000) return 'Recently'; // guard zero/0001 dates + if (Number.isNaN(then) || then < 1420070400000) return 'Recently'; const mins = Math.round((Date.now() - then) / 60000); if (mins < 1) return 'Just now'; if (mins < 60) return `${mins} min ago`; @@ -50,11 +44,7 @@ const timeAgo = (iso) => { return `${Math.round(hrs / 24)} d ago`; }; -// Map an API inbound record onto the shape this page renders. -// Real backend uses consignmentid / trackingno / chargeableweight / deliverypincode. const mapInbound = (row, hubName) => { - // Backend GET /hub/inbound/today sends: originname, destinationname, updatedat. - // (chargeableweight is NOT sent by this endpoint yet — see the note to backend.) const w = row.chargeableweight ?? row.deadweight ?? row.weight; return { bookingid: row.consignmentid ?? row.bookingid, @@ -62,8 +52,7 @@ const mapInbound = (row, hubName) => { sender: row.sendername || (row.senderid ? `Sender #${row.senderid}` : '—'), origin: row.originname || row.origin || (row.originhubid ? `Hub ${row.originhubid}` : '—'), currentLoc: hubName, - destination: - row.destinationname || row.destination || row.deliverypincode || (row.destinationhubid ? `Hub ${row.destinationhubid}` : '—'), + destination: row.destinationname || row.destination || row.deliverypincode || (row.destinationhubid ? `Hub ${row.destinationhubid}` : '—'), weight: typeof w === 'string' ? w : w != null ? `${w} kg` : '—', condition: row.condition || 'Good', temp: row.temperature || 'N/A', @@ -72,18 +61,12 @@ const mapInbound = (row, hubName) => { }; }; -const shelfStyle = (shelf) => { - if (shelf === 'Exception Area') return { color: '#D93025', bg: '#FCE8E6' }; - if (shelf.includes('Cold')) return { color: '#00838F', bg: '#E0F7FA' }; - return { color: '#1A73E8', bg: '#E8F0FE' }; -}; const isGood = (c) => c === 'Good'; export default function Inbound() { - const theme = useTheme(); - const isMdDown = useMediaQuery(theme.breakpoints.down('md')); const hub = getHubContext(); const hubName = hub.hubname || 'this hub'; + const toast = useToast(); const [bookingId, setBookingId] = useState(''); const [trackingId, setTrackingId] = useState(''); @@ -99,9 +82,7 @@ export default function Inbound() { const [inboundLogs, setInboundLogs] = useState([]); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); - const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' }); - // Date range for the "Recently Received" history. Defaults to today. const today = dayjs().format(DATE_FMT); const [range, setRange] = useState({ from: today, to: today }); const isToday = range.from === today && range.to === today; @@ -122,9 +103,7 @@ export default function Inbound() { } finally { setLoading(false); } - // hubName is derived from a stable localStorage read; safe to omit. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [range.from, range.to]); + }, [range.from, range.to, hubName]); useEffect(() => { loadInbound(); @@ -138,7 +117,6 @@ export default function Inbound() { return { received, exceptions, coldChain, pendingSort }; }, [inboundLogs]); - // Local preview of the shelf the backend is likely to recommend (it decides for real). const recommendShelf = () => { if (condition.includes('Damaged') || condition.includes('Wet') || condition.includes('Missing')) return 'Exception Area'; if (temp !== 'N/A' && temp !== '') return 'Zone C (Cold Room)'; @@ -149,7 +127,7 @@ export default function Inbound() { e.preventDefault(); if (submitting) return; if (!bookingId.trim() || !trackingId.trim()) { - setToast({ open: true, msg: 'Enter the booking ID and tracking ID to scan a parcel in.', severity: 'warning' }); + toast({ body: 'Enter the booking ID and tracking ID to scan a parcel in.', type: 'warning' }); return; } @@ -165,7 +143,6 @@ export default function Inbound() { const data = res?.data || {}; const shelf = data.recommended_shelf || recommendShelf(); - // Optimistically prepend, then refresh from the server for the source of truth. setInboundLogs((prev) => [ { bookingid: data.bookingid ?? bookingId, @@ -182,284 +159,288 @@ export default function Inbound() { }, ...prev ]); - setToast({ open: true, msg: `${data.trackingnumber || trackingId.trim()} received · routed to ${shelf}`, severity: 'success' }); + toast({ body: `${data.trackingnumber || trackingId.trim()} received · routed to ${shelf}`, type: 'success' }); setBookingId(''); setTrackingId(''); setCustomer(''); setSenderAddress(''); setDestination(''); setWeight(''); setCondition('Good'); setTemp(''); loadInbound(); } catch (err) { - // 404 = the booking ID doesn't exist. This screen receives an EXISTING - // booking into the hub; it does not create a new parcel. Make that clear. const notFound = err?.status === 404 || /not found/i.test(err?.message || ''); - setToast({ - open: true, - msg: notFound + toast({ + body: notFound ? `No booking found with ID "${bookingId.trim()}". This screen receives a booking that already exists — enter a Booking ID from the system (e.g. an unassigned pickup).` : err?.message || 'Could not scan this parcel in.', - severity: notFound ? 'warning' : 'error' + type: notFound ? 'warning' : 'error' }); } finally { setSubmitting(false); } }; - const fieldSx = { '& .MuiOutlinedInput-root': { borderRadius: 2 } }; - - // Reusable journey block - const Journey = ({ log }) => ( - - - {log.origin} - - {log.currentLoc} - - {log.destination.split(',')[0]} - - - - {log.destination} - - - ); + const columns = useMemo(() => [ + { + key: 'parcel', + header:
Parcel Info
, + width: pixel(224), + renderCell: (row) => ( +
+
{row.trackingId}
+
+ {row.weight} + + {row.time} +
+
+ ) + }, + { + key: 'journey', + width: proportional(2), + header:
Journey
, + renderCell: (row) => ( +
+
+
+ +
+
+
{row.origin}
+
Origin
+
+
+
+
+ +
+
+
{row.destination.split(',')[0]}
+
{row.destination}
+
+
+
+ ) + }, + { + key: 'condition', + header:
Condition
, + width: pixel(140), + renderCell: (row) => ( +
+ + {row.temp !== 'N/A' && ( +
+ {row.temp} +
+ )} +
+ ) + }, + { + key: 'shelf', + header:
Placement
, + width: pixel(180), + renderCell: (row) => { + let variant = 'info'; + if (row.shelf === 'Exception Area') variant = 'error'; + if (row.shelf.includes('Cold')) variant = 'neutral'; + return ( +
+ +
+ ); + } + } + ], []); return ( - - - {/* ── Header ── */} +
} /> - {/* ── KPI strip ── */} - - {[ - { icon: MoveToInboxOutlinedIcon, label: isToday ? 'Received Today' : 'Received', value: stats.received, color: '#1A73E8', bg: '#E8F0FE' }, - { icon: Inventory2OutlinedIcon, label: 'To Sort', value: stats.pendingSort, color: '#B06000', bg: '#FEF7E0' }, - { icon: WarningAmberOutlinedIcon, label: 'Needs Checking', value: stats.exceptions, color: '#D93025', bg: '#FCE8E6' }, - { icon: AcUnitOutlinedIcon, label: 'Cold Items', value: stats.coldChain, color: '#00838F', bg: '#E0F7FA' }, - ].map((s, i) => ( - - - - ))} - + {/* KPI strip - Ultra Compact */} +
+ +
+
+
+
{isToday ? 'Received Today' : 'Received'}
+
{loading ? '...' : stats.received}
+
+
- {/* ── Main ── */} - +
+ +
+
+
+
To Sort
+
{loading ? '...' : stats.pendingSort}
+
+
+ +
+ +
+
+
+
Needs Checking
+
{loading ? '...' : stats.exceptions}
+
+
+ +
+ +
+
+
+
Cold Items
+
{loading ? '...' : stats.coldChain}
+
+
+
+ +
{/* Scanner Panel */} - - - - - - - +
+ +
+
+ +
+
+ Add a Parcel + Record a parcel arriving at {hubName} +
+
- - - Add a Parcel - +
+ + + + - - Record a parcel arriving at {hubName} - -
- - - - - - - setBookingId(e.target.value)} sx={fieldSx} required - helperText="Must be an existing booking in the system (this receives it — it doesn't create a new one)" - InputProps={{ startAdornment: }} /> + + + - setTrackingId(e.target.value)} sx={fieldSx} - InputProps={{ - startAdornment: - }} /> + + + - setOrigin(e.target.value)} sx={fieldSx} - InputProps={{ startAdornment: }}> - {ORIGINS.map((o) => {o.label})} - + + + - setSenderAddress(e.target.value)} sx={fieldSx} /> + + + - setDestination(e.target.value)} required sx={fieldSx} - InputProps={{ startAdornment: }} /> +
+ + + + + + +
- - setWeight(e.target.value)} sx={fieldSx} - InputProps={{ startAdornment: }} /> - setCondition(e.target.value)} sx={fieldSx}> - Good - Damaged Box - Wet / Crushed - Missing Label - - + + + - setTemp(e.target.value)} sx={fieldSx} - InputProps={{ startAdornment: }} /> - - -
-
-
+
+ +
-
+
{/* Ledger Panel */} - - - - - - - - - - Recently Received - Parcels logged at the hub {rangeLabel} - - - - - - +
+ +
+
+
+ +
+
+ Recently Received + Parcels logged at the hub {rangeLabel} +
+
+
+ {inboundLogs.length} total +
+
{loadError && ( - setLoadError('')} sx={{ m: 2, borderRadius: 2 }}> - {loadError} - +
+ +
)} {loading ? ( - - - - ) : inboundLogs.length === 0 && !loadError ? ( - - - - {isToday ? 'No parcels received yet today.' : `No parcels received ${rangeLabel}.`} - - - ) : isMdDown ? ( - - {inboundLogs.map((log, i) => { - const ss = shelfStyle(log.shelf); - return ( - - - - {log.trackingId} - - - {log.time} - - - - - } label={log.weight} - sx={{ bgcolor: '#F1F3F5', color: '#495057', fontWeight: 600, '& .MuiChip-icon': { color: '#9AA0A6' } }} /> - - {log.temp !== 'N/A' && ( - } label={log.temp} - sx={{ bgcolor: '#E0F7FA', color: '#00838F', fontWeight: 600, '& .MuiChip-icon': { color: '#00838F' } }} /> - )} - } label={log.shelf} - sx={{ bgcolor: ss.bg, color: ss.color, fontWeight: 600, '& .MuiChip-icon': { color: ss.color } }} /> - - - - ); - })} - +
+ +
) : ( - /* ── DESKTOP: table with horizontal scroll guard ── */ - -
- - - {['Parcel No.', 'Journey', 'Weight', 'Condition', 'Temp', 'Goes On Shelf', 'Time'].map((h, i) => ( - = 2 && i <= 4 ? 'center' : i === 6 ? 'right' : 'left'} - sx={{ fontWeight: 700, fontSize: '0.7rem', color: '#6C757D', textTransform: 'uppercase', letterSpacing: 0.5, borderBottom: '1px solid #ECEEF1', py: 1.5, whiteSpace: 'nowrap' }}> - {h} - - ))} - - - - {inboundLogs.map((log, index) => { - const ss = shelfStyle(log.shelf); - return ( - - {log.trackingId} - - {log.weight} - - - - {log.temp} - - } label={log.shelf} - sx={{ fontWeight: 600, whiteSpace: 'nowrap', bgcolor: ss.bg, color: ss.color, '& .MuiChip-icon': { color: ss.color } }} /> - - {log.time} - - ); - })} - -
- +
+ } + title={isToday ? 'No parcels received yet today.' : `No parcels received ${rangeLabel}.`} + description="Waiting for incoming shipments." + style={{ padding: '64px 0' }} + /> + ) + } + /> + )} - - + + - setToast({ ...toast, open: false })} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}> - setToast({ ...toast, open: false })} - sx={{ borderRadius: 2, fontWeight: 600, boxShadow: '0 8px 24px rgba(0,0,0,0.18)' }}> - {toast.msg} - - - + + ); } diff --git a/src/pages/operations/OrderAssignment.jsx b/src/pages/operations/OrderAssignment.jsx index dc227a3..b0077e7 100644 --- a/src/pages/operations/OrderAssignment.jsx +++ b/src/pages/operations/OrderAssignment.jsx @@ -1,72 +1,54 @@ -import { useState, useEffect, useCallback } from 'react'; -import { - Box, - Typography, - Card, - CardContent, - CardHeader, - Button, - Divider, - Avatar, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Chip, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - List, - ListItemButton, - ListItemText, - ListItemAvatar, - Radio, - Badge, - Checkbox, - useMediaQuery, - CircularProgress, - Alert, - Snackbar -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import AssignmentIndIcon from '@mui/icons-material/AssignmentInd'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; -import TwoWheelerIcon from '@mui/icons-material/TwoWheeler'; -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; -import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; -import FlagOutlinedIcon from '@mui/icons-material/FlagOutlined'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import PersonOutlineRoundedIcon from '@mui/icons-material/PersonOutlineRounded'; -import StarRoundedIcon from '@mui/icons-material/StarRounded'; - +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { UserCheck, Bike, Sparkles, Star } from 'lucide-react'; import dayjs from 'dayjs'; + +import { Banner } from '@astryxdesign/core/Banner'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { Table, proportional, pixel, useTableSelection, useTableSelectionState } from '@astryxdesign/core/Table'; +import { HStack, VStack, Card } from '@astryxdesign/core/Layout'; +import { CheckboxInput } from '@astryxdesign/core/CheckboxInput'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { useToast } from '@astryxdesign/core/Toast'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; + +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; import PageHeader from '@/components/PageHeader'; import DateRangePicker, { DATE_FMT } from '@/components/DateRangePicker'; import { getBookingsRange, getMilers, assignMiler, autoAssignBooking } from '@/api/hub'; import { getHubContext } from '@/auth/session'; +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} + const PENDING = 'Pending Assignment'; -// Treat any status that isn't explicitly pending/unassigned as already handled. const isPendingStatus = (s) => { const v = (s || '').toLowerCase(); return !v || v === 'pending' || v === 'unassigned' || v === 'pending assignment'; }; -// Backend normalizes booking status to pending|assigned|picked_up|delivered|cancelled. -// Map the page's display status → the chip label + colours (and whether it's "done"). const statusChip = (status) => { - if (status === PENDING) return { label: 'Needs a miler', bg: '#FEF7E0', color: '#B06000', done: false }; - if (/^cancel/i.test(status)) return { label: 'Cancelled', bg: '#FCE8E6', color: '#D93025', done: true }; - if (/deliver/i.test(status)) return { label: 'Delivered', bg: '#E6F4EA', color: '#1E8E3E', done: true }; - if (/picked/i.test(status)) return { label: 'Picked up', bg: '#E8F0FE', color: '#1A73E8', done: true }; - if (/no miler/i.test(status)) return { label: 'No miler in range', bg: '#FCE8E6', color: '#D93025', done: false }; - if (/assigning/i.test(status)) return { label: 'Assigning…', bg: '#E8F0FE', color: '#1A73E8', done: true }; - return { label: 'Assigned', bg: '#E6F4EA', color: '#1E8E3E', done: true }; // "Assigned to X" + if (status === PENDING) return { label: 'Needs a miler', variant: 'warning' }; + if (/^cancel/i.test(status)) return { label: 'Cancelled', variant: 'error' }; + if (/deliver/i.test(status)) return { label: 'Delivered', variant: 'success' }; + if (/picked/i.test(status)) return { label: 'Picked up', variant: 'info' }; + if (/no miler/i.test(status)) return { label: 'No miler in range', variant: 'error' }; + if (/assigning/i.test(status)) return { label: 'Assigning…', variant: 'info' }; + return { label: 'Assigned', variant: 'success' }; }; const timeAgo = (iso) => { @@ -81,14 +63,12 @@ const timeAgo = (iso) => { return `${Math.round(hrs / 24)} d ago`; }; -// Real backend: snake_case fields + a parcels[] array (no packagedescription). const mapOrder = (b) => { const parcels = b.parcels || []; const totalWeight = parcels.reduce((s, p) => s + (p.weight || 0), 0); const pkg = parcels.length ? `${parcels.map((p) => p.itemcategory || 'Parcel').join(', ')}${totalWeight ? ` · ${totalWeight}kg` : ''}` : b.packagedescription || '—'; - // Ranged results carry a real status; the live "unassigned" fallback has none → PENDING. const raw = (b.status || '').toLowerCase(); let status; if (isPendingStatus(raw)) status = PENDING; @@ -97,7 +77,7 @@ const mapOrder = (b) => { else if (raw === 'picked_up') status = 'Picked up'; else status = b.milername ? `Assigned to ${b.milername}` : 'Assigned'; return { - id: b.bookingid, + id: b.bookingid ?? b.consignmentid ?? b.booking_id ?? b.id, customer: b.customer_name || b.customerName || 'Customer', pickup: b.pickup_address || b.pickupaddress || '—', drop: b.delivery_address || b.deliveryaddress || '—', @@ -116,8 +96,7 @@ const mapMiler = (m) => ({ }); export default function OrderAssignment() { - const theme = useTheme(); - const isMdDown = useMediaQuery(theme.breakpoints.down('md')); + const isMdDown = useMediaQuery('(max-width: 900px)'); const hub = getHubContext(); const [orders, setOrders] = useState([]); @@ -125,11 +104,10 @@ export default function OrderAssignment() { const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(''); const [busy, setBusy] = useState(false); - const [toast, setToast] = useState({ open: false, msg: '', severity: 'success' }); + const toast = useToast(); - const [selectedOrders, setSelectedOrders] = useState([]); + const [selectedOrders, setSelectedOrders] = useState(() => new Set()); - // Date range for the pickup-request history. Defaults to today. const today = dayjs().format(DATE_FMT); const [range, setRange] = useState({ from: today, to: today }); const isToday = range.from === today && range.to === today; @@ -139,12 +117,11 @@ export default function OrderAssignment() { ? dayjs(range.from).format('DD MMM') : `${dayjs(range.from).format('DD MMM')} – ${dayjs(range.to).format('DD MMM')}`; - // Single Assign Dialog State const [selectedOrderForAssign, setSelectedOrderForAssign] = useState(null); const [assignDialogOpen, setAssignDialogOpen] = useState(false); const [selectedMiler, setSelectedMiler] = useState(''); - const notify = (msg, severity = 'success') => setToast({ open: true, msg, severity }); + const notify = (msg, status = 'success') => toast({ body: msg, type: status }); const load = useCallback(async () => { setLoading(true); @@ -164,24 +141,15 @@ export default function OrderAssignment() { load(); }, [load]); - // Milers that can take a new pickup right now. const availableMilers = milers.filter((m) => ['Available', 'Assigned'].includes(m.status)); - const handleSelectAll = (event) => { - if (event.target.checked) { - const pendingIds = orders.filter(o => o.status === PENDING).map(o => o.id); - setSelectedOrders(pendingIds); - } else { - setSelectedOrders([]); - } - }; - - const handleSelectOne = (id) => { - if (selectedOrders.includes(id)) { - setSelectedOrders(selectedOrders.filter(selectedId => selectedId !== id)); - } else { - setSelectedOrders([...selectedOrders, id]); - } + const handleSelectOne = (id, isSelected) => { + setSelectedOrders((prev) => { + const next = new Set(prev); + if (isSelected) next.add(String(id)); + else next.delete(String(id)); + return next; + }); }; const handleOpenAssign = (order) => { @@ -190,7 +158,6 @@ export default function OrderAssignment() { setAssignDialogOpen(true); }; - // Assign a specific miler to a single booking. const handleAssign = async () => { if (!selectedMiler || busy) return; const miler = availableMilers.find((m) => m.id === selectedMiler); @@ -200,7 +167,11 @@ export default function OrderAssignment() { const res = await assignMiler(order.id, selectedMiler); const name = res?.data?.milername || miler?.name || 'miler'; setOrders((prev) => prev.map((o) => (o.id === order.id ? { ...o, status: `Assigned to ${name}` } : o))); - setSelectedOrders((prev) => prev.filter((id) => id !== order.id)); + setSelectedOrders((prev) => { + const next = new Set(prev); + next.delete(String(order.id)); + return next; + }); setAssignDialogOpen(false); notify(`Pickup #${order.id} assigned to ${name}.`); } catch (err) { @@ -210,10 +181,9 @@ export default function OrderAssignment() { } }; - // Let the AI engine assign every selected booking (POST /hub/bookings/:id/auto-assign). const handleAutoAssignAll = async () => { - if (selectedOrders.length === 0 || busy) return; - const ids = orders.filter((o) => selectedOrders.includes(o.id) && o.status === PENDING).map((o) => o.id); + if (selectedOrders.size === 0 || busy) return; + const ids = orders.filter((o) => selectedOrders.has(String(o.id)) && o.status === PENDING).map((o) => o.id); setBusy(true); let ok = 0; let pending = 0; @@ -221,7 +191,6 @@ export default function OrderAssignment() { for (const id of ids) { try { const res = await autoAssignBooking(id); - // 202 → engine still working: success:true but no miler yet. const name = res?.data?.milername; if (name) { setOrders((prev) => prev.map((o) => (o.id === id ? { ...o, status: `Assigned to ${name}` } : o))); @@ -231,272 +200,351 @@ export default function OrderAssignment() { pending += 1; } } catch (err) { - // 422 → no eligible miler in range; other codes → generic failure. failed += 1; if (err?.status === 422) { setOrders((prev) => prev.map((o) => (o.id === id ? { ...o, status: 'No miler in range' } : o))); } } } - setSelectedOrders([]); + setSelectedOrders(new Set()); setBusy(false); const parts = []; if (ok) parts.push(`assigned ${ok}`); if (pending) parts.push(`${pending} in progress`); if (failed) parts.push(`${failed} failed`); notify(`Auto-assign: ${parts.join(', ')}.`, failed ? 'warning' : 'success'); - load(); // refresh from server for the true state + load(); }; - const pendingCount = orders.filter((o) => o.status === PENDING).length; - const isAllSelected = selectedOrders.length > 0 && selectedOrders.length === pendingCount; + const { selectionConfig } = useTableSelectionState({ + data: orders, + idKey: 'id', + getIsItemSelectable: (o) => o.status === PENDING, + selectedKeys: selectedOrders, + setSelectedKeys: setSelectedOrders + }); + const selectionPlugin = useTableSelection(selectionConfig); + + const columns = useMemo(() => [ + { + key: 'request', + header: 'Request Details', + width: pixel(160), + renderCell: (row) => ( + + {row.id} + {row.package} + + ) + }, + { + key: 'journey', + header: 'Journey', + width: proportional(2), + renderCell: (row) => ( + + + {row.customer} + {row.pickup} + + + Going to + {row.drop} + + + ) + }, + { + key: 'status', + header: 'Status', + width: pixel(140), + renderCell: (row) => { + const c = statusChip(row.status); + return ; + } + }, + { + key: 'action', + header: 'Assignment', + width: pixel(180), + renderCell: (row) => { + const isAssigned = row.status !== PENDING; + const c = statusChip(row.status); + return !isAssigned ? ( + + ) : ( + + {row.status} + + ); + } + } + ], []); return ( - +
} /> - - } - action={ - - } - sx={{ flexWrap: 'wrap', gap: 1, '& .MuiCardHeader-action': { m: 0, alignSelf: 'center' } }} - /> - + + + + {isToday ? 'Waiting for a Miler' : `Pickup requests · ${rangeLabel}`} + + {isToday + ? `New pickup requests around ${hub.city || 'your city'}` + : `${orders.length} pickup request${orders.length === 1 ? '' : 's'} in this range around ${hub.city || 'your city'}`} + + + + +
{loadError && ( - setLoadError('')} sx={{ m: 2, borderRadius: 2 }}> - {loadError} - +
+ +
)} {loading ? ( - - - - ) : orders.length === 0 && !loadError ? ( - - - - {isToday ? 'No pickup requests waiting right now.' : `No pickup requests ${rangeLabel}.`} - - +
+ Loading... +
) : isMdDown ? ( /* ── MOBILE / TABLET: cards ── */ - - {orders.map((row) => { - const isSelected = selectedOrders.includes(row.id); - const isAssigned = row.status !== 'Pending Assignment'; - return ( - - - - + orders.length === 0 && !loadError ? ( + } + title={isToday ? 'No pickup requests waiting right now.' : `No pickup requests ${rangeLabel}.`} + description="There is no data available." + style={{ padding: '64px 0' }} + /> + ) : ( +
+ {orders.map((row) => { + const isSelected = selectedOrders.has(String(row.id)); + const isAssigned = row.status !== PENDING; + const c = statusChip(row.status); + return ( + + + {!isAssigned && ( - handleSelectOne(row.id)} /> + handleSelectOne(row.id, checked)} + /> )} - - {row.id} - {row.time} - - - {(() => { - const c = statusChip(row.status); - return ( - : undefined} - label={c.label} sx={{ fontWeight: 700, flexShrink: 0, bgcolor: c.bg, color: c.color, '& .MuiChip-icon': { color: c.color } }} /> - ); - })()} - + + {row.id} + {row.time} + + + + - - - - {row.customer} - - - - Pick up: {row.pickup} - - - - Going to: {row.drop} - - - - {row.package} - - + + {row.customer} + Pick up: {row.pickup} + Going to: {row.drop} + {row.package} + {!isAssigned ? ( - ) : ( - {row.status} + + {row.status} + )} - - - ); - })} - + + ); + })} +
+ ) ) : ( /* ── DESKTOP: spacious table ── */ - -
- - - - 0 && selectedOrders.length < pendingCount} - checked={isAllSelected && pendingCount > 0} - onChange={handleSelectAll} - disabled={pendingCount === 0} - /> - - {['Request', 'Pick Up From', 'Going To', 'Parcel', 'Status', 'Action'].map((h, i) => ( - - {h} - - ))} - - - - {orders.map((row) => { - const isSelected = selectedOrders.includes(row.id); - const isAssigned = row.status !== 'Pending Assignment'; - return ( - - - handleSelectOne(row.id)} disabled={isAssigned} /> - - {row.id} - - {row.customer} - - - {row.pickup} - - - {row.drop} - {row.package} - - {(() => { - const c = statusChip(row.status); - return ( - : undefined} - label={c.label} sx={{ fontWeight: 700, whiteSpace: 'nowrap', bgcolor: c.bg, color: c.color, '& .MuiChip-icon': { color: c.color } }} /> - ); - })()} - - - {!isAssigned ? ( - - ) : ( - {row.status} - )} - - - ); - })} - -
- - )} - - - setAssignDialogOpen(false)} maxWidth="sm" fullWidth> - Choose a Miler for pickup #{selectedOrderForAssign?.id} - - Milers at {hub.hubname || 'this hub'}: - {availableMilers.length === 0 ? ( - - No available milers right now. Try Auto-Assign, or check the Milers page. - - ) : ( - - {availableMilers.map((miler) => ( - setSelectedMiler(miler.id)} - sx={{ - border: '1px solid', - borderColor: selectedMiler === miler.id ? 'primary.main' : 'divider', - borderRadius: 2, - mb: 1, - bgcolor: selectedMiler === miler.id ? 'primary.lighter' : 'transparent' - }} - > - - - - - - - {miler.status} - {miler.rating != null && ( - <> - - - {miler.rating} - - )} - - } - primaryTypographyProps={{ fontWeight: 600 }} + /* ── DESKTOP: spacious table ── */ +
+ } + title={isToday ? 'No pickup requests waiting right now.' : `No pickup requests ${rangeLabel}.`} + description="There is no data available." + style={{ padding: '64px 0' }} /> - setSelectedMiler(miler.id)} /> - - ))} - - )} - - - - - - + ) + } + /> + + )} + - setToast({ ...toast, open: false })} - anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}> - setToast({ ...toast, open: false })} sx={{ borderRadius: 2, fontWeight: 600 }}> - {toast.msg} - - - + {/* Assign Dialog Overlay */} + + } + content={ + +
+ Milers at {hub.hubname || 'this hub'}: + + {availableMilers.length === 0 ? ( + + ) : ( + + {availableMilers.map((miler) => ( +
setSelectedMiler(miler.id)} + style={{ + border: `1px solid ${selectedMiler === miler.id ? 'var(--color-accent)' : 'var(--color-border)'}`, + borderRadius: 'var(--radius-inner)', + padding: '12px 16px', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + background: selectedMiler === miler.id ? 'var(--color-background-muted)' : 'transparent', + cursor: 'pointer' + }} + > + +
+ + + + +
+ + {miler.name} + + {miler.status} + {miler.rating != null && ( + <> + + + + + {miler.rating} + + )} + + +
+ +
+ ))} +
+ )} +
+
+ } + footer={ + +
+ + +
+
+ } + /> +
+ ); } diff --git a/src/pages/operations/RiderRoutes.jsx b/src/pages/operations/RiderRoutes.jsx index 4a59302..93c1897 100644 --- a/src/pages/operations/RiderRoutes.jsx +++ b/src/pages/operations/RiderRoutes.jsx @@ -1,47 +1,47 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { - Box, Typography, Card, Avatar, Chip, Stack, Button, Grid, - IconButton, List, ListItemButton, ListItemText, Collapse, Tooltip, Divider, - LinearProgress, Menu, MenuItem, Drawer, Paper -} from '@mui/material'; -import { alpha } from '@mui/material/styles'; import { MapContainer, TileLayer, Marker, Popup, Polyline, Tooltip as LTooltip, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import { + Route, Play, Pause, RotateCcw, Gauge, Eye, EyeOff, ChevronDown, ChevronUp, + Warehouse, Flag, Package, CheckCircle2, XCircle, Bike, Store, Users, Ruler, + Wallet, Phone, MapPin, Clock, User, ArrowLeft, Scale, CalendarClock, FileText +} from 'lucide-react'; +import { Card } from '@astryxdesign/core/Card'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { HStack, VStack } from '@astryxdesign/core/Layout'; +import { Grid } from '@astryxdesign/core/Grid'; +import { Avatar } from '@astryxdesign/core/Avatar'; +import { Badge } from '@astryxdesign/core/Badge'; +import { ProgressBar } from '@astryxdesign/core/ProgressBar'; +import { IconButton } from '@astryxdesign/core/IconButton'; +import { DropdownMenu } from '@astryxdesign/core/DropdownMenu'; +import { Dialog } from '@astryxdesign/core/Dialog'; +import { Layout, LayoutContent } from '@astryxdesign/core/Layout'; +import { Divider } from '@astryxdesign/core/Divider'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; + +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; +import PageHeader from '@/components/PageHeader'; import StatCard from '@/components/StatCard'; import { getRiderRoutes } from '@/api/hub'; import { getHubContext } from '@/auth/session'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded'; -import PauseRoundedIcon from '@mui/icons-material/PauseRounded'; -import ReplayRoundedIcon from '@mui/icons-material/ReplayRounded'; -import SpeedRoundedIcon from '@mui/icons-material/SpeedRounded'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; -import ExpandMoreRoundedIcon from '@mui/icons-material/ExpandMoreRounded'; -import ExpandLessRoundedIcon from '@mui/icons-material/ExpandLessRounded'; -import WarehouseRoundedIcon from '@mui/icons-material/WarehouseRounded'; -import FlagRoundedIcon from '@mui/icons-material/FlagRounded'; -import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded'; -import CancelRoundedIcon from '@mui/icons-material/CancelRounded'; -import DeliveryDiningRoundedIcon from '@mui/icons-material/DeliveryDiningRounded'; -import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; -import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import StraightenRoundedIcon from '@mui/icons-material/StraightenRounded'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; -import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; -import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; -import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined'; -import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'; -import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; -import ScaleOutlinedIcon from '@mui/icons-material/ScaleOutlined'; -import ScheduleOutlinedIcon from '@mui/icons-material/ScheduleOutlined'; -import NotesOutlinedIcon from '@mui/icons-material/NotesOutlined'; -import CallOutlinedIcon from '@mui/icons-material/CallOutlined'; -import MyLocationOutlinedIcon from '@mui/icons-material/MyLocationOutlined'; +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} // ════════════════════════════════════════════════════════════════════════════════ // Leaflet base-icon fix (same approach as TrackingMap.jsx) @@ -139,8 +139,20 @@ const moverIcon = (color) => new L.DivIcon({ const HUB = { lat: 11.0168, lng: 76.9558, label: getHubContext().hubname || 'Hub' }; // Palette assigned to milers round-robin so each route line is a distinct colour. +// These are intentionally literal (not design tokens) — they exist purely to tell +// routes apart at a glance, the same way a legend uses arbitrary swatch colours. const ROUTE_COLORS = ['#1A73E8', '#8E24AA', '#1E8E3E', '#E8710A', '#C01227', '#00838F']; +// Mixes a hex colour with white/alpha for tints — a tiny stand-in for the +// alpha() helper MUI provided, kept local since these are per-rider swatch +// colours rather than themed tokens. +const hexAlpha = (hex, a) => { + const h = hex.replace('#', ''); + const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h; + const n = parseInt(full, 16); + return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`; +}; + // Map an API rider-route (from GET /hub/rider-routes) into the structure this // page renders: a `pickup` trip with a list of order stops. Fields the API does // not provide (customer, weight, COD, slot, instructions) default gracefully. @@ -194,19 +206,18 @@ const mapRoute = (r, i) => { }; // ── helpers ──────────────────────────────────────────────────────────────────── -const initials = (n) => n.split(' ').map((w) => w[0]).slice(0, 2).join('').toUpperCase(); const inr = (n) => `₹${(Number(n) || 0).toLocaleString('en-IN')}`; const lerpPoint = (a, b, t) => ({ lat: a.lat + (b.lat - a.lat) * t, lng: a.lng + (b.lng - a.lng) * t }); const STATUS_META = { - Picked: { color: '#1A73E8', icon: CheckCircleRoundedIcon, label: 'Picked up' }, - Missed: { color: '#D93025', icon: CancelRoundedIcon, label: 'Missed' }, - 'In progress': { color: '#F29900', icon: DeliveryDiningRoundedIcon, label: 'In progress' }, - Pending: { color: '#80868B', icon: ScheduleOutlinedIcon, label: 'Pending' }, + Picked: { variant: 'success', icon: CheckCircle2, label: 'Picked up' }, + Missed: { variant: 'error', icon: XCircle, label: 'Missed' }, + 'In progress': { variant: 'warning', icon: Bike, label: 'In progress' }, + Pending: { variant: 'neutral', icon: CalendarClock, label: 'Pending' } }; const MODES = { - pickup: { label: 'Pickups', icon: StorefrontOutlinedIcon, doneLabel: 'Picked up', doneStatus: 'Picked', failStatus: 'Missed', pointLabel: 'Pickup', dash: '8 8' }, + pickup: { label: 'Pickups', icon: Store, doneLabel: 'Picked up', doneStatus: 'Picked', failStatus: 'Missed', pointLabel: 'Pickup', dash: '8 8' } }; // ════════════════════════════════════════════════════════════════════════════════ @@ -226,128 +237,107 @@ async function fetchRoadRoute(stops) { // ════════════════════════════════════════════════════════════════════════════════ // Small presentational pieces // ════════════════════════════════════════════════════════════════════════════════ -// KPI card — aliased to the shared StatCard so this strip matches every other page. -const KpiCard = (props) => ; - function DetailRow({ icon: Icon, label, value, valueColor }) { if (value === undefined || value === null || value === '') return null; return ( - - - - {label} - {value} - - + + + + {label} + {value} + + ); } -// Full order/pickup detail panel — rendered INLINE in the left column (not a modal) -// so the map stays visible and zoomed to the stop while these details are read. +// Full order/pickup detail panel — rendered in the right-side sheet so the map +// stays visible and zoomed to the stop while these details are read. function OrderDetailPanel({ data, onBack }) { const { order, rider, mode, index } = data; const meta = STATUS_META[order.status] || {}; - const StatusIcon = meta.icon || CheckCircleRoundedIcon; const isPickup = mode === 'pickup'; return ( - - {/* Header */} - - - - : )} label={isPickup ? 'Pickup' : 'Delivery'} - sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700, '& .MuiChip-icon': { color: '#fff' } }} /> - } label={meta.label || order.status} - sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} /> - - {order.customer} - #{order.orderId} - + + + + + + + {order.customer} + #{order.orderId} + + } + content={ + + + + + {isPickup ? 'Merchant / Sender' : 'Customer'} + + + + + + + - {/* Body */} - - + + Pickup Summary + + + + + + + + + - {/* Contact */} - - - {isPickup ? 'MERCHANT / SENDER' : 'CUSTOMER'} - - - - - - - + {order.cod > 0 && ( +
+ + + + CASH ON PICKUP + {inr(order.cod)} + + +
+ )} - {/* Pickup summary */} - - - PICKUP SUMMARY - - - - - - - - - - + + Route & Location + + + + {order.instructions && } + + - {order.cod > 0 && ( - - - - - CASH ON PICKUP - {inr(order.cod)} - - - - )} - - {/* Route & location */} - - - ROUTE & LOCATION - - - - - {order.instructions && } - - - - {/* Assigned miler */} - - - ASSIGNED MILER - - - {initials(rider.name)} - - {rider.name} - {rider.vehicle} · {rider.vehicleNo} · Stop {index} - - - - -
-
- - {/* Footer */} - - - - -
+ + Assigned Miler + + + + {rider.name} + {rider.vehicle} · {rider.vehicleNo} · Stop {index} + + + + + + } + footer={ +
+ + +
+ } + /> ); } @@ -355,6 +345,7 @@ function OrderDetailPanel({ data, onBack }) { // MAIN COMPONENT // ════════════════════════════════════════════════════════════════════════════════ export default function RiderRoutes() { + const isMdDown = useMediaQuery('(max-width: 900px)'); const [mode] = useState('pickup'); // pickups only const [riders, setRiders] = useState([]); // loaded from /hub/rider-routes const [loading, setLoading] = useState(true); @@ -369,7 +360,6 @@ export default function RiderRoutes() { const [playing, setPlaying] = useState(null); const [progress, setProgress] = useState(0); const [speed, setSpeed] = useState(1); - const [speedAnchor, setSpeedAnchor] = useState(null); const rafRef = useRef(null); const lastTsRef = useRef(0); const resolvedRef = useRef({}); // keys we've already fetched/attempted @@ -504,59 +494,50 @@ export default function RiderRoutes() { return { rider, path, travelled, pos }; }, [playing, progress, pathFor, riders]); + const speedMenuItems = [0.5, 1, 2, 4].map((s) => ({ + label: `${s}× speed`, + icon: speed === s ? : undefined, + onClick: () => setSpeed(s) + })); + return ( - - {/* ── Header ── */} - - - - - - - Rider Routes - - Pickups each miler covered today open any order for full details, or press play to replay the trip. - - - - - +
+ {!loading && riders.length === 0 && ( - - - No rider routes today - - Once milers are assigned pickups, their planned stops will show up here. - - + } + title="No rider routes today" + description="Once milers are assigned pickups, their planned stops will show up here." + style={{ padding: '48px 0', marginBottom: '20px' }} + /> )} - {/* ── Analysis KPI strip ── */} - - {[ - { icon: GroupsOutlinedIcon, label: 'Active Milers', value: kpi.activeRiders, sub: `of ${riders.length}`, color: '#1A73E8', bg: '#E8F0FE' }, - { icon: modeCfg.icon, label: modeCfg.label, value: kpi.orders, sub: 'pickups covered', color: '#C01227', bg: alpha('#C01227', 0.1) }, - { icon: CheckCircleRoundedIcon, label: modeCfg.doneLabel, value: kpi.done, sub: `${kpi.fail} missed`, color: '#1E8E3E', bg: '#E6F4EA' }, - { icon: StraightenRoundedIcon, label: 'Distance', value: `${kpi.km} km`, sub: 'fleet total today', color: '#8E24AA', bg: '#F3E5F5' }, - ].map((k, i) => ( - - ))} + {/* KPI strip */} + + + + + - +
{/* ── Left control panel — the milers list; clicking an order opens the - full detail in a right-side drawer (below). ── */} - - - - Milers & their {modeCfg.label.toLowerCase()} + full detail in a right-side sheet (below). ── */} +
+ + + Milers & their {modeCfg.label.toLowerCase()} {(playing || progress > 0) && ( - + )} - + - +
{riders.map((rider) => { const trip = rider[mode]; const orders = trip.stops.filter((s) => s.kind === 'order'); @@ -565,174 +546,150 @@ export default function RiderRoutes() { const isPlaying = playing === rider.id; const isVisible = visible[rider.id]; return ( - - - - {initials(rider.name)} - setExpanded(isOpen ? null : rider.id)} sx={{ flex: 1, borderRadius: 2, px: 1, py: 0.5, minWidth: 0 }}> - {rider.name}} - secondary={{done}/{orders.length} {modeCfg.doneLabel.toLowerCase()} · {trip.distanceKm} km} - /> - {isOpen ? : } - - +
+ + + + + - {/* per-rider stat chips (analysis) */} - - } label={`${orders.length} ${modeCfg.label.toLowerCase()}`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> - } label={done} sx={{ height: 24, fontWeight: 600, bgcolor: alpha('#1E8E3E', 0.1), color: '#1E8E3E' }} /> - } label={`${trip.distanceKm} km`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> - } label={`${trip.startTime}–${trip.endTime}`} sx={{ height: 24, fontWeight: 600, bgcolor: '#F1F3F5' }} /> - + {/* per-rider stat badges (analysis) */} + + + + + {/* action row */} - + - - toggleVisible(rider.id)} sx={{ border: '1px solid #E9ECEF', borderRadius: 2 }}> - {isVisible ? : } - - - + : } + onClick={() => toggleVisible(rider.id)} + /> + {isPlaying && ( - - - +
+ +
)} {/* expandable stops — rich order cards; click to open detail */} - - - - {trip.stops.map((s, i) => { - const key = `${rider.id}-${i}`; - const isHub = s.kind === 'hub'; - const meta = STATUS_META[s.status]; - const StatusIcon = meta?.icon; + {isOpen && ( + + {trip.stops.map((s, i) => { + const key = `${rider.id}-${i}`; + const meta = STATUS_META[s.status]; + const StatusIcon = meta?.icon; + return ( +
setFocusedStop(key)} + onMouseLeave={() => setFocusedStop((f) => (f === key ? null : f))} + onClick={() => openDetail(rider, s, i)} + style={{ + padding: '12px', borderRadius: 'var(--radius-element)', cursor: 'pointer', + border: `1px solid ${focusedStop === key ? hexAlpha(rider.color, 0.55) : 'var(--color-border)'}`, + background: focusedStop === key ? hexAlpha(rider.color, 0.05) : 'var(--color-background-surface)' + }} + > + + + {i} + + Order #{s.orderId} + + + {meta && : undefined} />} + - // Hub return — compact row, not a card. - if (isHub) { - return ( - - - - - - {s.label} - Back to hub · {s.time} - - - ); - } + + + + {rider.name} + + + + {s.time} + + - return ( - setFocusedStop(key)} - onMouseLeave={() => setFocusedStop((f) => (f === key ? null : f))} - onClick={() => openDetail(rider, s, i)} - sx={{ - p: 1.5, borderRadius: 2, cursor: 'pointer', transition: 'all .15s', - border: '1px solid', borderColor: focusedStop === key ? alpha(rider.color, 0.55) : '#ECEEF1', - bgcolor: focusedStop === key ? alpha(rider.color, 0.04) : '#fff', - boxShadow: '0 1px 3px rgba(0,0,0,0.03)', - '&:hover': { borderColor: alpha(rider.color, 0.55), boxShadow: '0 4px 14px rgba(0,0,0,0.07)' }, - }} - > - {/* order id + status */} - - - {i} - - Order #{s.orderId} - - - {meta && ( - } label={meta.label} - sx={{ height: 22, fontSize: '0.68rem', fontWeight: 700, color: meta.color, bgcolor: alpha(meta.color, 0.1), flexShrink: 0 }} /> - )} - + - {/* rider + time */} - - - - {rider.name} - - - - {s.time} - - + + + {s.customer} + + + + {s.address} + - - - {/* merchant + address */} - - - {s.customer} - - - - {s.address} - - - {/* metric chips — only render fields the API actually returned */} - {(s.legKm != null || s.weight || s.items != null) && ( - - {s.legKm != null && } label={`${s.legKm} km`} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} />} - {s.weight && } label={s.weight} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} />} - {s.items != null && } label={`${s.items} ${s.items === 1 ? 'parcel' : 'parcels'}`} sx={{ height: 22, fontSize: '0.68rem', fontWeight: 600, bgcolor: '#F1F3F5' }} />} - - )} - - ); - })} - - - - + {(s.legKm != null || s.weight || s.items != null) && ( + + {s.legKm != null && } + {s.weight && } + {s.items != null && } + + )} +
+ ); + })} +
+ )} +
); })} - - -
+
+
+
{/* ── Map ── */} - - +
+ {playState && ( - - - {initials(playState.rider.name)} - - {playState.rider.name} - Replaying {modeCfg.label.toLowerCase().replace(/s$/, '')} · {Math.round(progress * 100)}% - - setPlaying(null)}> - setSpeedAnchor(e.currentTarget)}> - - - +
+ + + + {playState.rider.name} + Replaying pickup · {Math.round(progress * 100)}% + + } onClick={() => setPlaying(null)} /> + , variant: 'ghost', size: 'sm', isIconOnly: true, label: 'Change speed' }} + hasChevron={false} + items={speedMenuItems} + /> + + +
)} - setSpeedAnchor(null)}> - {[0.5, 1, 2, 4].map((s) => ( { setSpeed(s); setSpeedAnchor(null); }}>{s}× speed))} - @@ -769,17 +726,17 @@ export default function RiderRoutes() { eventHandlers={{ click: () => openDetail(rider, s, i) }} > - {modeCfg.pointLabel} {i} · {s.customer} - {s.address} - #{s.orderId} · {s.time} · {STATUS_META[s.status]?.label} - openDetail(rider, s, i)}>View full details → + {modeCfg.pointLabel} {i} · {s.customer} +
{s.address}
+
#{s.orderId} · {s.time} · {STATUS_META[s.status]?.label}
+
openDetail(rider, s, i)}>View full details →
); })} {!dimmed && endStop && ( - {mode === 'pickup' ? 'Returned to hub' : 'Trip end'} · {rider[mode].endTime} + Returned to hub · {rider[mode].endTime} )} @@ -798,49 +755,38 @@ export default function RiderRoutes() {
{/* Legend */} - + {riders.map((r) => ( - toggleVisible(r.id)}> - - {r.name} - + toggleVisible(r.id)}> + + {r.name} + ))} - - - - {mode === 'pickup' ? 'Hub return' : 'Trip end'} - - - - Hub - - - - +
+ + + Hub return + + + + Hub + + +
+
- {/* ── Order detail — right-side drawer ── */} - t.zIndex.modal, - '& .MuiDrawer-paper': { - width: { xs: '100%', sm: 360 }, - maxWidth: '100%', - top: { xs: 0, sm: 64 }, - height: { xs: '100%', sm: 'calc(100% - 64px)' }, - borderTopLeftRadius: { sm: 16 }, - overflow: 'hidden', - boxShadow: '-8px 0 30px rgba(0,0,0,0.12)', - }, - }} + {/* ── Order detail — right-side sheet ── */} + { if (!o) closeDetail(); }} + width={380} + maxHeight="100vh" + style={{ height: '100vh' }} + position={{ top: 0, right: 0, bottom: 0 }} + purpose="info" > {detail && } - -
+ +
); } diff --git a/src/pages/operations/Riders.jsx b/src/pages/operations/Riders.jsx index faa227c..31a073e 100644 --- a/src/pages/operations/Riders.jsx +++ b/src/pages/operations/Riders.jsx @@ -1,56 +1,54 @@ -import React, { useState, useMemo, useEffect, useCallback } from 'react'; +/* eslint-disable react/prop-types */ +import { useState, useMemo, useEffect, useCallback } from 'react'; import { - Box, Typography, Card, CardContent, Avatar, Chip, Table, TableBody, - TableCell, TableContainer, TableHead, TableRow, IconButton, Button, - Stack, Tooltip, LinearProgress, Dialog, DialogContent, DialogActions, Drawer, - TextField, MenuItem, Divider, InputAdornment, Menu, ListItemIcon, - Snackbar, Alert, useMediaQuery, Grid, Select, FormControl, InputLabel, - Autocomplete, Switch, FormControlLabel, Paper, ToggleButtonGroup, - ToggleButton, Stepper, Step, StepLabel, CircularProgress -} from '@mui/material'; -import { useTheme, alpha } from '@mui/material/styles'; + Users, Plus, Download, Phone, MapPin, Bike, Zap, Truck, CheckCircle2, + AlertTriangle, Clock, Wallet, Pencil, ArrowLeft, ShieldCheck, LayoutGrid, Rows3, Route +} from 'lucide-react'; -// ── Professional outlined icon set ────────────────────────────────────────────── -import AddOutlinedIcon from '@mui/icons-material/AddOutlined'; -import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlineOutlined'; -import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; -import MoreVertOutlinedIcon from '@mui/icons-material/MoreVertOutlined'; -import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined'; -import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined'; -import CircleRoundedIcon from '@mui/icons-material/CircleRounded'; -import StarRoundedIcon from '@mui/icons-material/StarRounded'; -import PhoneOutlinedIcon from '@mui/icons-material/PhoneOutlined'; -import CallOutlinedIcon from '@mui/icons-material/CallOutlined'; -import PlaceOutlinedIcon from '@mui/icons-material/PlaceOutlined'; -import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; -import PersonOutlineOutlinedIcon from '@mui/icons-material/PersonOutlineOutlined'; -import PersonAddAlt1OutlinedIcon from '@mui/icons-material/PersonAddAlt1Outlined'; -import GroupsOutlinedIcon from '@mui/icons-material/GroupsOutlined'; -import TwoWheelerOutlinedIcon from '@mui/icons-material/TwoWheelerOutlined'; -import ElectricBikeOutlinedIcon from '@mui/icons-material/ElectricBikeOutlined'; -import PedalBikeOutlinedIcon from '@mui/icons-material/PedalBikeOutlined'; -import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; -import AirportShuttleOutlinedIcon from '@mui/icons-material/AirportShuttleOutlined'; -import InventoryOutlinedIcon from '@mui/icons-material/Inventory2Outlined'; -import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; -import AccessTimeOutlinedIcon from '@mui/icons-material/AccessTimeOutlined'; -import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'; -import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined'; -import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; -import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined'; -import ApartmentOutlinedIcon from '@mui/icons-material/ApartmentOutlined'; -import TableRowsOutlinedIcon from '@mui/icons-material/TableRowsOutlined'; -import GridViewOutlinedIcon from '@mui/icons-material/GridViewOutlined'; -import DownloadOutlinedIcon from '@mui/icons-material/DownloadOutlined'; -import HomeWorkOutlinedIcon from '@mui/icons-material/HomeWorkOutlined'; -import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded'; +import { Card } from '@astryxdesign/core/Card'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { HStack, VStack } from '@astryxdesign/core/Layout'; +import { Grid } from '@astryxdesign/core/Grid'; +import { Avatar } from '@astryxdesign/core/Avatar'; +import { Badge } from '@astryxdesign/core/Badge'; +import { StatusDot } from '@astryxdesign/core/StatusDot'; +import { ProgressBar } from '@astryxdesign/core/ProgressBar'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Selector } from '@astryxdesign/core/Selector'; +import { MultiSelector } from '@astryxdesign/core/MultiSelector'; +import { Switch } from '@astryxdesign/core/Switch'; +import { SegmentedControl, SegmentedControlItem } from '@astryxdesign/core/SegmentedControl'; +import { MoreMenu } from '@astryxdesign/core/MoreMenu'; +import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; +import { AlertDialog } from '@astryxdesign/core/AlertDialog'; +import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; +import { Table, proportional, pixel } from '@astryxdesign/core/Table'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { Banner } from '@astryxdesign/core/Banner'; +import { Spinner } from '@astryxdesign/core/Spinner'; +import { useToast } from '@astryxdesign/core/Toast'; +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; +import PageHeader from '@/components/PageHeader'; import StatCard from '@/components/StatCard'; import { getMilers, createMiler, updateMiler, deleteMiler } from '@/api/hub'; import { getHubContext } from '@/auth/session'; +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} + // The backend uses snake-case availability values; this page uses friendlier labels. const API_TO_UI_STATUS = { Available: 'Idle', Assigned: 'On Pickup', On_Break: 'On Break', Offline: 'Offline' }; const UI_TO_API_STATUS = { @@ -64,11 +62,11 @@ const UI_TO_API_STATUS = { // ── Reference data ────────────────────────────────────────────────────────────── const VEHICLES = { - 'Electric Bike': { capacity: 30, icon: ElectricBikeOutlinedIcon }, - 'Motorcycle': { capacity: 25, icon: TwoWheelerOutlinedIcon }, - 'Cycle': { capacity: 15, icon: PedalBikeOutlinedIcon }, - 'Cargo Van': { capacity: 120, icon: AirportShuttleOutlinedIcon }, - 'Mini Truck': { capacity: 200, icon: LocalShippingOutlinedIcon }, + 'Electric Bike': { capacity: 30, icon: Zap }, + Motorcycle: { capacity: 25, icon: Bike }, + Cycle: { capacity: 15, icon: Bike }, + 'Cargo Van': { capacity: 120, icon: Truck }, + 'Mini Truck': { capacity: 200, icon: Truck } }; const VEHICLE_TYPES = Object.keys(VEHICLES); @@ -76,13 +74,16 @@ const ALL_ZONES = ['Dwarka', 'Janakpuri', 'Saket', 'Malviya Nagar', 'Rohini', 'V const HUB_OPTIONS = ['Delhi Operations Hub', 'Mumbai Hub (BOM-02)', 'Bengaluru Hub (BLR-03)', 'Jaipur Hub (JAI-08)']; const STATUS_OPTIONS = ['On Pickup', 'Idle', 'On Break', 'Returning to Hub', 'Offline', 'Suspended']; +// Status color maps to Astryx's own categorical tokens (same palette Badge +// uses elsewhere), so status reads as part of the same visual language as +// the rest of the app instead of a one-off set of hand-picked hex values. const STATUS_META = { - 'On Pickup': { color: '#1A73E8', bg: '#E8F0FE', label: 'On Pickup' }, - 'Idle': { color: '#B06000', bg: '#FEF7E0', label: 'Idle / Available' }, - 'On Break': { color: '#8E24AA', bg: '#F3E5F5', label: 'On Break' }, - 'Returning to Hub': { color: '#0B8043', bg: '#E6F4EA', label: 'Returning' }, - 'Offline': { color: '#80868B', bg: '#F1F3F4', label: 'Offline' }, - 'Suspended': { color: '#D93025', bg: '#FCE8E6', label: 'Suspended' }, + 'On Pickup': { variant: 'blue', dot: 'accent', label: 'On Pickup' }, + Idle: { variant: 'warning', dot: 'warning', label: 'Idle / Available' }, + 'On Break': { variant: 'purple', dot: 'accent', label: 'On Break' }, + 'Returning to Hub': { variant: 'success', dot: 'success', label: 'Returning' }, + Offline: { variant: 'neutral', dot: 'neutral', label: 'Offline' }, + Suspended: { variant: 'error', dot: 'error', label: 'Suspended' } }; let idSeq = 8060; @@ -91,8 +92,6 @@ const genId = () => `RDR-${idSeq++}`; // Turn a vehicle type name into the 1-based id the backend uses (best effort). const vehicleIdFor = (vehicle) => Math.max(1, VEHICLE_TYPES.indexOf(vehicle) + 1); -// Map a raw API miler onto the rich shape this page renders. Fields the API does -// not provide (zones, COD, live load) default to empty/zero so the UI still works. // Format an ISO check-in timestamp as HH:MM (or "—"). const checkInLabel = (iso) => { if (!iso) return '—'; @@ -101,6 +100,8 @@ const checkInLabel = (iso) => { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; +// Map a raw API miler onto the rich shape this page renders. Fields the API does +// not provide (zones, COD, live load) default to empty/zero so the UI still works. const mapMiler = (m, hubName) => { // Real backend exposes defaultvehicletype (e.g. "Bike"); fall back to id lookup. const vehicle = m.defaultvehicletype || VEHICLE_TYPES[(m.vehicleid || 1) - 1] || 'Motorcycle'; @@ -141,762 +142,67 @@ const successRate = (r) => { }; const loadPct = (r) => (r.capacity === 0 ? 0 : Math.round((r.assigned / r.capacity) * 100)); -const initials = (name) => name.split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase(); const inr = (n) => `₹${n.toLocaleString('en-IN')}`; const EMPTY_FORM = { name: '', phone: '', hub: 'Delhi Operations Hub', zones: [], - vehicle: '', vehicleNo: '', status: 'Idle', verified: false, + vehicle: '', vehicleNo: '', status: 'Idle', verified: false }; +const STEPS = ['Personal', 'Assignment', 'Vehicle & KYC']; + // ════════════════════════════════════════════════════════════════════════════════ -// Presentational Components (Improved spacing & visuals) +// Presentational pieces // ════════════════════════════════════════════════════════════════════════════════ -function StatusPill({ status, size = 'md' }) { - const m = STATUS_META[status] || STATUS_META['Offline']; - const small = size === 'sm'; - return ( - - - - {m.label} - - - ); +function StatusPill({ status }) { + const m = STATUS_META[status] || STATUS_META.Offline; + return ; } function CapacityBar({ rider, showLabel = true }) { const pct = loadPct(rider); - const color = pct > 85 ? '#D93025' : pct > 60 ? '#F29900' : '#1A73E8'; + const variant = pct > 85 ? 'error' : pct > 60 ? 'warning' : 'accent'; return ( - + + {showLabel && ( - - {rider.assigned}/{rider.capacity} - · {pct}% - + + {rider.assigned}/{rider.capacity} · {pct}% + )} - - + ); } -function RiderAvatar({ rider, size = 48 }) { - const m = STATUS_META[rider.status] || STATUS_META['Offline']; +function RiderAvatar({ rider, size = 40 }) { + const m = STATUS_META[rider.status] || STATUS_META.Offline; return ( - - - {initials(rider.name)} - - - + } + /> ); } function VehicleCell({ vehicle, vehicleNo }) { - const Icon = VEHICLES[vehicle]?.icon || LocalShippingOutlinedIcon; + const Icon = VEHICLES[vehicle]?.icon || Truck; return ( - - - - - - {vehicle} - {vehicleNo} - - - ); -} - -// KPI card — the shared StatCard is the single reference design (see components/StatCard). -// Kept as a thin alias so the call sites below stay unchanged; `hover` on for the lift effect. -const KpiCard = (props) => ; - -// ════════════════════════════════════════════════════════════════════════════════ -// Dialogs (Cleaner & Better Spaced) -// ════════════════════════════════════════════════════════════════════════════════ - -const STEPS = ['Personal', 'Assignment', 'Vehicle & KYC']; - -function RiderFormDialog({ open, onClose, onSave, initial, mode }) { - const theme = useTheme(); - const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); - const [activeStep, setActiveStep] = useState(0); - const [form, setForm] = useState(initial || EMPTY_FORM); - const [errors, setErrors] = useState({}); - - React.useEffect(() => { - if (open) { - setForm(initial || EMPTY_FORM); - setErrors({}); - setActiveStep(0); - } - }, [open, initial]); - - const set = (k, v) => { - setForm(p => ({ ...p, [k]: v })); - setErrors(e => ({ ...e, [k]: undefined })); - }; - - const validateStep = (step) => { - const e = {}; - if (step === 0) { - if (!form.name.trim()) e.name = 'Full name is required'; - if (!form.phone.trim()) e.phone = 'Phone number is required'; - else if (!/[0-9]{10}/.test(form.phone.replace(/\D/g, ''))) e.phone = 'Enter a valid 10-digit phone number'; - } - if (step === 1) { - if (form.zones.length === 0) e.zones = 'Assign at least one service zone'; - } - if (step === 2) { - if (!form.vehicle) e.vehicle = 'Select a vehicle type'; - if (!form.vehicleNo.trim() && form.vehicle !== 'Cycle') e.vehicleNo = 'Vehicle number is required'; - } - setErrors(e); - return Object.keys(e).length === 0; - }; - - const next = () => { if (validateStep(activeStep)) setActiveStep(s => s + 1); }; - const back = () => setActiveStep(s => s - 1); - - const submit = () => { - if (!validateStep(2)) return; - const cap = VEHICLES[form.vehicle]?.capacity || 30; - onSave({ - ...form, - vehicleNo: form.vehicleNo.trim() || '—', - id: mode === 'add' ? genId() : form.id, - capacity: cap, - checkInTime: mode === 'add' ? '—' : form.checkInTime, - hoursToday: mode === 'add' ? 0 : form.hoursToday, - assigned: mode === 'add' ? 0 : form.assigned, - pickupsPending: mode === 'add' ? 0 : form.pickupsPending, - deliveriesPending: mode === 'add' ? 0 : form.deliveriesPending, - deliveriesDone: mode === 'add' ? 0 : form.deliveriesDone, - deliveriesFailed: mode === 'add' ? 0 : form.deliveriesFailed, - codCollected: mode === 'add' ? 0 : form.codCollected, - codPending: mode === 'add' ? 0 : form.codPending, - rating: mode === 'add' ? 5.0 : form.rating, - }); - }; - - return ( - - {/* Header */} - - - - - - - - - {mode === 'add' ? 'Onboard New Miler' : 'Edit Miler'} - - - {mode === 'add' ? 'Add delivery agent to the fleet' : `Updating ${form.name}`} - - - - - - - - - - {STEPS.map(label => ( - - {label} - - ))} - - - - - - - {/* STEP 0 */} - {activeStep === 0 && ( - - set('name', e.target.value)} error={!!errors.name} helperText={errors.name} - InputProps={{ startAdornment: }} /> - - set('phone', e.target.value)} error={!!errors.phone} helperText={errors.phone} - placeholder="+91 XXXXXXXXXX" InputProps={{ startAdornment: }} /> - - - Home Hub - - - - )} - - {/* STEP 1 */} - {activeStep === 1 && ( - - set('zones', v)} - renderInput={(params) => } - renderTags={(value, getTagProps) => value.map((option, i) => )} - /> - - {mode === 'edit' && ( - - Status - - - )} - - )} - - {/* STEP 2 */} - {activeStep === 2 && ( - - - Vehicle Type - - {errors.vehicle && {errors.vehicle}} - - - set('vehicleNo', e.target.value.toUpperCase())} error={!!errors.vehicleNo} - helperText={errors.vehicleNo || (form.vehicle === 'Cycle' ? 'Optional for cycles' : '')} - placeholder="DL-00-AB-0000" sx={{ '& input': { fontFamily: 'monospace' } }} /> - - - set('verified', e.target.checked)} color="success" />} - label={ - - - - KYC Verified - Driving licence & ID proof validated - - - } - /> - - - )} - - - - - - - {activeStep > 0 && } - {activeStep < STEPS.length - 1 ? ( - - ) : ( - - )} - - - ); -} - -// Profile Drawer - view + inline edit, in the same right-side sheet -function ProfileDrawer({ rider: riderProp, onClose, onSave, startInEdit = false }) { - const [editing, setEditing] = useState(startInEdit); - // Retain the last opened miler so the sheet keeps rendering its content while it - // slides OUT — the prop becomes null on close, and unmounting here (an early - // `return null`) would kill the exit animation and make closing feel instant. - const [retained, setRetained] = useState(riderProp); - const [form, setForm] = useState(riderProp || {}); - - // Reset the editable copy + mode whenever a different miler is opened. - useEffect(() => { - if (riderProp) { - setRetained(riderProp); - setForm(riderProp); - setEditing(startInEdit); - } - }, [riderProp, startInEdit]); - - const open = Boolean(riderProp); // drives the slide; false triggers the exit anim - const rider = riderProp || retained; // keep content during the close transition - // NOTE: we deliberately do NOT early-return when there's no rider. The Drawer stays - // mounted (open=false) from first render, so the first click is a real false→true - // transition and animates — otherwise the first open mounts already-open and skips it. - const sr = rider ? successRate(rider) : 0; - - const setField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value })); - - const cancelEdit = () => { setForm(rider); setEditing(false); }; - const saveEdit = () => { - const updated = { ...rider, ...form, capacity: VEHICLES[form.vehicle]?.capacity ?? rider.capacity }; - onSave(updated); - setEditing(false); - }; - - return ( - t.zIndex.modal, - '& .MuiDrawer-paper': { - width: { xs: '100%', sm: 520 }, - maxWidth: '100%', - // Run the sheet the full height of the viewport so there is no empty - // strip above the red header (zIndex.modal keeps Close clickable). - top: 0, - height: '100%', - borderTopLeftRadius: { sm: 16 }, - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', - boxShadow: '-8px 0 30px rgba(0,0,0,0.12)', - }, - }} - > - {rider && (<> - {/* Header */} - - - {form.name || rider.name} - {rider.id} - - - {rider.verified && ( - } label="Verified" - sx={{ bgcolor: 'rgba(255,255,255,0.18)', color: '#fff', fontWeight: 700 }} /> - )} - - - - - - {/* Content */} - - {editing ? ( - /* ── Inline edit form (same sheet) ── */ - - - EDIT MILER DETAILS - - - - - - Status - - - - Vehicle - - - - - Hub - - - setForm((f) => ({ ...f, zones: v }))} - renderInput={(params) => } - /> - - - ) : ( - - - {/* Today's Performance */} - - - TODAY'S PERFORMANCE - - - {[ - { label: "Picked up", value: rider.deliveriesDone, icon: CheckCircleOutlinedIcon, color: "#16A34A", bg: "#DCFCE7" }, - { label: "Failed", value: rider.deliveriesFailed, icon: WarningAmberOutlinedIcon, color: "#DC2626", bg: "#FEE2E2" }, - { label: "COP", value: inr(rider.codCollected), icon: PaymentsOutlinedIcon, color: "#D97706", bg: "#FEF3C7" }, - ].map((stat, i) => ( - - - - ))} - - - - {/* Live Load Panel */} - - - - LIVE LOAD - - - - - Pending pickups - {rider.pickupsPending} - - - - - {/* Miler Details Panel */} - - - - MILER DETAILS - - - {[ - { icon: PhoneOutlinedIcon, label: "Phone", value: rider.phone }, - { icon: HomeWorkOutlinedIcon, label: "Hub", value: rider.hub }, - { icon: PlaceOutlinedIcon, label: "Zones", value: rider.zones?.join(', ') || 'N/A' }, - { icon: BadgeOutlinedIcon, label: "Vehicle", value: `${rider.vehicle} • ${rider.vehicleNo}` }, - { icon: AccessTimeOutlinedIcon, label: "Check-in", value: `${rider.checkInTime} (${rider.hoursToday}h)` }, - ].map((detail, idx) => ( - - - - - {detail.label} - {detail.value} - - - - ))} - - - - - - )} - - - {/* Footer */} - - {editing ? ( - <> - - - - ) : ( - <> - - - - )} - - )} - - ); -} - -// Delete Dialog -function DeleteDialog({ rider, onClose, onConfirm }) { - return ( - - - - - - Remove Miler? - - {rider?.name} will be permanently removed from the roster. - - - - - - - - ); -} - -// Miler Card (Grid) -function RiderCard({ rider, onView, onMenu }) { - const pct = loadPct(rider); - const pctColor = - pct > 85 ? "#D93025" : pct > 60 ? "#F29900" : "#1A73E8"; - - return ( - onView(rider)} - sx={{ - height: "100%", - borderRadius: 2, - border: "1px solid #E8EAED", - cursor: "pointer", - transition: "all .25s ease", - bgcolor: "#fff", - "&:hover": { - transform: "translateY(-4px)", - borderColor: "#C01227", - boxShadow: "0 16px 40px rgba(0,0,0,.08)", - }, - }} - > - + - {/* Header */} - - - - - - - {rider.name} - - - {rider.verified && ( - - )} - - - - {rider.id} - - - - { - e.stopPropagation(); - onMenu(e, rider); - }} - > - - - - - {/* Status */} - - - - - - - {/* Load */} - - - - Today's Load - - - - {rider.assigned}/{rider.capacity} - - - {pct}% - - - - - - - - {/* Zones */} - - - SERVICE ZONES - - - - {rider.zones.slice(0, 3).map((z) => ( - - ))} - - {rider.zones.length > 3 && ( - - )} - - - - + + + + {vehicle} + {vehicleNo} + + ); } @@ -904,9 +210,10 @@ function RiderCard({ rider, onView, onMenu }) { // MAIN COMPONENT // ════════════════════════════════════════════════════════════════════════════════ export default function Riders() { - const theme = useTheme(); - const isMdDown = useMediaQuery(theme.breakpoints.down('md')); - const isLgDown = useMediaQuery(theme.breakpoints.down('lg')); + const isMdDown = useMediaQuery('(max-width: 900px)'); + const isLgDown = useMediaQuery('(max-width: 1200px)'); + const toast = useToast(); + const notify = (msg, status = 'success') => toast({ body: msg, type: status }); const hub = getHubContext(); const hubName = hub.hubname || 'This Hub'; @@ -941,31 +248,27 @@ export default function Riders() { const [profile, setProfile] = useState(null); const [profileEdit, setProfileEdit] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); - const [menuAnchor, setMenuAnchor] = useState(null); - const [menuRider, setMenuRider] = useState(null); - const [snack, setSnack] = useState({ open: false, msg: '', severity: 'success' }); - - const toast = (msg, severity = 'success') => setSnack({ open: true, msg, severity }); + const [deleting, setDeleting] = useState(false); const effectiveView = isLgDown ? 'grid' : view; const kpi = useMemo(() => { - const onDuty = riders.filter(r => !['Offline', 'Suspended'].includes(r.status)).length; - const onField = riders.filter(r => ['On Pickup', 'Returning to Hub'].includes(r.status)).length; - const idle = riders.filter(r => r.status === 'Idle').length; - const slaRisk = riders.filter(r => loadPct(r) > 85 || r.deliveriesFailed >= 5).length; + const onDuty = riders.filter((r) => !['Offline', 'Suspended'].includes(r.status)).length; + const onField = riders.filter((r) => ['On Pickup', 'Returning to Hub'].includes(r.status)).length; + const idle = riders.filter((r) => r.status === 'Idle').length; + const slaRisk = riders.filter((r) => loadPct(r) > 85 || r.deliveriesFailed >= 5).length; const codToCollect = riders.reduce((s, r) => s + r.codPending, 0); return { onDuty, onField, idle, slaRisk, codToCollect, total: riders.length }; }, [riders]); const filtered = useMemo(() => { const q = search.trim().toLowerCase(); - return riders.filter(r => { - const matchSearch = !q || - r.name.toLowerCase().includes(q) || - r.id.toLowerCase().includes(q) || - r.phone.includes(q) || - r.zones.some(z => z.toLowerCase().includes(q)); + return riders.filter((r) => { + const matchSearch = !q + || r.name.toLowerCase().includes(q) + || r.id.toString().toLowerCase().includes(q) + || r.phone.includes(q) + || r.zones.some((z) => z.toLowerCase().includes(q)); const matchStatus = statusFilter === 'All' || r.status === statusFilter; const matchVehicle = vehicleFilter === 'All' || r.vehicle === vehicleFilter; return matchSearch && matchStatus && matchVehicle; @@ -985,41 +288,41 @@ export default function Riders() { try { if (formDialog.mode === 'add') { await createMiler(toApiPayload(rider)); - toast(`${rider.name} onboarded successfully`); + notify(`${rider.name} onboarded successfully`); } else { await updateMiler(rider.userid ?? rider.id, toApiPayload(rider)); - toast(`${rider.name} updated`); + notify(`${rider.name} updated`); } setFormDialog({ open: false, mode: 'add', initial: null }); loadMilers(); } catch (err) { - toast(err?.message || 'Could not save this miler.', 'error'); + notify(err?.message || 'Could not save this miler.', 'error'); } }; const handleDelete = async () => { const target = deleteTarget; + if (!target || deleting) return; + setDeleting(true); try { await deleteMiler(target.userid ?? target.id); setRiders((p) => p.filter((r) => r.id !== target.id)); - toast(`${target.name} removed`, 'info'); + notify(`${target.name} removed`, 'info'); } catch (err) { - toast(err?.message || 'Could not remove this miler.', 'error'); + notify(err?.message || 'Could not remove this miler.', 'error'); } finally { + setDeleting(false); setDeleteTarget(null); setProfile(null); } }; const openAdd = () => setFormDialog({ open: true, mode: 'add', initial: null }); - // Edit now opens the same right-side sheet in edit mode (no separate modal). const openEdit = (r) => { - setMenuAnchor(null); setProfile(r); setProfileEdit(true); }; const openView = (r) => { - setMenuAnchor(null); setProfile(r); setProfileEdit(false); }; @@ -1029,377 +332,604 @@ export default function Riders() { setRiders((p) => p.map((r) => (r.id === updated.id ? updated : r))); setProfile(updated); setProfileEdit(false); - toast(`${updated.name} updated`); + notify(`${updated.name} updated`); } catch (err) { - toast(err?.message || 'Could not update this miler.', 'error'); + notify(err?.message || 'Could not update this miler.', 'error'); } }; - const openMenu = (e, r) => { - setMenuAnchor(e.currentTarget); - setMenuRider(r); - }; + + const menuItemsFor = (r) => [ + { label: 'View Profile', icon: , onClick: () => openView(r) }, + { label: 'Edit', icon: , onClick: () => openEdit(r) }, + { label: 'Call', icon: , onClick: () => window.open(`tel:${r.phone}`, '_self') }, + { type: 'divider' }, + { label: 'Remove Miler', icon: , onClick: () => setDeleteTarget(r) } + ]; const STATUS_CHIPS = ['All', 'On Pickup', 'Idle', 'On Break', 'Offline', 'Suspended']; + const columns = useMemo(() => [ + { + key: 'miler', + header: 'Miler', + width: proportional(1.6), + renderCell: (r) => ( + openView(r)}> + + + {r.name} + {r.id} + + + ) + }, + { + key: 'status', + header: 'Status', + width: pixel(140), + renderCell: (r) => + }, + { + key: 'zones', + header: 'Zones', + width: proportional(1.4), + renderCell: (r) => ( + + {r.zones.length === 0 ? : r.zones.map((z) => )} + + ) + }, + { + key: 'vehicle', + header: 'Vehicle', + width: proportional(1.2), + renderCell: (r) => + }, + { + key: 'load', + header: 'Load', + width: pixel(140), + renderCell: (r) => + }, + { + key: 'today', + header: 'Today', + width: pixel(130), + renderCell: (r) => ( + + + Done + {r.deliveriesDone} + + + COP + {inr(r.codCollected)} + + + ) + }, + { + key: 'actions', + header: '', + width: pixel(56), + align: 'end', + renderCell: (r) => + } + // eslint-disable-next-line react-hooks/exhaustive-deps + ], []); + return ( - - {/* Header */} - {/* ── Improved Header with better gaps ── */} - - {/* Title & Avatar Branding Group */} - - - - - - - + + + + } - }} - > - Milers - - - - {kpi.total} milers • who's working and where - - - - - {/* Actions Group - pushed to the far right with clear spacing between buttons */} - - - - - - + /> {/* KPIs */} - - {[ - { icon: GroupsOutlinedIcon, label: 'On Duty', value: kpi.onDuty, sub: `of ${kpi.total}`, color: '#1A73E8', bg: '#E8F0FE', trend: true }, - { icon: RouteOutlinedIcon, label: 'In Field', value: kpi.onField, sub: 'Active pickups', color: '#1E8E3E', bg: '#E6F4EA' }, - { icon: AccessTimeOutlinedIcon, label: 'Available', value: kpi.idle, sub: 'Ready for assignment', color: '#B06000', bg: '#FEF7E0' }, - { icon: WarningAmberOutlinedIcon, label: 'SLA Risk', value: kpi.slaRisk, sub: 'Overloaded milers', color: '#D93025', bg: '#FCE8E6' }, - { icon: PaymentsOutlinedIcon, label: 'COP Pending', value: inr(kpi.codToCollect), sub: 'To be collected', color: '#8E24AA', bg: '#F3E5F5' }, - ].map((k, i) => ( - - - - ))} + + + + + + - {/* Main Card */} - + {/* Toolbar */} - - - setSearch(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - sx: { borderRadius: 2 }, - }} - sx={{ - maxWidth: { lg: 420 }, - }} - /> +
+ +
+ setSearch(e.target.value)} + /> +
+
+ setVehicleFilter(v || 'All')} + hasClear + /> +
+
+ {!isLgDown && ( + + } /> + } /> + + )} + - - - - - - - {!isLgDown && ( - v && setView(v)} - sx={{ borderRadius: 2 }} - > - - - - - - - - )} - - - {/* Status Chips */} - - {STATUS_CHIPS.map(s => { + + {STATUS_CHIPS.map((s) => { const active = statusFilter === s; - const meta = STATUS_META[s] || {}; - // Each status carries its own colour; "All" falls back to a neutral dark. - const accent = meta.color || '#1A1A2E'; - const tint = meta.bg || '#F8F9FB'; + const meta = STATUS_META[s]; return ( - setStatusFilter(s)} - icon={meta.color ? : undefined} - sx={{ - height: 38, - px: 1, - borderRadius: 2, - fontSize: '0.82rem', - fontWeight: active ? 700 : 600, - bgcolor: active ? accent : tint, - color: active ? '#fff' : accent, - border: active ? 'none' : `1px solid ${alpha(accent, 0.25)}`, - '& .MuiChip-icon': { color: active ? '#fff' : meta.color }, - '& .MuiChip-label': { px: 1.25 }, - '&:hover': { bgcolor: active ? accent : alpha(accent, 0.14) } + style={{ + height: 32, + padding: '0 12px', + borderRadius: 'var(--radius-full)', + fontSize: '0.8rem', + fontWeight: 600, + cursor: 'pointer', + border: active ? 'none' : '1px solid var(--color-border)', + background: active ? 'var(--color-accent)' : 'var(--color-background-surface)', + color: active ? 'var(--color-on-accent)' : 'var(--color-text-secondary)' }} - /> + > + {meta ? meta.label.split(' / ')[0] : s} + ); })} - - + +
- {/* Content Area */} + {/* Content */} {loadError && ( - setLoadError('')} sx={{ m: 2, borderRadius: 2 }}> - {loadError} - +
+ +
)} - {loading ? ( - - - - ) : filtered.length === 0 ? ( - - - No milers match your filters - - - ) : effectiveView === 'table' ? ( - -
- - - Miler - Status - Zones - Vehicle - Load - Today - - - - - {filtered.map(rider => ( - openView(rider)}> - - - - - - - {rider.name} - {rider.id} - - - - - - - {rider.zones.map(z => )} - - - - - - - - Done - {rider.deliveriesDone} - - - COP - {inr(rider.codCollected)} - - - - - - { e.stopPropagation(); openView(rider); }}> - { e.stopPropagation(); openEdit(rider); }}> - { e.stopPropagation(); openMenu(e, rider); }}> - - - - ))} - -
- + {loading ? ( +
+ +
+ ) : filtered.length === 0 ? ( + } + title="No milers match your filters" + description="Try adjusting or clearing your search and filters." + style={{ padding: '64px 0' }} + actions={ + + } + /> + ) : effectiveView === 'table' ? ( +
+ + ) : ( - - - {filtered.map(r => ( - - - +
+ + {filtered.map((r) => ( + ))} - +
)} {filtered.length > 0 && ( - - - Showing {filtered.length} of {riders.length} milers - - +
+ Showing {filtered.length} of {riders.length} milers +
)} - + - {/* Context Menu */} - setMenuAnchor(null)} PaperProps={{ sx: { borderRadius: 2 } }}> - openView(menuRider)}>View Profile - openEdit(menuRider)}>Edit - Call - - { setDeleteTarget(menuRider); setMenuAnchor(null); }} sx={{ color: '#D93025' }}> - Remove Miler - - + {/* Add / Edit Dialog */} + setFormDialog({ open: false, mode: 'add', initial: null })} + onSave={handleSave} + /> - {/* Dialogs */} - setFormDialog({ open: false, mode: 'add', initial: null })} onSave={handleSave} /> + {/* Profile Sheet */} { setProfile(null); setProfileEdit(false); }} onSave={saveProfileEdit} + onDelete={(r) => setDeleteTarget(r)} /> - setDeleteTarget(null)} onConfirm={handleDelete} /> - {/* Toast */} - setSnack(s => ({ ...s, open: false }))}> - {snack.msg} - -
+ {/* Delete confirmation */} + { if (!open) setDeleteTarget(null); }} + title="Remove miler?" + description={`${deleteTarget?.name || 'This miler'} will be permanently removed from the roster.`} + actionLabel="Yes, remove" + actionVariant="destructive" + isActionLoading={deleting} + onAction={handleDelete} + /> + ); -} \ No newline at end of file +} + +// ════════════════════════════════════════════════════════════════════════════════ +// Rider Card (grid view) +// ════════════════════════════════════════════════════════════════════════════════ +function RiderCard({ rider, onView, menuItems }) { + const pct = loadPct(rider); + return ( + onView(rider)}> + + + + + {rider.name} + {rider.id} + + +
e.stopPropagation()}> + +
+
+ +
+ +
+ + + + Today's Load + {rider.assigned}/{rider.capacity} · {pct}% + + 85 ? 'error' : pct > 60 ? 'warning' : 'accent'} /> + + + + Service Zones + + {rider.zones.length === 0 ? : ( + <> + {rider.zones.slice(0, 3).map((z) => )} + {rider.zones.length > 3 && } + + )} + + +
+ ); +} + +// ════════════════════════════════════════════════════════════════════════════════ +// Add / Edit Rider Dialog (3-step) +// ════════════════════════════════════════════════════════════════════════════════ +function RiderFormDialog({ open, onClose, onSave, initial, mode, isMdDown }) { + const [activeStep, setActiveStep] = useState(0); + const [form, setForm] = useState(initial || EMPTY_FORM); + const [errors, setErrors] = useState({}); + + useEffect(() => { + if (open) { + setForm(initial || EMPTY_FORM); + setErrors({}); + setActiveStep(0); + } + }, [open, initial]); + + const set = (k, v) => { + setForm((p) => ({ ...p, [k]: v })); + setErrors((e) => ({ ...e, [k]: undefined })); + }; + + const validateStep = (step) => { + const e = {}; + if (step === 0) { + if (!form.name.trim()) e.name = 'Full name is required'; + if (!form.phone.trim()) e.phone = 'Phone number is required'; + else if (!/[0-9]{10}/.test(form.phone.replace(/\D/g, ''))) e.phone = 'Enter a valid 10-digit phone number'; + } + if (step === 1) { + if (form.zones.length === 0) e.zones = 'Assign at least one service zone'; + } + if (step === 2) { + if (!form.vehicle) e.vehicle = 'Select a vehicle type'; + if (!form.vehicleNo.trim() && form.vehicle !== 'Cycle') e.vehicleNo = 'Vehicle number is required'; + } + setErrors(e); + return Object.keys(e).length === 0; + }; + + const next = () => { if (validateStep(activeStep)) setActiveStep((s) => s + 1); }; + const back = () => setActiveStep((s) => s - 1); + + const submit = (e) => { + e.preventDefault(); + if (!validateStep(2)) return; + const cap = VEHICLES[form.vehicle]?.capacity || 30; + onSave({ + ...form, + vehicleNo: form.vehicleNo.trim() || '—', + id: mode === 'add' ? genId() : form.id, + capacity: cap, + checkInTime: mode === 'add' ? '—' : form.checkInTime, + hoursToday: mode === 'add' ? 0 : form.hoursToday, + assigned: mode === 'add' ? 0 : form.assigned, + pickupsPending: mode === 'add' ? 0 : form.pickupsPending, + deliveriesPending: mode === 'add' ? 0 : form.deliveriesPending, + deliveriesDone: mode === 'add' ? 0 : form.deliveriesDone, + deliveriesFailed: mode === 'add' ? 0 : form.deliveriesFailed, + codCollected: mode === 'add' ? 0 : form.codCollected, + codPending: mode === 'add' ? 0 : form.codPending, + rating: mode === 'add' ? 5.0 : form.rating + }); + }; + + const isLast = activeStep === STEPS.length - 1; + + return ( + { if (!o) onClose(); }} width={480} variant={isMdDown ? 'fullscreen' : 'standard'} maxHeight="85vh" purpose="form"> +
+ } + content={ + + + Step {activeStep + 1} of {STEPS.length} · {STEPS[activeStep]} + + + + + {activeStep === 0 && ( + <> + set('name', e.target.value)} status={errors.name ? { type: 'error', message: errors.name } : undefined} /> + set('phone', e.target.value)} status={errors.phone ? { type: 'error', message: errors.phone } : undefined} /> + set('hub', v)} /> + + )} + + {activeStep === 1 && ( + <> + set('zones', v)} + triggerDisplay="badges" + hasSearch + status={errors.zones ? { type: 'error', message: errors.zones } : undefined} + /> + {mode === 'edit' && ( + set('status', v)} /> + )} + + )} + + {activeStep === 2 && ( + <> + ({ value: v, label: `${v} · ${VEHICLES[v].capacity} parcels` }))} + value={form.vehicle} + onChange={(v) => set('vehicle', v)} + status={errors.vehicle ? { type: 'error', message: errors.vehicle } : undefined} + /> + set('vehicleNo', e.target.value.toUpperCase())} + description={form.vehicle === 'Cycle' ? 'Optional for cycles' : undefined} + status={errors.vehicleNo ? { type: 'error', message: errors.vehicleNo } : undefined} + /> + + + + + + + set('verified', v)} description="Driving licence & ID proof validated" labelSpacing="spread" /> + + + + + )} + + + } + footer={ + + + + + {activeStep > 0 && } + {!isLast ? ( + + ) : ( + + )} + + + + } + /> + +
+ ); +} + +// ════════════════════════════════════════════════════════════════════════════════ +// Profile Sheet — view + inline edit, right-anchored full-height dialog +// ════════════════════════════════════════════════════════════════════════════════ +function ProfileDrawer({ rider: riderProp, onClose, onSave, onDelete, startInEdit = false, isMdDown }) { + const [editing, setEditing] = useState(startInEdit); + // Retain the last opened miler so the sheet keeps rendering its content + // while it closes — riderProp goes null immediately on close, and + // unmounting the Dialog right away would cut off its closing animation. + const [retained, setRetained] = useState(riderProp); + const [form, setForm] = useState(riderProp || {}); + + useEffect(() => { + if (riderProp) { + setRetained(riderProp); + setForm(riderProp); + setEditing(startInEdit); + } + }, [riderProp, startInEdit]); + + const open = Boolean(riderProp); + const rider = riderProp || retained; + + const sr = rider ? successRate(rider) : 0; + const setField = (key) => (e) => setForm((f) => ({ ...f, [key]: e.target.value })); + + const cancelEdit = () => { setForm(rider); setEditing(false); }; + const saveEdit = () => { + const updated = { ...rider, ...form, capacity: VEHICLES[form.vehicle]?.capacity ?? rider.capacity }; + onSave(updated); + }; + + const meta = rider ? (STATUS_META[rider.status] || STATUS_META.Offline) : STATUS_META.Offline; + + return ( + { if (!o) onClose(); }} + width={isMdDown ? '100%' : 480} + maxHeight="100vh" + style={{ height: '100vh' }} + position={{ top: 0, right: 0, bottom: 0 }} + purpose="info" + > + {rider && ( + + + {form.name || rider.name} + {rider.id} + + + {rider.verified && } + + + + } + content={ + + + {editing ? ( + + + Edit Miler Details + + + + + setForm((f) => ({ ...f, status: v }))} /> + setForm((f) => ({ ...f, vehicle: v }))} /> + + setForm((f) => ({ ...f, hub: v }))} /> + setForm((f) => ({ ...f, zones: v }))} triggerDisplay="badges" hasSearch /> + + + ) : ( + <> + + Today's Performance + + + + + + + + + Live Load + + + Pending pickups + {rider.pickupsPending} + + + + + Miler Details + + {[ + { icon: Phone, label: 'Phone', value: rider.phone }, + { icon: MapPin, label: 'Hub', value: rider.hub }, + { icon: MapPin, label: 'Zones', value: rider.zones?.join(', ') || 'N/A' }, + { icon: Bike, label: 'Vehicle', value: `${rider.vehicle} • ${rider.vehicleNo}` }, + { icon: Clock, label: 'Check-in', value: `${rider.checkInTime} (${rider.hoursToday}h)` } + ].map((d, i) => ( + + + + {d.label} + {d.value} + + + ))} + + + + )} + + + } + footer={ + + + {editing ? ( + <> + + + + ) : ( + <> + + + + + )} + + + } + /> + )} + + ); +} diff --git a/src/pages/operations/Routing.jsx b/src/pages/operations/Routing.jsx index e8ff26c..fb614c2 100644 --- a/src/pages/operations/Routing.jsx +++ b/src/pages/operations/Routing.jsx @@ -1,18 +1,17 @@ +/* eslint-disable react/prop-types */ import { useState, useEffect, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { - Box, Typography, Card, CardContent, CardHeader, Grid, TextField, Button, - Stack, Divider, Alert, AlertTitle, Avatar, List, ListItemButton, ListItemText, Chip, CircularProgress -} from '@mui/material'; -import QrCodeScannerIcon from '@mui/icons-material/QrCodeScanner'; -import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; -import HelpIcon from '@mui/icons-material/Help'; -import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import LocalShippingIcon from '@mui/icons-material/LocalShipping'; -import HubIcon from '@mui/icons-material/Hub'; -import AcUnitIcon from '@mui/icons-material/AcUnit'; -import LocalOfferIcon from '@mui/icons-material/LocalOffer'; +import { Box, QrCode, ArrowRight, HelpCircle, AlertTriangle, Truck, MapPin, Snowflake, Tag } from 'lucide-react'; +import { Card } from '@astryxdesign/core/Card'; +import { TextInput } from '@astryxdesign/core/TextInput'; +import { Banner } from '@astryxdesign/core/Banner'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { HStack, VStack } from '@astryxdesign/core/Layout'; +import Panel from '@/components/Panel'; +import Button from '@/components/Button'; +import PageHeader from '@/components/PageHeader'; import { getRouting, getInboundToday } from '@/api/hub'; import { getHubContext } from '@/auth/session'; @@ -20,6 +19,56 @@ import { getHubContext } from '@/auth/session'; const isException = (condition) => /damag|wet|crush|missing|broken/i.test(condition || ''); +// Classify the next action from the API's nexthop + condition. Colors map to +// Astryx's categorical tokens so each queue type reads as part of the same +// status language as Badge/StatCard, not one-off hex values. +const determineNextAction = (pkg) => { + if (isException(pkg.condition)) { + return { queue: 'Needs Checking', action: 'Set aside for a supervisor', tone: 'red', icon: }; + } + if (pkg.iscoldchain) { + return { queue: 'Cold Chain', action: 'Put in the cold room (Zone C)', tone: 'cyan', icon: }; + } + if ((pkg.nexthop || '').toLowerCase().startsWith('transfer')) { + return { queue: 'Transfer to Another City', action: pkg.nexthop, tone: 'purple', icon: }; + } + if ((pkg.nexthop || '').toLowerCase().includes('local')) { + return { queue: 'Local Delivery', action: `Send to ${pkg.destination || 'the delivery lane'}`, tone: 'green', icon: }; + } + return { queue: pkg.nexthop || 'Check the address', action: pkg.nexthop || 'Check the address', tone: 'gray', icon: }; +}; + +const TONE_VARS = { + red: { icon: 'var(--color-icon-red)', bg: 'var(--color-background-red)' }, + cyan: { icon: 'var(--color-icon-cyan)', bg: 'var(--color-background-cyan)' }, + purple: { icon: 'var(--color-icon-purple)', bg: 'var(--color-background-purple)' }, + green: { icon: 'var(--color-icon-green)', bg: 'var(--color-background-green)' }, + gray: { icon: 'var(--color-icon-secondary)', bg: 'var(--color-background-muted)' } +}; + +function IconTile({ tone = 'blue', size = 32, children }) { + const { icon, bg } = tone === 'blue' + ? { icon: 'var(--color-icon-blue)', bg: 'var(--color-background-blue)' } + : TONE_VARS[tone] || TONE_VARS.gray; + return ( + + {children} + + ); +} + export default function Routing() { const [searchParams] = useSearchParams(); const hub = getHubContext(); @@ -68,230 +117,189 @@ export default function Routing() { } }, [searchParams, handleSearch]); - // Classify the next action from the API's nexthop + condition. - const determineNextAction = (pkg) => { - if (isException(pkg.condition)) { - return { queue: 'Needs Checking', action: 'Set aside for a supervisor', color: '#D93025', bg: '#FCE8E6', icon: }; - } - if (pkg.iscoldchain) { - return { queue: 'Cold Chain', action: 'Put in the cold room (Zone C)', color: '#00838F', bg: '#E0F7FA', icon: }; - } - if ((pkg.nexthop || '').toLowerCase().startsWith('transfer')) { - return { queue: 'Transfer to Another City', action: pkg.nexthop, color: '#8E24AA', bg: '#F3E5F5', icon: }; - } - if ((pkg.nexthop || '').toLowerCase().includes('local')) { - return { queue: 'Local Delivery', action: `Send to ${pkg.destination || 'the delivery lane'}`, color: '#1E8E3E', bg: '#E6F4EA', icon: }; - } - return { queue: pkg.nexthop || 'Check the address', action: pkg.nexthop || 'Check the address', color: '#5F6368', bg: '#F1F3F4', icon: }; - }; - return ( - - - Where Does It Go? - - Scan a parcel and we'll tell you exactly what to do with it next deliver locally, transfer to another city, or set it aside. - - +
+ - +
{/* Search Panel */} - - - - Scan a Parcel} - avatar={} + + +
+ + Scan a Parcel +
+ + e.key === 'Enter' && handleSearch()} /> - - - - setSearchId(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - InputProps={{ - sx: { borderRadius: 2, bgcolor: '#fff' } - }} - /> - - - -
+ + + - - Parcels Waiting} - subheader={Tap a parcel to check it} - avatar={} - /> - - - - {waiting.length === 0 && ( - - No parcels inbounded today. - - )} - {waiting.map((pkg) => ( - { - setSearchId(pkg.trackingno); - handleSearch(pkg.trackingno); - }} - selected={searchId === pkg.trackingno} - sx={{ - borderRadius: 2, mb: 1, p: 2, - border: '1px solid', - borderColor: searchId === pkg.trackingno ? '#C01227' : '#eaeaea', - bgcolor: searchId === pkg.trackingno ? '#C0122708' : '#fff', - '&:hover': { bgcolor: '#f8f9fa' } - }} - > - - - ))} - - - -
-
+ +
+ + + Parcels Waiting + Tap a parcel to check it + +
+
+ {waiting.length === 0 && ( +
+ No parcels inbounded today. +
+ )} + {waiting.map((pkg) => { + const selected = searchId === pkg.trackingno; + return ( +
{ + setSearchId(pkg.trackingno); + handleSearch(pkg.trackingno); + }} + style={{ + padding: '10px 12px', + borderRadius: 'var(--radius-inner)', + cursor: 'pointer', + border: `1px solid ${selected ? 'var(--color-accent)' : 'var(--color-border)'}`, + backgroundColor: selected ? 'var(--color-accent-muted)' : 'var(--color-background-surface)', + transition: 'all 0.15s ease' + }} + > + {pkg.trackingno} + Going to {pkg.dest} +
+ ); + })} +
+
+ {/* Visual Route Guideline Panel */} - +
{!searched ? ( - - - - - - - Scan a parcel to begin - We'll show you where it needs to go. - - + +
+ +
+ Scan a parcel to begin + We'll show you where it needs to go.
) : matchedPkg ? ( - - {matchedPkg.trackingno}} - subheader={{matchedPkg.customername ? `For ${matchedPkg.customername}` : `Shelf: ${matchedPkg.recommendedshelf || '—'}`}} - action={ - - } - sx={{ px: { xs: 2.5, sm: 4 }, pt: { xs: 3, sm: 4 }, pb: 2 }} - /> - - - + +
+ + {matchedPkg.trackingno} + + {matchedPkg.customername ? `For ${matchedPkg.customername}` : `Shelf: ${matchedPkg.recommendedshelf || '—'}`} + + + +
+ {/* Action Banner */} - - - {determineNextAction(matchedPkg).icon} - - - + + {determineNextAction(matchedPkg).icon} + + {determineNextAction(matchedPkg).queue} - - + + {determineNextAction(matchedPkg).action} - - - + + + {/* Parcel Journey */} - - - The Parcel's Journey - - - - Right Now (Here) - {HUB_NAME} - - - - Put On Shelf - {matchedPkg.recommendedshelf || '—'} - - - - Going To - {matchedPkg.destination || '—'} - - - +
+ + The Parcel's Journey + + + + Right Now (Here) + {HUB_NAME} + + + + Put On Shelf + {matchedPkg.recommendedshelf || '—'} + + + + Going To + {matchedPkg.destination || '—'} + + +
{/* Handling Alerts */} {isException(matchedPkg.condition) && ( - - Something's Wrong - Condition reported as {matchedPkg.condition}. Set it aside in the Exception Area for a supervisor. - + )} {matchedPkg.iscoldchain && ( - - Cold chain parcel - Move this parcel to the Cold Room (Zone C) right away. - + )} {determineNextAction(matchedPkg).queue === 'Transfer to Another City' && ( - - Put in the transfer bin - {matchedPkg.nexthop}. It will go out with the next city transfer. - + )} {determineNextAction(matchedPkg).queue === 'Local Delivery' && ( - - Ready for local delivery - Place this parcel in the {matchedPkg.destination} lane so a miler can take it out. - + )} - -
-
+ + ) : ( - - - - Package Not Found - The package code {searchId} is not registered in the system. - - + + )} - - - +
+
+
); } diff --git a/src/pages/operations/TrackingMap.jsx b/src/pages/operations/TrackingMap.jsx index ccca772..0ce97c6 100644 --- a/src/pages/operations/TrackingMap.jsx +++ b/src/pages/operations/TrackingMap.jsx @@ -1,21 +1,35 @@ import React, { useState, useEffect, useRef } from 'react'; -import { - Box, Typography, Card, CardHeader, Avatar, Stack, Chip, List, ListItem, - ListItemAvatar, ListItemText, Badge, Divider, Alert -} from '@mui/material'; -import DeliveryDiningIcon from '@mui/icons-material/DeliveryDining'; -import HubIcon from '@mui/icons-material/Hub'; -import LocalShippingIcon from '@mui/icons-material/LocalShipping'; +import { Bike, Truck, MapPin } from 'lucide-react'; import { MapContainer, TileLayer, Marker, Popup, Polyline, useMap } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import { Card } from '@astryxdesign/core/Card'; +import { Heading, Text } from '@astryxdesign/core/Text'; +import { Banner } from '@astryxdesign/core/Banner'; +import { Badge } from '@astryxdesign/core/Badge'; +import { Avatar } from '@astryxdesign/core/Avatar'; +import { EmptyState } from '@astryxdesign/core/EmptyState'; +import { StatusDot } from '@astryxdesign/core/StatusDot'; +import { Collapsible } from '@astryxdesign/core/Collapsible'; + import { getMilers, getMilerLocations, getHubs, getTripsheetsInTransit } from '@/api/hub'; import { getHubContext } from '@/auth/session'; -// Keeps Leaflet's canvas sized correctly when the container resizes (sidebar -// toggle, window resize, first paint inside a flex box). Without this the map -// renders grey/blank tiles — the #1 reason a real map "doesn't show". +function useMediaQuery(query) { + const [matches, setMatches] = useState(window.matchMedia(query).matches); + useEffect(() => { + const media = window.matchMedia(query); + if (media.matches !== matches) { + setMatches(media.matches); + } + const listener = () => setMatches(media.matches); + media.addEventListener('change', listener); + return () => media.removeEventListener('change', listener); + }, [matches, query]); + return matches; +} + function MapResizeHandler() { const map = useMap(); useEffect(() => { @@ -29,7 +43,6 @@ function MapResizeHandler() { return null; } -// Fix leaflet default marker icons issue delete L.Icon.Default.prototype._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png', @@ -37,24 +50,23 @@ L.Icon.Default.mergeOptions({ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png', }); -// Custom HTML Icons for Leaflet const createHubIcon = () => new L.DivIcon({ className: 'custom-leaflet-icon', - html: `
`, + html: `
`, iconSize: [36, 36], iconAnchor: [18, 18], }); const createRiderIcon = () => new L.DivIcon({ className: 'custom-leaflet-icon', - html: `
`, + html: `
`, iconSize: [30, 30], iconAnchor: [15, 15], }); const createLinehaulIcon = () => new L.DivIcon({ className: 'custom-leaflet-icon', - html: `
`, + html: `
`, iconSize: [34, 34], iconAnchor: [17, 17], }); @@ -62,26 +74,26 @@ const createLinehaulIcon = () => new L.DivIcon({ const prettyHubType = (t) => ({ sorting_center: 'Sorting Center', delivery_hub: 'Delivery Hub', spoke: 'Spoke', warehouse: 'Warehouse' }[t] || t || 'Hub'); -// Colour-code a miler pin/list item by their live availability status. const statusMeta = (status) => { switch (status) { case 'Assigned': case 'On Pickup': - return { color: '#1A73E8', label: status }; + return { color: '#3b82f6', bg: '#eff6ff', label: status, variant: 'info' }; case 'Available': case 'Idle': - return { color: '#1E8E3E', label: status }; + return { color: '#10b981', bg: '#ecfdf5', label: status, variant: 'success' }; case 'On_Break': case 'On Break': - return { color: '#8E24AA', label: 'On Break' }; + return { color: '#a855f7', bg: '#faf5ff', label: 'On Break', variant: 'warning' }; case 'Offline': - return { color: '#80868B', label: 'Offline' }; + return { color: '#64748b', bg: '#f8fafc', label: 'Offline', variant: 'neutral' }; default: - return { color: '#0070f3', label: status || 'Active' }; + return { color: '#3b82f6', bg: '#eff6ff', label: status || 'Active', variant: 'info' }; } }; export default function TrackingMap() { + const isMdDown = useMediaQuery('(max-width: 900px)'); const hub = getHubContext(); const [milers, setMilers] = useState([]); const [error, setError] = useState(''); @@ -89,7 +101,6 @@ export default function TrackingMap() { const [linehauls, setLinehauls] = useState([]); const didLoad = useRef(false); - // Hub pins are static per session — load once from /hub/hubs (has lat/lon). useEffect(() => { getHubs() .then((res) => @@ -102,10 +113,6 @@ export default function TrackingMap() { .catch(() => setHubs([])); }, []); - // Poll live miler GPS every 5 seconds. The guide's /admin/milers/locations - // (Redis GEO) returns nothing on this backend, so we read coordinates from - // /hub/milers (currentlatitude/currentlongitude) and fall back to the GEO - // endpoint if it ever starts returning data. useEffect(() => { let active = true; const normalize = (list) => @@ -144,7 +151,6 @@ export default function TrackingMap() { }; }, []); - // Poll real transfer trucks in transit every 5 seconds. useEffect(() => { let active = true; const poll = async () => { @@ -164,8 +170,8 @@ export default function TrackingMap() { current: [t.currentlat, t.currentlon] })) ); - } catch { - /* leave last-known trucks on transient failure */ + } catch (err) { + console.error(err); } }; poll(); @@ -176,7 +182,6 @@ export default function TrackingMap() { }; }, []); - // Centre on the milers when we have them, otherwise a sensible national view. const mapCenter = milers.length > 0 ? [ @@ -186,164 +191,169 @@ export default function TrackingMap() { : [22.0, 76.0]; return ( - - - Live Map - See where your milers and transfer trucks are right now, on the map. - +
+
+ Live Map + See where your milers and transfer trucks are right now, on the map. +
{error && ( - setError('')} sx={{ mb: 3, borderRadius: 2 }}> - {error} - +
+ +
)} - +
+ {/* Map Canvas Frame */} - - +
+ - {/* Plot Hubs */} {hubs.map((h) => ( - {h.name} - {h.type} +
{h.name}
+
{h.type}
))} - {/* Plot live Milers */} {milers.map((m) => ( - {m.displayname || `Miler ${m.userid}`} - {statusMeta(m.status).label} - {m.bookingid && On booking #{m.bookingid}} +
{m.displayname || `Miler ${m.userid}`}
+
{statusMeta(m.status).label}
+ {m.bookingid &&
On booking #{m.bookingid}
}
))} - {/* Plot transfer trucks in transit (real positions) */} {linehauls.map((lh) => ( - + - {lh.name} - Progress: {Math.round(lh.progress * 100)}% +
{lh.name}
+
Progress: {Math.round(lh.progress * 100)}%
))}
- +
{/* Sidebar Status Trackers */} - - - - {/* Active Network Hub Nodes Summary */} - - Active Hub Nodes} - avatar={} - /> - - +
+ + + +
+ +
+ Active Hub Nodes +
+ } + > +
{hubs.length === 0 && ( - - - + )} {hubs.map((h) => ( - - - - +
+
+
{h.name}
+
{h.type}
+
+ +
))} - - +
+ +
- {/* Real-time Last Mile Miler Logs */} - - Milers Out Now{hub.city ? ` (${hub.city})` : ''}} - avatar={} - /> - - {milers.length === 0 ? ( - - No milers reporting a location right now. - - ) : ( - - {milers.map((m) => { + + +
+ +
+ Milers Out Now {hub.city ? `(${hub.city})` : ''} +
+ } + > +
+ {milers.length === 0 ? ( + + ) : ( + milers.map((m) => { const meta = statusMeta(m.status); const name = m.displayname || `Miler ${m.userid}`; return ( - - - - {name.charAt(0)} - - - + } + size="md" /> - - {meta.label} - - +
+
{name}
+
{m.bookingid ? `On booking #${m.bookingid}` : 'No active booking'}
+
+ {m.status !== 'Offline' && } +
); - })} - - )} - + }) + )} +
+ + - {/* Linehaul Fleet Shipments Tracker */} - - City Transfers} - avatar={} - /> - - + + +
+ +
+ City Transfers + + } + > +
+ {linehauls.length === 0 && ( + + )} {linehauls.map(lh => ( - - - - {Math.round(lh.progress * 100)}% route - - +
+
+
{lh.name}
+
Status: {lh.status}
+
+ +
))} - - +
+
+
- -
-
- + + + ); } - diff --git a/src/theme/componentsOverride.js b/src/theme/componentsOverride.js deleted file mode 100644 index 2d625bf..0000000 --- a/src/theme/componentsOverride.js +++ /dev/null @@ -1,191 +0,0 @@ -import customShadows from './shadows'; - -// ==============================|| DOORMILE THEME - COMPONENT OVERRIDES ||============================== // -// Clean, corporate Material Design tuning for the whole console. - -export default function componentsOverride(theme) { - const { palette } = theme; - - return { - MuiCssBaseline: { - styleOverrides: { - html: { WebkitFontSmoothing: 'antialiased', MozOsxFontSmoothing: 'grayscale', textRendering: 'optimizeLegibility' }, - body: { backgroundColor: palette.background.default }, - '*::-webkit-scrollbar': { width: 8, height: 8 }, - '*::-webkit-scrollbar-thumb': { background: palette.grey[300], borderRadius: 8 }, - '*::-webkit-scrollbar-thumb:hover': { background: palette.grey[400] } - } - }, - MuiButton: { - defaultProps: { disableElevation: true, disableRipple: false }, - styleOverrides: { - root: { - borderRadius: 8, - fontWeight: 600, - padding: '8px 18px', - transition: 'background-color .2s ease, box-shadow .2s ease, transform .12s ease, border-color .2s ease', - '&:active': { transform: 'translateY(0.5px)' }, - '&.Mui-focusVisible': { boxShadow: `0 0 0 3px ${palette.primary.lighter}` } - }, - containedPrimary: { - boxShadow: customShadows.primaryGlow, - '&:hover': { boxShadow: customShadows.primaryGlowHover, backgroundColor: palette.primary.dark } - }, - outlined: { - borderColor: palette.grey[300], - '&:hover': { borderColor: palette.grey[400], backgroundColor: palette.grey[50] } - }, - text: { '&:hover': { backgroundColor: palette.grey[100] } }, - sizeLarge: { padding: '11px 24px', fontSize: '0.9375rem' }, - sizeSmall: { padding: '5px 14px' }, - // Give every button clear breathing room between its icon and label - startIcon: { marginRight: 10 }, - endIcon: { marginLeft: 10 } - } - }, - MuiIconButton: { - styleOverrides: { root: { borderRadius: 8 } } - }, - MuiCard: { - styleOverrides: { - root: { - borderRadius: 14, - border: `1px solid ${palette.grey[200]}`, - boxShadow: customShadows.card, - backgroundImage: 'none', - transition: 'box-shadow .22s ease, border-color .22s ease, transform .22s ease' - } - } - }, - MuiCardHeader: { - defaultProps: { titleTypographyProps: { variant: 'h5' }, subheaderTypographyProps: { variant: 'caption' } }, - styleOverrides: { root: { padding: 20 } } - }, - MuiCardContent: { - styleOverrides: { root: { padding: 20, '&:last-child': { paddingBottom: 20 } } } - }, - MuiPaper: { - defaultProps: { elevation: 0 }, - styleOverrides: { rounded: { borderRadius: 14 } } - }, - MuiToggleButtonGroup: { - styleOverrides: { - root: { backgroundColor: palette.grey[100], borderRadius: 10, padding: 4, gap: 4 }, - grouped: { - border: 0, - borderRadius: '8px !important', - margin: 0 - } - } - }, - MuiToggleButton: { - styleOverrides: { - root: { - border: 0, - borderRadius: 8, - padding: '6px 12px', - color: palette.grey[600], - transition: 'all .18s ease', - '&:hover': { backgroundColor: palette.grey[200] }, - '&.Mui-selected': { - backgroundColor: palette.background.paper, - color: palette.primary.main, - boxShadow: customShadows.card, - '&:hover': { backgroundColor: palette.background.paper } - } - } - } - }, - MuiChip: { - styleOverrides: { - root: { borderRadius: 6, fontWeight: 600, fontSize: '0.75rem' }, - sizeSmall: { height: 22 }, - label: { paddingLeft: 8, paddingRight: 8 } - } - }, - MuiTableCell: { - styleOverrides: { - root: { borderColor: palette.grey[200], padding: '12px 16px', fontSize: '0.8125rem' }, - head: { - fontWeight: 600, - color: palette.grey[600], - backgroundColor: palette.grey[50], - textTransform: 'none', - whiteSpace: 'nowrap' - } - } - }, - MuiTableRow: { - styleOverrides: { - root: { transition: 'background-color .15s ease', '&:hover': { backgroundColor: palette.grey[50] } } - } - }, - MuiOutlinedInput: { - styleOverrides: { - root: { - borderRadius: 8, - backgroundColor: palette.background.paper, - transition: 'box-shadow .2s ease, border-color .2s ease', - '& .MuiOutlinedInput-notchedOutline': { borderColor: palette.grey[300], transition: 'border-color .2s ease' }, - '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: palette.grey[400] }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${palette.primary.lighter}` }, - '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: palette.primary.main, borderWidth: 1 } - }, - input: { padding: '11px 14px' } - } - }, - MuiSelect: { - styleOverrides: { - select: { - paddingTop: '11px', - paddingBottom: '11px', - paddingLeft: '14px', - display: 'flex', - alignItems: 'center' - } - } - }, - MuiInputLabel: { - styleOverrides: { root: { color: palette.grey[600], fontSize: '0.875rem' } } - }, - MuiTab: { - styleOverrides: { - root: { textTransform: 'none', fontWeight: 600, minHeight: 46, fontSize: '0.875rem' } - } - }, - MuiTabs: { - styleOverrides: { indicator: { height: 3, borderRadius: 3 } } - }, - MuiTooltip: { - styleOverrides: { - tooltip: { backgroundColor: palette.grey[800], borderRadius: 6, fontSize: '0.75rem', padding: '6px 10px' } - } - }, - MuiDialog: { - styleOverrides: { paper: { borderRadius: 16 } } - }, - MuiAvatar: { - styleOverrides: { root: { fontWeight: 600, fontSize: '0.875rem' } } - }, - MuiListItemButton: { - styleOverrides: { root: { borderRadius: 8 } } - }, - MuiLinearProgress: { - styleOverrides: { root: { borderRadius: 8, height: 6, backgroundColor: palette.grey[200] } } - }, - MuiMenu: { - styleOverrides: { paper: { borderRadius: 12, boxShadow: customShadows.dropdown, marginTop: 6, border: `1px solid ${palette.grey[200]}` } } - }, - MuiMenuItem: { - styleOverrides: { - root: { - borderRadius: 8, - margin: '2px 6px', - padding: '8px 10px', - fontSize: '0.875rem', - '&:hover': { backgroundColor: palette.grey[100] } - } - } - } - }; -} diff --git a/src/theme/index.js b/src/theme/index.js deleted file mode 100644 index 53e9cb8..0000000 --- a/src/theme/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { createTheme } from '@mui/material/styles'; - -import palette from './palette'; -import typography from './typography'; -import customShadows from './shadows'; -import componentsOverride from './componentsOverride'; - -// ==============================|| DOORMILE THEME - ENTRY ||============================== // - -let theme = createTheme({ - palette, - typography, - shape: { borderRadius: 6 }, - customShadows, - mixins: { toolbar: { minHeight: 64 } } -}); - -theme.components = componentsOverride(theme); - -export default theme; diff --git a/src/theme/palette.js b/src/theme/palette.js deleted file mode 100644 index c43e390..0000000 --- a/src/theme/palette.js +++ /dev/null @@ -1,102 +0,0 @@ -// ==============================|| DOORMILE THEME - PALETTE ||============================== // -// Corporate red brand palette. Brand red #C01227. - -export const grey = { - 0: '#FFFFFF', - 50: '#F8F9FA', - 100: '#F1F3F5', - 200: '#E9ECEF', - 300: '#DEE2E6', - 400: '#CED4DA', - 500: '#ADB5BD', - 600: '#868E96', - 700: '#495057', - 800: '#343A40', // Slate - 900: '#212529', // Graphite - A50: '#F8F9FA', - A100: '#E9ECEF' -}; - -const palette = { - mode: 'light', - common: { black: '#000000', white: '#FFFFFF' }, - primary: { - lighter: '#F8E0E3', - 100: '#EFBBC1', - 200: '#E08A92', - light: '#D6515C', - 400: '#CC2E3C', - main: '#C01227', - dark: '#9E0E20', - 700: '#870C1B', - darker: '#7E0B17', - 900: '#520710', - contrastText: '#FFFFFF' - }, - secondary: { - lighter: grey[100], - 100: grey[100], - 200: grey[200], - light: grey[300], - 400: grey[400], - main: grey[500], - 600: grey[600], - dark: grey[700], - 800: grey[800], - darker: grey[900], - A100: grey[0], - A200: grey[400], - A300: grey[700], - contrastText: grey[0] - }, - error: { - lighter: '#FEEAE9', - light: '#F88078', - main: '#F04134', - dark: '#A82216', - darker: '#7A150C', - contrastText: '#FFFFFF' - }, - warning: { - lighter: '#FFF7E0', - light: '#FFD666', - main: '#FFBF00', - dark: '#B38600', - darker: '#805F00', - contrastText: '#262626' - }, - info: { - lighter: '#E0F7F8', - light: '#66CBD2', - main: '#00A2AE', - dark: '#00727B', - darker: '#005159', - contrastText: '#FFFFFF' - }, - success: { - lighter: '#E3F6EC', - light: '#5CC98C', - main: '#00A854', - dark: '#00773B', - darker: '#00552A', - contrastText: '#FFFFFF' - }, - grey, - text: { - primary: grey[800], - secondary: grey[600], - disabled: grey[400] - }, - action: { - disabled: grey[300], - hover: 'rgba(192, 18, 39, 0.04)', - selected: 'rgba(192, 18, 39, 0.08)' - }, - divider: grey[200], - background: { - paper: '#FFFFFF', - default: grey.A50 - } -}; - -export default palette; diff --git a/src/theme/shadows.js b/src/theme/shadows.js deleted file mode 100644 index bb08a1d..0000000 --- a/src/theme/shadows.js +++ /dev/null @@ -1,15 +0,0 @@ -// ==============================|| DOORMILE THEME - CUSTOM SHADOWS ||============================== // -// Layered elevation (a tight contact shadow + a soft ambient shadow) reads far more -// premium than a single blurry drop shadow. Brand glow on CTAs is kept restrained. - -const customShadows = { - card: '0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.06)', - cardHover: '0 10px 28px -6px rgba(16, 24, 40, 0.12), 0 2px 6px rgba(16, 24, 40, 0.05)', - widget: '0 1px 3px rgba(16, 24, 40, 0.06)', - dropdown: '0 12px 32px -8px rgba(16, 24, 40, 0.18), 0 4px 10px rgba(16, 24, 40, 0.06)', - primaryGlow: '0 4px 12px -2px rgba(192, 18, 39, 0.22)', - primaryGlowHover: '0 6px 16px -2px rgba(192, 18, 39, 0.30)', - header: '0 1px 0 rgba(16, 24, 40, 0.06)' -}; - -export default customShadows; diff --git a/src/theme/typography.js b/src/theme/typography.js deleted file mode 100644 index a958335..0000000 --- a/src/theme/typography.js +++ /dev/null @@ -1,28 +0,0 @@ -// ==============================|| DOORMILE THEME - TYPOGRAPHY ||============================== // -// A deliberate type scale with optical tracking: large display text gets tighter -// negative letter-spacing, small UI text stays legible. Optical tracking like this -// is what separates a designed interface from a default Material build. - -const typography = { - fontFamily: '"Public Sans", "Inter", "Helvetica", "Arial", sans-serif', - htmlFontSize: 16, - fontWeightLight: 300, - fontWeightRegular: 400, - fontWeightMedium: 500, - fontWeightBold: 700, - h1: { fontWeight: 800, fontSize: '2.375rem', lineHeight: 1.15, letterSpacing: '-0.022em' }, - h2: { fontWeight: 800, fontSize: '1.875rem', lineHeight: 1.2, letterSpacing: '-0.02em' }, - h3: { fontWeight: 700, fontSize: '1.5rem', lineHeight: 1.28, letterSpacing: '-0.018em' }, - h4: { fontWeight: 700, fontSize: '1.25rem', lineHeight: 1.35, letterSpacing: '-0.014em' }, - h5: { fontWeight: 700, fontSize: '1rem', lineHeight: 1.5, letterSpacing: '-0.01em' }, - h6: { fontWeight: 600, fontSize: '0.875rem', lineHeight: 1.57, letterSpacing: '-0.006em' }, - caption: { fontWeight: 400, fontSize: '0.75rem', lineHeight: 1.66 }, - body1: { fontSize: '0.875rem', lineHeight: 1.6, letterSpacing: '-0.003em' }, - body2: { fontSize: '0.75rem', lineHeight: 1.66 }, - subtitle1: { fontSize: '0.875rem', fontWeight: 600, lineHeight: 1.57, letterSpacing: '-0.006em' }, - subtitle2: { fontSize: '0.75rem', fontWeight: 600, lineHeight: 1.66 }, - overline: { fontSize: '0.6875rem', fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase' }, - button: { textTransform: 'none', fontWeight: 600, letterSpacing: '-0.006em' } -}; - -export default typography;