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

{activeChat.name}

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

+ Every parcel,
handled with ease. +

+

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

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

Hub Sign In

+

Sign in to your Doormile Hub operations account. - +

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

+ Join the Connected
Logistics Network. +

+

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

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

Request Access

+

Register your credentials to join hub operations. - +

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