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 0000000..f643518
Binary files /dev/null and b/public/navbarLogo.png differ
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 (
-
-
+ onDay(d)}
- sx={{
- width: 28, height: 28, m: '1px', border: 'none', cursor: disabled ? 'default' : 'pointer',
- borderRadius: 1.5, fontSize: '0.72rem', fontFamily: 'inherit',
+ style={{
+ width: 28, height: 28, margin: '1px', border: 'none', cursor: disabled ? 'default' : 'pointer',
+ borderRadius: 6, fontSize: '0.72rem', fontFamily: 'inherit',
fontWeight: isEndpoint ? 700 : 500,
color: disabled ? '#D5D9DD' : isEndpoint ? '#fff' : inMonth ? '#3C4043' : '#C4C9CE',
- bgcolor: isEndpoint ? BRAND : 'transparent',
+ backgroundColor: 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) }
}}
+ onMouseOver={(e) => { if(!disabled && !isEndpoint) e.target.style.backgroundColor = 'var(--color-brand-muted)'; }}
+ onMouseOut={(e) => { if(!disabled && !isEndpoint) e.target.style.backgroundColor = 'transparent'; }}
>
{d.date()}
-
-
+
+
);
})}
-
-
+
+
);
}
-// 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 }}>
-
-
-
-
+
+
+ setView((v) => v.subtract(1, 'month'))} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#9AA0A6' }}>
+
+
+ setView((v) => v.add(1, 'month'))} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#9AA0A6' }}>
+
+
+
+
+
+
);
}
@@ -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(e.currentTarget)}
- variant="outlined"
- startIcon={ }
- sx={{
- height: 36, px: 1.5, borderRadius: 2, textTransform: 'none', fontWeight: 600, fontSize: '0.825rem',
- color: '#334155', borderColor: invalid ? '#EF4444' : '#E2E8F0', bgcolor: '#fff',
- justifyContent: 'flex-start', minWidth: { sm: 210 },
- '&:hover': { borderColor: BRAND, bgcolor: alpha(BRAND, 0.02) }
+ onClick={() => setOpen(!open)}
+ variant="outline"
+ style={{
+ height: 36, padding: '0 12px', borderRadius: 8, fontWeight: 600, fontSize: '0.825rem',
+ color: '#334155', borderColor: invalid ? '#EF4444' : '#E2E8F0', backgroundColor: '#fff',
+ justifyContent: 'flex-start'
}}
>
-
- {dayjs(from).format('DD MMM YYYY')}
-
- {dayjs(to).format('DD MMM YYYY')}
-
+
+
+
{dayjs(from).format('DD MMM YYYY')}
+
+
{dayjs(to).format('DD MMM YYYY')}
+
-
+
-
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 })} />
-
-
- applyPreset(1)} sx={{ textTransform: 'none', color: '#5F6368', fontWeight: 700 }}>
- Reset to today
-
- setAnchor(null)}
- sx={{ textTransform: 'none', fontWeight: 700, bgcolor: BRAND, borderRadius: 2, '&:hover': { bgcolor: '#9E0E20' } }}>
- Done
-
-
-
- >
+ {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 })} />
+
+ applyPreset(1)} style={{ background: 'none', border: 'none', color: '#5F6368', fontWeight: 700, cursor: 'pointer', fontSize: '0.875rem' }}>
+ Reset to today
+
+ setOpen(false)}>
+ Done
+
+
+
+ >
+ )}
+
);
}
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 (
-
-
+
-
+
);
}
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 (
-
-
+
)}
-
-
+
+
{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
-
- } onClick={markAllRead} disabled={unread === 0}>
- Mark all read
-
-
-
- {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}
-
-
-
- ))}
-
-
-
-
- setSelectedNotif(null)}>Dismiss
- }
- onClick={() => {
- setSelectedNotif(null);
- navigate(selectedNotif.to);
- }}
- >
- {selectedNotif.actionText}
+ {selectedNotif && (
+
+ {selectedNotif.title}
+ {selectedNotif.desc}
+
+ {selectedNotif.stats.map((s, i) => (
+
+
{s.label}
+
{s.value}
+
+ ))}
+
+
+
setSelectedNotif(null)}>Dismiss
+ {selectedNotif.actionText && (
+
{ setSelectedNotif(null); navigate(selectedNotif.to); }}>
+ {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 ? (
+
+ ) : —
+ }
+ ], []);
+
+ 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}` : ''}
-
-
+
+
+
+
+
+
+
+
+ {exporting ? 'Exporting…' : 'Export'}
+
+
+
+ }
+ />
- {/* 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 */}
- setCalAnchor(e.currentTarget)}
- variant="outlined"
- startIcon={ }
- sx={{
- height: 36,
- px: 1.5,
- borderRadius: 2,
- textTransform: 'none',
- fontWeight: 600,
- fontSize: '0.825rem',
- color: '#334155',
- borderColor: invalidRange ? '#EF4444' : '#E2E8F0',
- bgcolor: '#ffffff',
- justifyContent: 'flex-start',
- // Full width on mobile so it wraps to its own line; fixed on larger screens.
- width: { xs: '100%', sm: 'auto' },
- minWidth: { sm: 210 }, // Compressed from 250px to remove dead layout space
- '&:hover': { borderColor: BRAND, bgcolor: alpha(BRAND, 0.02) }
- }}
- >
-
- {dayjs(range.from).format('DD MMM YYYY')}
-
- {dayjs(range.to).format('DD MMM YYYY')}
-
-
-
- {/* Export report (Excel) for the selected date range */}
-
- : }
- sx={{
- height: 36,
- px: 2,
- borderRadius: 2,
- textTransform: 'none',
- fontWeight: 700,
- fontSize: '0.825rem',
- bgcolor: BRAND,
- boxShadow: 'none',
- whiteSpace: 'nowrap',
- width: { xs: '100%', sm: 'auto' },
- '&:hover': { bgcolor: '#9E0E20', boxShadow: 'none' }
- }}
- >
- {exporting ? 'Exporting…' : 'Export'}
-
-
-
-
- {/* 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 })}
- />
-
-
- applyPreset(1)} sx={{ textTransform: 'none', color: '#5F6368', fontWeight: 700 }}>
- Reset to today
-
- setCalAnchor(null)}
- sx={{ textTransform: 'none', fontWeight: 700, bgcolor: BRAND, borderRadius: 2, '&:hover': { bgcolor: '#9E0E20' } }}>
- Done
-
-
-
-
- {/* 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.
-
- } href="/routing">
- Where Does It Go?
+
+
Not sure where a parcel goes?
+
Scan it and we'll tell you exactly what to do next.
+
+
+ Where Does It Go?
+
-
-
-
-
-
+
+
+
+
- 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}
-
+
))}
-
-
+
+
-
+
© 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()}
+ />
+ setShow(!show)}
+ style={{ position: 'absolute', right: 12, top: 8, background: 'none', border: 'none', cursor: 'pointer', color: '#64748b' }}
+ >
+ {show ? : }
+
+
+
-
- }
- label={Remember me }
- />
-
+
{error && (
-
+
{error}
-
+
)}
: null}
- sx={{
- bgcolor: '#C01227',
- color: '#fff',
- py: 1.5,
- fontWeight: 600,
- borderRadius: 2,
- textTransform: 'none',
- boxShadow: 'none',
- '&:hover': {
- bgcolor: '#9E0E20',
- boxShadow: 'none'
- }
- }}
+ style={{ width: '100%', justifyContent: 'center' }}
>
+ {loading && }
{loading ? 'Signing in…' : 'Sign In'}
-
-
- 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}
-
+
))}
-
-
+
+
-
+
© 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)} />
-
+
+
+
+
+
);
}
\ 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 (
- handleSeal(m)}
- startIcon={isBusy ? : null} sx={{ borderRadius: 2, fontWeight: 700 }}>
- Check & Mark Ready
+ handleSeal(m)}>
+ {isBusy ? 'Working...' : 'Check & Mark Ready'}
);
}
if (m.status === 'Ready') {
return (
- : } onClick={() => handleDispatch(m)} sx={{ borderRadius: 2, fontWeight: 700, boxShadow: 'none' }}>
- Send Out
+ handleDispatch(m)}>
+ {isBusy ? 'Working...' : 'Send Out'}
);
}
- 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={
- } onClick={() => setOpenModal(true)}>
- New Batch
-
- }
- />
-
-
- {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
- />
-
-
-
-
- setOpenModal(false)} disabled={creating}>Cancel
- : null}>
- {creating ? 'Creating…' : 'Create Batch'}
+ : `${manifests.length} batch${manifests.length === 1 ? '' : 'es'} · ${rangeLabel}`}
+
+
+
+
setOpenModal(true)} icon={ }>
+ New Batch
-
-
+
- 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 */}
+
+
+
+
);
}
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={
-
- } onClick={openStaffDialog} disabled={hubs.length === 0}
- sx={{ borderRadius: 2, fontWeight: 700 }}>
- Add Staff
-
- } onClick={openHubDialog}
- sx={{ borderRadius: 2, fontWeight: 700, bgcolor: BRAND, '&:hover': { bgcolor: '#9E0E20' } }}>
- Add New Hub
-
-
- }
- sx={{ flexWrap: 'wrap', gap: 1, '& .MuiCardHeader-action': { m: 0, alignSelf: 'center' } }}
- />
-
+
+
+
+
+
+
+
+ Hubs in {hub.city || 'your city'}
+ Every hub Doormile operates here
+
+
+
+ } onClick={openStaffDialog} disabled={hubs.length === 0}>Add Staff
+ } onClick={openHubDialog}>Add New Hub
+
+
{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 }))} />
-
-
-
- setHubDialog(false)} disabled={savingHub}>Cancel
- : null}
- sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#9E0E20' } }}>
- {savingHub ? 'Creating…' : 'Create Hub'}
-
-
+
+
{/* 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 ? : }
-
-
- )
- }} />
-
-
-
- setStaffDialog(false)} disabled={savingStaff}>Cancel
- : null}
- sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#9E0E20' } }}>
- {savingStaff ? 'Creating…' : 'Create Login'}
-
-
+
+
-
- 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
-
+
-
-
-
-
-
-
- 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)}
+ style={{ width: '100%', padding: '8px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', background: '#fff', color: '#0f172a', fontSize: '0.875rem' }}
+ >
+ {ORIGINS.map((o) => {o.label} )}
+
+
- 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: }} />
+
+
+
+
+
+ setCondition(e.target.value)}
+ style={{ width: '100%', padding: '8px 12px', borderRadius: '8px', border: '1px solid #cbd5e1', background: '#fff', color: '#0f172a', fontSize: '0.875rem' }}
+ >
+ Good
+ Damaged Box
+ Wet / Crushed
+ Missing Label
+
+
+
-
- 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: }} />
-
- : }
- sx={{ mt: 0.5, py: 1.3, borderRadius: 2, bgcolor: '#C01227', fontWeight: 700,
- boxShadow: '0 4px 14px rgba(192,18,39,0.30)', '&:hover': { bgcolor: '#9E0E20' } }}>
+
+ : }
+ style={{ width: '100%', justifyContent: 'center' }}
+ >
{submitting ? 'Scanning in…' : 'Mark Received at Hub'}
-
-
-
+
+
+
-
+
{/* 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 ? (
+ handleOpenAssign(row)} style={{ width: '100%', justifyContent: 'center' }}>
+ Choose Miler
+
+ ) : (
+
+ {row.status}
+
+ );
+ }
+ }
+ ], []);
return (
-
+
}
/>
-
- }
- action={
- : }
- onClick={handleAutoAssignAll}
- disabled={selectedOrders.length === 0 || busy}
- sx={{ borderRadius: 2, fontWeight: 700, boxShadow: 'none', whiteSpace: 'nowrap' }}
+
+
+
+
- {selectedOrders.length === 0 ? 'Auto-Assign' : `Auto-Assign (${selectedOrders.length})`}
-
- }
- 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'}`}
+
+
+
+
: undefined}
+ >
+ {busy ? 'Working...' : `Auto-Assign ${selectedOrders.size > 0 ? `(${selectedOrders.size})` : ''}`}
+
+
{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 ? (
- }
- onClick={() => handleOpenAssign(row)} sx={{ borderRadius: 2, fontWeight: 700, boxShadow: 'none' }}>
+ handleOpenAssign(row)}>
Choose a Miler
) : (
- {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 ? (
- }
- onClick={() => handleOpenAssign(row)} sx={{ borderRadius: 2, fontWeight: 700, whiteSpace: 'nowrap' }}>
- Choose Miler
-
- ) : (
- {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)} />
-
- ))}
-
- )}
-
-
- setAssignDialogOpen(false)} color="inherit">Cancel
- : null}>
- Confirm Assignment
-
-
-
+ )
+ }
+ />
+
+ )}
+
- 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={
+
+
+ setAssignDialogOpen(false)}>Cancel
+
+ {busy ? 'Working...' : 'Confirm Assignment'}
+
+
+
+ }
+ />
+
+
);
}
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 */}
-
- } size="small"
- sx={{ color: 'rgba(255,255,255,0.95)', textTransform: 'none', fontWeight: 600, mb: 1.25, ml: -0.5, '&:hover': { bgcolor: 'rgba(255,255,255,0.12)' } }}>
- Close
-
-
- : )} 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}
-
+
+ } style={{ color: '#fff', marginBottom: '12px', marginLeft: '-8px' }}>Close
+
+
+
+
+ {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 */}
-
- } href={`tel:${order.phone}`} sx={{ borderRadius: 2 }}>Call
- } onClick={onBack} sx={{ borderRadius: 2, bgcolor: rider.color, '&:hover': { bgcolor: rider.color } }}>Done
-
-
+
+ Assigned Miler
+
+
+
+ {rider.name}
+ {rider.vehicle} · {rider.vehicleNo} · Stop {index}
+
+
+
+
+
+ }
+ footer={
+
+ } onClick={() => window.open(`tel:${order.phone}`, '_self')}>Call
+ } onClick={onBack}>Done
+
+ }
+ />
);
}
@@ -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) && (
- } onClick={resetAnim} sx={{ textTransform: 'none', color: '#5F6368' }}>Reset
+ } onClick={resetAnim}>Reset
)}
-
+
-
+
{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 ? : }
-
-
+
+
+
+
+ setExpanded(isOpen ? null : rider.id)}
+ style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, background: 'none', border: 'none', cursor: 'pointer', padding: '4px 0', textAlign: 'left' }}
+ >
+
+ {rider.name}
+ {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 */}
-
+
: }
+ size="sm"
+ variant={isPlaying ? 'primary' : 'secondary'}
+ icon={isPlaying ? : }
onClick={() => startAnim(rider.id)}
- sx={{
- textTransform: 'none', fontWeight: 600, borderRadius: 2, flex: 1,
- ...(isPlaying
- ? { bgcolor: rider.color, '&:hover': { bgcolor: rider.color } }
- : { color: rider.color, borderColor: alpha(rider.color, 0.5), '&:hover': { borderColor: rider.color, bgcolor: alpha(rider.color, 0.06) } }),
- }}
+ style={{ flex: 1, justifyContent: 'center', ...(isPlaying ? { background: rider.color } : { color: rider.color, borderColor: hexAlpha(rider.color, 0.5) }) }}
>
{isPlaying ? 'Playing…' : 'Animate route'}
-
- 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
- set('hub', e.target.value)}>
- {HUB_OPTIONS.map(h => {h} )}
-
-
-
- )}
-
- {/* STEP 1 */}
- {activeStep === 1 && (
-
- set('zones', v)}
- renderInput={(params) => }
- renderTags={(value, getTagProps) => value.map((option, i) => )}
- />
-
- {mode === 'edit' && (
-
- Status
- set('status', e.target.value)}>
- {STATUS_OPTIONS.map(s => (
-
-
-
- ))}
-
-
- )}
-
- )}
-
- {/* STEP 2 */}
- {activeStep === 2 && (
-
-
- Vehicle Type
- set('vehicle', e.target.value)}>
- {VEHICLE_TYPES.map(v => {
- const IconComp = VEHICLES[v].icon;
- return (
-
-
-
-
- {v}
- {VEHICLES[v].capacity} parcels
-
-
-
- );
- })}
-
- {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
-
-
- }
- />
-
-
- )}
-
-
-
-
- Cancel
-
- {activeStep > 0 && Back }
- {activeStep < STEPS.length - 1 ? (
-
- Continue
-
- ) : (
- } sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E20' } }}>
- {mode === 'add' ? 'Add Miler' : 'Save Changes'}
-
- )}
-
-
- );
-}
-
-// 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 */}
-
- } size="small"
- sx={{ color: 'rgba(255,255,255,0.95)', textTransform: 'none', fontWeight: 600, mb: 1.25, ml: -0.5, '&:hover': { bgcolor: 'rgba(255,255,255,0.12)' } }}>
- Close
-
- {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
-
- {STATUS_OPTIONS.map((s) => {s} )}
-
-
-
- Vehicle
-
- {VEHICLE_TYPES.map((v) => {v} )}
-
-
-
-
- Hub
-
- {HUB_OPTIONS.map((h) => {h} )}
-
-
- 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 ? (
- <>
-
- Cancel
-
- } onClick={saveEdit} sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E1F' } }}>
- Save Changes
-
- >
- ) : (
- <>
- } href={`tel:${rider.phone}`} sx={{ borderRadius: 2 }}>
- Call Miler
-
- } onClick={() => setEditing(true)} sx={{ borderRadius: 2, bgcolor: '#C01227', '&:hover': { bgcolor: '#9E0E1F' } }}>
- Edit Details
-
- >
- )}
-
- >)}
-
- );
-}
-
-// Delete Dialog
-function DeleteDialog({ rider, onClose, onConfirm }) {
- return (
-
-
-
-
-
- Remove Miler?
-
- {rider?.name} will be permanently removed from the roster.
-
-
-
- Cancel
- Yes, Remove
-
-
- );
-}
-
-// 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 */}
-
-
-
-
-
-
-
+
+ } onClick={() => notify('Exported as CSV')}>Export Roster
+ } onClick={openAdd}>Add New Miler
+
}
- }}
- >
- Milers
-
-
-
- {kpi.total} milers • who's working and where
-
-
-
-
- {/* Actions Group - pushed to the far right with clear spacing between buttons */}
-
- }
- onClick={() => toast("Exported as CSV")}
- sx={{
- width: { xs: "100%", sm: "auto" },
- borderRadius: 2,
- px: 3,
- height: 48,
- textTransform: "none",
- fontWeight: 600,
- mr: { sm: 1 }, // Extra gap on desktop
- }}
- >
- Export Roster
-
-
- }
- onClick={openAdd}
- sx={{
- width: { xs: "100%", sm: "auto" },
- borderRadius: 2,
- bgcolor: "#C01227",
- px: 3.5,
- height: 48,
- textTransform: "none",
- fontWeight: 600,
- boxShadow: "none",
- "&:hover": {
- bgcolor: "#9E0E1F",
- boxShadow: "none",
- },
- }}
- >
- Add New Miler
-
-
-
+ />
{/* 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 && (
+
+ } />
+ } />
+
+ )}
+
-
- setVehicleFilter(e.target.value)}
- sx={{ borderRadius: 2 }}
- >
- All Vehicles
- {VEHICLE_TYPES.map((v) => (
-
- {v}
-
- ))}
-
-
-
-
-
- {!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
- { setSearch(''); setStatusFilter('All'); setVehicleFilter('All'); }} sx={{ mt: 3 }} variant="outlined">
- Clear all 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={
+ { setSearch(''); setStatusFilter('All'); setVehicleFilter('All'); }}>
+ Clear all filters
+
+ }
+ />
+ ) : 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">
+
+
+ );
+}
+
+// ════════════════════════════════════════════════════════════════════════════════
+// 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 && (
+
+ } style={{ color: 'var(--color-on-brand)', marginBottom: '12px', marginLeft: '-8px' }}>
+ Close
+
+ {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 ? (
+ <>
+ Cancel
+ } onClick={saveEdit}>Save Changes
+ >
+ ) : (
+ <>
+ } onClick={() => window.open(`tel:${rider.phone}`, '_self')}>Call Miler
+ } onClick={() => setEditing(true)}>Edit Details
+ onDelete(rider)}>Remove
+ >
+ )}
+
+
+ }
+ />
+ )}
+
+ );
+}
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' }
- }}
- />
- handleSearch()}
- fullWidth
- disabled={loading}
- startIcon={loading ? : null}
- sx={{ bgcolor: '#C01227', py: 1.5, borderRadius: 2, boxShadow: '0px 6px 16px rgba(192, 18, 39, 0.28)', '&:hover': { bgcolor: '#9E0E20' } }}
- >
- {loading ? 'Checking…' : 'Tell Me What To Do'}
-
-
-
-
+ handleSearch()}
+ style={{ width: '100%', justifyContent: 'center' }}
+ disabled={loading}
+ variant="primary"
+ >
+ {loading ? 'Checking…' : 'Tell Me What To Do'}
+
+
+
-
- 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) => (
-
-
-
-
+
))}
-
-
+
+
+
- {/* 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;