Compare commits
18 Commits
d73c714290
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b6f5cdcaa8 | |||
| be0ff70ee4 | |||
| fd27ac92d8 | |||
| bbec0aa910 | |||
| 174ea5e8f2 | |||
| b78730a9a5 | |||
| 1978b2bb18 | |||
| 12341b1a0c | |||
| 8d0c796ba5 | |||
|
|
c882dbdcdd | ||
|
|
fe80659ccf | ||
|
|
2455088234 | ||
|
|
15b12ae0ec | ||
|
|
5bee7c6392 | ||
|
|
0d78530b25 | ||
|
|
671da509c1 | ||
|
|
37ba849bfb | ||
|
|
f38853bec1 |
9
.claude/settings.local.json
Normal file
9
.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/reports/orderSummary.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")",
|
||||||
|
"Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/reports/ridersummary.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")",
|
||||||
|
"Bash(node -e \"require\\('@babel/parser'\\).parse\\(require\\('fs'\\).readFileSync\\('src/pages/nearle/invoice/invoice.js','utf8'\\), { sourceType: 'module', plugins: ['jsx', 'optionalChaining', 'nullishCoalescingOperator', 'classProperties', 'objectRestSpread'] }\\); console.log\\('OK'\\)\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
31
.env
31
.env
@@ -1,31 +0,0 @@
|
|||||||
REACT_APP_VERSION = v2.1.0
|
|
||||||
GENERATE_SOURCEMAP = false
|
|
||||||
|
|
||||||
## Backend API URL
|
|
||||||
REACT_APP_API_URL=https://mock-data-api-nextjs.vercel.app/
|
|
||||||
|
|
||||||
## Google Map Key
|
|
||||||
|
|
||||||
REACT_APP_GOOGLE_MAPS_API_KEY=AIzaSyCF4KatYCI3vqz1_H3kiHeyS3yCMfYToh8
|
|
||||||
REACT_APP_RIDER_ACCESS_ID=AAAAILMpCEU:APA91bEavuOllBI6sFgYtxXAgNmAVwNA-MnCMHLGlR4_t7UqpLajAkdn3T0CZr_zaLBknLyim9ytFLMZgbeXmKqTad_PKCbqlYjHpaizVrLXtecxqyEy4UktIacK2UvHVUATHL-7VQQk
|
|
||||||
## Firebase - Google Auth
|
|
||||||
|
|
||||||
REACT_APP_FIREBASE_API_KEY=
|
|
||||||
REACT_APP_FIREBASE_AUTH_DOMAIN=
|
|
||||||
REACT_APP_FIREBASE_PROJECT_ID=
|
|
||||||
REACT_APP_FIREBASE_STORAGE_BUCKET=
|
|
||||||
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=
|
|
||||||
REACT_APP_FIREBASE_APP_ID=
|
|
||||||
REACT_APP_FIREBASE_MEASUREMENT_ID=
|
|
||||||
|
|
||||||
## AWS
|
|
||||||
|
|
||||||
REACT_APP_AWS_POOL_ID=
|
|
||||||
REACT_APP_AWS_APP_CLIENT_ID=
|
|
||||||
|
|
||||||
## Auth0
|
|
||||||
|
|
||||||
REACT_APP_AUTH0_CLIENT_ID=
|
|
||||||
REACT_APP_AUTH0_DOMAIN=
|
|
||||||
|
|
||||||
DISABLE_ESLINT_PLUGIN=true
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
|
||||||
REACT_APP_URL2='https://jupiter.nearle.app/live/api/v2'
|
|
||||||
REACT_APP_STAFF_TOKEN=''
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
REACT_APP_URL='https://jupiter.nearle.app/live/api/v1'
|
|
||||||
REACT_APP_URL2='https://jupiter.nearle.app/live/api/v2'
|
|
||||||
REACT_APP_STAFF_TOKEN=''
|
|
||||||
137
.gitignore
vendored
137
.gitignore
vendored
@@ -1,108 +1,107 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.development
|
||||||
|
.env.staging
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
# Runtime data
|
||||||
pids
|
pids
|
||||||
*.pid
|
*.pid
|
||||||
*.seed
|
*.seed
|
||||||
*.pid.lock
|
*.pid.lock
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
# Diagnostic reports
|
||||||
lib-cov
|
report.*.json
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
# Coverage
|
||||||
coverage
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
*.lcov
|
*.lcov
|
||||||
|
|
||||||
# nyc test coverage
|
# Cache
|
||||||
.nyc_output
|
.cache/
|
||||||
|
.parcel-cache/
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
build
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
.eslintcache
|
||||||
|
.npm
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
.rpt2_cache/
|
||||||
.rts2_cache_cjs/
|
.rts2_cache_cjs/
|
||||||
.rts2_cache_es/
|
.rts2_cache_es/
|
||||||
.rts2_cache_umd/
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IDE / Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Package manager
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
# Optional REPL history
|
# Optional REPL history
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
# Output of 'npm pack'
|
# Temporary files
|
||||||
*.tgz
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
|
||||||
# Yarn Integrity file
|
# Next.js
|
||||||
.yarn-integrity
|
.next/
|
||||||
|
|
||||||
# dotenv environment variables file
|
# Nuxt.js
|
||||||
# .env
|
.nuxt/
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
# Gatsby
|
||||||
.cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
.cache/
|
||||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
|
# VuePress
|
||||||
.vuepress/dist
|
.vuepress/dist
|
||||||
|
|
||||||
# Serverless directories
|
# Serverless
|
||||||
.serverless/
|
.serverless/
|
||||||
|
|
||||||
# FuseBox cache
|
# DynamoDB
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
.dynamodb/
|
||||||
|
|
||||||
# TernJS port file
|
# TernJS
|
||||||
.tern-port
|
.tern-port
|
||||||
|
|
||||||
# wincompare file
|
# Bower
|
||||||
*.bak
|
bower_components/
|
||||||
|
|
||||||
|
# JSPM
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional test outputs
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.svg
|
||||||
177
package-lock.json
generated
177
package-lock.json
generated
@@ -40,6 +40,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mui-daterange-picker": "^1.0.5",
|
"mui-daterange-picker": "^1.0.5",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"stylis-plugin-rtl": "^2.1.1",
|
"stylis-plugin-rtl": "^2.1.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"web-vitals": "^3.3.1",
|
"web-vitals": "^3.3.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"yup": "^1.1.1"
|
"yup": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -5648,6 +5650,15 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -6825,6 +6836,19 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
@@ -6989,6 +7013,15 @@
|
|||||||
"node": ">= 4.0"
|
"node": ">= 4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/collect-v8-coverage": {
|
"node_modules/collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -7242,6 +7275,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz",
|
||||||
"integrity": "sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g=="
|
"integrity": "sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g=="
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/create-ecdh": {
|
"node_modules/create-ecdh": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||||
@@ -9907,6 +9952,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
@@ -14337,6 +14391,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/papaparse": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/param-case": {
|
"node_modules/param-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||||
@@ -18683,6 +18743,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable": {
|
"node_modules/stable": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||||
@@ -20176,6 +20248,24 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@@ -20560,6 +20650,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||||
@@ -24455,6 +24566,11 @@
|
|||||||
"regex-parser": "^2.2.11"
|
"regex-parser": "^2.2.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="
|
||||||
|
},
|
||||||
"agent-base": {
|
"agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -25342,6 +25458,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz",
|
||||||
"integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="
|
"integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw=="
|
||||||
},
|
},
|
||||||
|
"cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"requires": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"chalk": {
|
"chalk": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||||
@@ -25468,6 +25593,11 @@
|
|||||||
"q": "^1.1.2"
|
"q": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="
|
||||||
|
},
|
||||||
"collect-v8-coverage": {
|
"collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -25667,6 +25797,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz",
|
||||||
"integrity": "sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g=="
|
"integrity": "sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g=="
|
||||||
},
|
},
|
||||||
|
"crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="
|
||||||
|
},
|
||||||
"create-ecdh": {
|
"create-ecdh": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||||
@@ -27606,6 +27741,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
|
||||||
},
|
},
|
||||||
|
"frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
|
||||||
|
},
|
||||||
"fraction.js": {
|
"fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
@@ -30787,6 +30927,11 @@
|
|||||||
"retry": "^0.13.1"
|
"retry": "^0.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"papaparse": {
|
||||||
|
"version": "5.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz",
|
||||||
|
"integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="
|
||||||
|
},
|
||||||
"param-case": {
|
"param-case": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||||
@@ -33726,6 +33871,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
|
||||||
},
|
},
|
||||||
|
"ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"requires": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"stable": {
|
"stable": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||||
@@ -34810,6 +34963,16 @@
|
|||||||
"is-typed-array": "^1.1.10"
|
"is-typed-array": "^1.1.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
|
||||||
|
},
|
||||||
|
"word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
|
||||||
|
},
|
||||||
"word-wrap": {
|
"word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@@ -35140,6 +35303,20 @@
|
|||||||
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
"integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"requires": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"xml-name-validator": {
|
"xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mui-daterange-picker": "^1.0.5",
|
"mui-daterange-picker": "^1.0.5",
|
||||||
"notistack": "^3.0.1",
|
"notistack": "^3.0.1",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"stylis-plugin-rtl": "^2.1.1",
|
"stylis-plugin-rtl": "^2.1.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"web-vitals": "^3.3.1",
|
"web-vitals": "^3.3.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"yup": "^1.1.1"
|
"yup": "^1.1.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
3
src/components/nearle_components/getValueColor.js
Normal file
3
src/components/nearle_components/getValueColor.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const getValueColor = (value) => {
|
||||||
|
return Number(value) !== 0 ? 'red' : 'inherit';
|
||||||
|
};
|
||||||
26
src/components/third-party/ReactTable.js
vendored
26
src/components/third-party/ReactTable.js
vendored
@@ -5,8 +5,10 @@ import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
|||||||
import { styled, useTheme } from '@mui/material/styles';
|
import { styled, useTheme } from '@mui/material/styles';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Chip,
|
Chip,
|
||||||
|
CircularProgress,
|
||||||
FormControl,
|
FormControl,
|
||||||
Grid,
|
Grid,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
@@ -493,20 +495,36 @@ SortingSelect.propTypes = {
|
|||||||
|
|
||||||
// ==============================|| CSV EXPORT ||============================== //
|
// ==============================|| CSV EXPORT ||============================== //
|
||||||
|
|
||||||
export const CSVExport = ({ data, filename, headers }) => {
|
export const CSVExport = ({ data, filename, headers, label, style, btnLoading, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<CSVLink data={data} filename={filename} headers={headers}>
|
<CSVLink data={data} filename={filename} headers={headers}>
|
||||||
<Tooltip title="Download CSV">
|
<Tooltip title="CSV Export">
|
||||||
<DownloadOutlined style={{ fontSize: '24px', color: 'gray', marginTop: 4, marginRight: 4, marginLeft: 4 }} />
|
<Button
|
||||||
|
startIcon={!btnLoading && <DownloadOutlined />}
|
||||||
|
variant={btnLoading ? 'outlined' : 'contained'}
|
||||||
|
sx={{ ...style }}
|
||||||
|
disabled={btnLoading}
|
||||||
|
onClick={(e) => {
|
||||||
|
onClick?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{btnLoading ? <CircularProgress size={20} thickness={5} /> : label || 'Download'}
|
||||||
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CSVLink>
|
</CSVLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default CSVExport;
|
||||||
|
|
||||||
CSVExport.propTypes = {
|
CSVExport.propTypes = {
|
||||||
data: PropTypes.array,
|
data: PropTypes.array,
|
||||||
headers: PropTypes.any,
|
headers: PropTypes.any,
|
||||||
filename: PropTypes.string
|
filename: PropTypes.string,
|
||||||
|
label: PropTypes.node,
|
||||||
|
style: PropTypes.object,
|
||||||
|
btnLoading: PropTypes.bool,
|
||||||
|
onClick: PropTypes.func
|
||||||
};
|
};
|
||||||
// ==============================|| EMPTY TABLE - NO DATA ||============================== //
|
// ==============================|| EMPTY TABLE - NO DATA ||============================== //
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const facebookColor = '#3b5998';
|
|||||||
export const linkedInColor = '#0e76a8';
|
export const linkedInColor = '#0e76a8';
|
||||||
|
|
||||||
// export const APP_DEFAULT_PATH = '/sample-page';
|
// export const APP_DEFAULT_PATH = '/sample-page';
|
||||||
export const APP_DEFAULT_PATH = '/nearle/orders';
|
export const APP_DEFAULT_PATH = '/nearle/dispatch';
|
||||||
export const HORIZONTAL_MAX_ITEM = 6;
|
export const HORIZONTAL_MAX_ITEM = 6;
|
||||||
export const DRAWER_WIDTH = 260;
|
export const DRAWER_WIDTH = 260;
|
||||||
|
|
||||||
|
|||||||
@@ -210,10 +210,14 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
const isSelected = selected === menu.id;
|
const isSelected = selected === menu.id;
|
||||||
const borderIcon = level === 1 ? <BorderOutlined style={{ fontSize: '1rem' }} /> : false;
|
const borderIcon = level === 1 ? <BorderOutlined style={{ fontSize: '1rem' }} /> : false;
|
||||||
const Icon = menu.icon;
|
const Icon = menu.icon;
|
||||||
const menuIcon = menu.icon ? <Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem' }} /> : borderIcon;
|
const menuIcon = menu.icon ? <Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: 'white' }} /> : borderIcon;
|
||||||
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? theme.palette.text.primary : theme.palette.primary.main;
|
// const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : 'text.primary';
|
||||||
|
// const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? theme.palette.text.primary : theme.palette.primary.main;
|
||||||
const popperId = miniMenuOpened ? `collapse-pop-${menu.id}` : undefined;
|
const popperId = miniMenuOpened ? `collapse-pop-${menu.id}` : undefined;
|
||||||
const FlexBox = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' };
|
const FlexBox = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' };
|
||||||
|
const textColor = 'white';
|
||||||
|
const iconSelectedColor = 'white';
|
||||||
|
// const isSelected = true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -227,9 +231,11 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
sx={{
|
sx={{
|
||||||
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
||||||
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
||||||
|
|
||||||
...(drawerOpen && {
|
...(drawerOpen && {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.light'
|
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter'
|
||||||
|
bgcolor: '#7b1fa2'
|
||||||
},
|
},
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
bgcolor: 'transparent',
|
bgcolor: 'transparent',
|
||||||
@@ -239,13 +245,14 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
}),
|
}),
|
||||||
...(!drawerOpen && {
|
...(!drawerOpen && {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: 'primary.light'
|
bgcolor: 'transparent'
|
||||||
|
// bgcolor:'#7b1fa2'
|
||||||
},
|
},
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: 'white'
|
bgcolor: 'transparent'
|
||||||
},
|
},
|
||||||
bgcolor: 'white'
|
bgcolor: 'transparent'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -255,7 +262,10 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
onClick={handlerIconLink}
|
onClick={handlerIconLink}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 28,
|
minWidth: 28,
|
||||||
color: selected === menu.id ? 'primary.main' : 'white',
|
// color: selected === menu.id ? 'primary.main' : textColor,
|
||||||
|
// color: selected === menu.id ? textColor : textColor,
|
||||||
|
// bgcolor:'white',
|
||||||
|
// color:'white',
|
||||||
...(!drawerOpen && {
|
...(!drawerOpen && {
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
width: 36,
|
width: 36,
|
||||||
@@ -264,13 +274,17 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
|
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'secondary.lighter'
|
||||||
|
bgcolor: '#7b1fa2',
|
||||||
|
color: 'white'
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...(!drawerOpen &&
|
...(!drawerOpen &&
|
||||||
selected === menu.id && {
|
selected === menu.id && {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.900' : 'primary.lighter',
|
bgcolor: 'primary.light',
|
||||||
|
color: 'primary.main',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.darker' : 'primary.lighter'
|
bgcolor: '#7b1fa2',
|
||||||
|
color: 'primary.main'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -281,7 +295,12 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography variant="h6" color={selected === menu.id ? 'white' : 'white'}>
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
// color={selected === menu.id ? 'primary' : textColor}
|
||||||
|
// color={'white'}
|
||||||
|
color={selected === menu.id ? textColor : textColor}
|
||||||
|
>
|
||||||
{menu.title}
|
{menu.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
@@ -296,9 +315,22 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
)}
|
)}
|
||||||
{(drawerOpen || (!drawerOpen && level !== 1)) &&
|
{(drawerOpen || (!drawerOpen && level !== 1)) &&
|
||||||
(miniMenuOpened || open ? (
|
(miniMenuOpened || open ? (
|
||||||
<UpOutlined style={{ fontSize: '0.625rem', marginLeft: 1, color: theme.palette.primary.main }} />
|
<UpOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
marginLeft: 1,
|
||||||
|
// color: theme.palette.primary.main
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DownOutlined style={{ fontSize: '0.625rem', marginLeft: 1, color: 'white' }} />
|
<DownOutlined
|
||||||
|
style={{
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
marginLeft: 1,
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!drawerOpen && (
|
{!drawerOpen && (
|
||||||
@@ -328,8 +360,8 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
mt: 1.5,
|
mt: 1.5,
|
||||||
boxShadow: theme.customShadows.z1,
|
boxShadow: theme.customShadows.z1,
|
||||||
backgroundImage: 'none',
|
backgroundImage: 'none',
|
||||||
border: `1px solid ${theme.palette.primary.main}`,
|
border: `2px solid ${theme.palette.primary.main}`,
|
||||||
bgcolor: 'primary.main'
|
width: 'auto'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ClickAwayListener onClickAway={handleClose}>
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
@@ -373,7 +405,14 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
>
|
>
|
||||||
<Box onClick={handlerIconLink} sx={FlexBox}>
|
<Box onClick={handlerIconLink} sx={FlexBox}>
|
||||||
{menuIcon && (
|
{menuIcon && (
|
||||||
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36, color: theme.palette.secondary.dark }}>
|
<ListItemIcon
|
||||||
|
sx={{
|
||||||
|
my: 'auto',
|
||||||
|
minWidth: !menu.icon ? 18 : 36
|
||||||
|
// color: theme.palette.secondary.dark
|
||||||
|
// color:'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{menuIcon}
|
{menuIcon}
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
)}
|
)}
|
||||||
@@ -386,7 +425,12 @@ const NavCollapse = ({ menu, level, parentId, setSelectedItems, selectedItems, s
|
|||||||
)}
|
)}
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography variant="body1" color="inherit" sx={{ my: 'auto' }}>
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
// color="inherit"
|
||||||
|
// color="white"
|
||||||
|
sx={{ my: 'auto' }}
|
||||||
|
>
|
||||||
{menu.title}
|
{menu.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import NavCollapse from './NavCollapse';
|
|||||||
import SimpleBar from 'components/third-party/SimpleBar';
|
import SimpleBar from 'components/third-party/SimpleBar';
|
||||||
import Transitions from 'components/@extended/Transitions';
|
import Transitions from 'components/@extended/Transitions';
|
||||||
|
|
||||||
import { MenuOrientation, ThemeMode } from 'config';
|
import { MenuOrientation } from 'config';
|
||||||
import useConfig from 'hooks/useConfig';
|
import useConfig from 'hooks/useConfig';
|
||||||
import { dispatch, useSelector } from 'store';
|
import { dispatch, useSelector } from 'store';
|
||||||
import { activeID } from 'store/reducers/menu';
|
import { activeID } from 'store/reducers/menu';
|
||||||
@@ -227,9 +227,9 @@ const NavGroup = ({ item, lastItem, remItems, lastItemId, setSelectedItems, sele
|
|||||||
item.title &&
|
item.title &&
|
||||||
drawerOpen && (
|
drawerOpen && (
|
||||||
<Box sx={{ pl: 3, mb: 1.5 }}>
|
<Box sx={{ pl: 3, mb: 1.5 }}>
|
||||||
<Typography variant="subtitle2"
|
<Typography
|
||||||
// color={theme.palette.mode === ThemeMode.DARK ? 'textSecondary' : 'text.secondary'}
|
variant="subtitle2"
|
||||||
sx={{color:'#fff'}}
|
sx={{ color: '#fff' }}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, u
|
|||||||
|
|
||||||
// project import
|
// project import
|
||||||
import Dot from 'components/@extended/Dot';
|
import Dot from 'components/@extended/Dot';
|
||||||
|
|
||||||
import { MenuOrientation, ThemeMode } from 'config';
|
import { MenuOrientation, ThemeMode } from 'config';
|
||||||
import useConfig from 'hooks/useConfig';
|
import useConfig from 'hooks/useConfig';
|
||||||
import { activeItem, openDrawer } from 'store/reducers/menu';
|
import { activeItem, openDrawer, setSelectedMenu } from 'store/reducers/menu';
|
||||||
|
|
||||||
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
|
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
|
||||||
|
|
||||||
@@ -35,10 +36,15 @@ const NavItem = ({ item, level }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const itemIcon = item.icon ? <Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem' }} /> : false;
|
|
||||||
|
|
||||||
const isSelected = openItem.findIndex((id) => id === item.id) > -1;
|
const isSelected = openItem.findIndex((id) => id === item.id) > -1;
|
||||||
|
|
||||||
|
const itemIcon = item.icon ? (
|
||||||
|
<Icon style={{ fontSize: drawerOpen ? '1rem' : '1.25rem', color: isSelected ? '#662582' : '#fff' }} />
|
||||||
|
) : (
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
// const { pathname } = useLocation();
|
// const { pathname } = useLocation();
|
||||||
const pathname = document.location.pathname;
|
const pathname = document.location.pathname;
|
||||||
|
|
||||||
@@ -59,10 +65,15 @@ const NavItem = ({ item, level }) => {
|
|||||||
if (pathname.includes(item.url)) {
|
if (pathname.includes(item.url)) {
|
||||||
dispatch(activeItem({ openItem: [item.id] }));
|
dispatch(activeItem({ openItem: [item.id] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : 'text.primary';
|
useEffect(() => {
|
||||||
|
dispatch(setSelectedMenu(pathname));
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
const textColor = theme.palette.mode === ThemeMode.DARK ? 'grey.400' : '#fff';
|
||||||
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? 'text.primary' : 'primary.main';
|
const iconSelectedColor = theme.palette.mode === ThemeMode.DARK && drawerOpen ? 'text.primary' : 'primary.main';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -72,14 +83,16 @@ const NavItem = ({ item, level }) => {
|
|||||||
{...listItemProps}
|
{...listItemProps}
|
||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
selected={isSelected}
|
selected={isSelected}
|
||||||
|
onClick={() => {
|
||||||
|
// dispatch(setSelectedMenu(item));
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
zIndex: 1201,
|
zIndex: 1201,
|
||||||
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
pl: drawerOpen ? `${level * 28}px` : 1.5,
|
||||||
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
py: !drawerOpen && level === 1 ? 1.25 : 1,
|
||||||
...(drawerOpen && {
|
...(drawerOpen && {
|
||||||
// bgcolor: 'primary.light',
|
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.light'
|
bgcolor: '#7b1fa2'
|
||||||
},
|
},
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
|
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'divider' : 'primary.lighter',
|
||||||
@@ -92,41 +105,43 @@ const NavItem = ({ item, level }) => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...(!drawerOpen && {
|
...(!drawerOpen && {
|
||||||
|
bgcolor: '#662582',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: 'primary.light'
|
bgcolor: '#662582'
|
||||||
},
|
},
|
||||||
'&.Mui-selected': {
|
'&.Mui-selected': {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
bgcolor: 'white'
|
bgcolor: 'transparent'
|
||||||
},
|
},
|
||||||
bgcolor: 'white'
|
bgcolor: 'transparent'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
{...(downLG && {
|
{...(downLG && {
|
||||||
onClick: () => dispatch(openDrawer(false))
|
onClick: () => {
|
||||||
|
dispatch(openDrawer(false));
|
||||||
|
}
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{itemIcon && (
|
{itemIcon && (
|
||||||
<ListItemIcon
|
<ListItemIcon
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 28,
|
minWidth: 28,
|
||||||
color: isSelected ? iconSelectedColor : 'white',
|
|
||||||
...(!drawerOpen && {
|
...(!drawerOpen && {
|
||||||
// borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
// '&:hover': {
|
'&:hover': {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'secondary.light' : 'primary.lighter'
|
bgcolor: '#7b1fa2'
|
||||||
// }
|
}
|
||||||
}),
|
}),
|
||||||
...(!drawerOpen &&
|
...(!drawerOpen &&
|
||||||
isSelected && {
|
isSelected && {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.900' : 'primary.lighter',
|
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.900' : 'primary.lighter',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
// bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.darker' : 'primary.lighter'
|
bgcolor: theme.palette.mode === ThemeMode.DARK ? 'primary.darker' : 'primary.lighter'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@@ -137,13 +152,7 @@ const NavItem = ({ item, level }) => {
|
|||||||
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
{(drawerOpen || (!drawerOpen && level !== 1)) && (
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography
|
<Typography variant="h6" sx={{ color: isSelected ? iconSelectedColor : textColor, whiteSpace: 'nowrap' }}>
|
||||||
variant="h6"
|
|
||||||
sx={{
|
|
||||||
ml: 1,
|
|
||||||
color: isSelected ? theme.palette.primary.main : 'white'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const HeaderContent = () => {
|
|||||||
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
|
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
|
||||||
<TbBoxMultiple1 />
|
<TbBoxMultiple1 />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<Typography color="textPrimary"> Orders</Typography>
|
<Typography color="textPrimary">Create Orders</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -188,7 +188,7 @@ const HeaderContent = () => {
|
|||||||
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
|
<ListItemIcon sx={{ mr: 1, fontSize: '20px' }}>
|
||||||
<GrMultiple />
|
<GrMultiple />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<Typography color="textPrimary"> Group Orders</Typography>
|
<Typography color="textPrimary">Create Group Orders</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import AppBarStyled from './AppBarStyled';
|
|||||||
import HeaderContent from './HeaderContent';
|
import HeaderContent from './HeaderContent';
|
||||||
import IconButton from 'components/@extended/IconButton';
|
import IconButton from 'components/@extended/IconButton';
|
||||||
|
|
||||||
import { MenuOrientation, ThemeMode } from 'config';
|
import { MenuOrientation } from 'config';
|
||||||
import useConfig from 'hooks/useConfig';
|
import useConfig from 'hooks/useConfig';
|
||||||
import { dispatch, useSelector } from 'store';
|
import { dispatch, useSelector } from 'store';
|
||||||
import { openDrawer } from 'store/reducers/menu';
|
import { openDrawer } from 'store/reducers/menu';
|
||||||
@@ -32,9 +32,6 @@ const Header = () => {
|
|||||||
// header content
|
// header content
|
||||||
const headerContent = useMemo(() => <HeaderContent />, []);
|
const headerContent = useMemo(() => <HeaderContent />, []);
|
||||||
|
|
||||||
const iconBackColorOpen = theme.palette.mode === ThemeMode.DARK ? 'grey.200' : 'grey.300';
|
|
||||||
const iconBackColor = theme.palette.mode === ThemeMode.DARK ? 'background.default' : 'grey.100';
|
|
||||||
|
|
||||||
// common header
|
// common header
|
||||||
const mainHeader = (
|
const mainHeader = (
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
@@ -43,9 +40,6 @@ const Header = () => {
|
|||||||
aria-label="open drawer"
|
aria-label="open drawer"
|
||||||
onClick={() => dispatch(openDrawer(!drawerOpen))}
|
onClick={() => dispatch(openDrawer(!drawerOpen))}
|
||||||
edge="start"
|
edge="start"
|
||||||
// color="secondary"
|
|
||||||
// variant="light"
|
|
||||||
// sx={{ color: 'text.primary', bgcolor: drawerOpen ? iconBackColorOpen : iconBackColor, ml: { xs: 0, lg: -2 } }}
|
|
||||||
sx={{
|
sx={{
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
bgcolor: 'transparent',
|
bgcolor: 'transparent',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { AiOutlineDashboard } from 'react-icons/ai';
|
|||||||
import { TbListDetails } from 'react-icons/tb';
|
import { TbListDetails } from 'react-icons/tb';
|
||||||
import { LiaFileInvoiceSolid } from 'react-icons/lia';
|
import { LiaFileInvoiceSolid } from 'react-icons/lia';
|
||||||
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
|
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
|
||||||
|
import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
|
||||||
|
|
||||||
// assets
|
// assets
|
||||||
import {
|
import {
|
||||||
@@ -49,6 +50,13 @@ const nearle = {
|
|||||||
title: <FormattedMessage id="MENU" />,
|
title: <FormattedMessage id="MENU" />,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: 'dispatch',
|
||||||
|
title: <FormattedMessage id="Dispatch" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/nearle/dispatch',
|
||||||
|
icon: RouteOutlinedIcon
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'orders',
|
id: 'orders',
|
||||||
title: <FormattedMessage id="Orders" />,
|
title: <FormattedMessage id="Orders" />,
|
||||||
@@ -96,14 +104,14 @@ const nearle = {
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
url: '/nearle/reports/ridersummary',
|
url: '/nearle/reports/ridersummary',
|
||||||
icon: DirectionsBikeOutlinedIcon
|
icon: DirectionsBikeOutlinedIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'RiderLogs',
|
||||||
|
title: <FormattedMessage id="RiderLogs" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/nearle/reports/riderlogs',
|
||||||
|
icon: DirectionsBikeOutlinedIcon
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// id: 'RiderLogs',
|
|
||||||
// title: <FormattedMessage id="RiderLogs" />,
|
|
||||||
// type: 'item',
|
|
||||||
// url: '/nearle/reports/riderlogs',
|
|
||||||
// icon: DirectionsBikeOutlinedIcon
|
|
||||||
// }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
341
src/pages/nearle/_shared/ordersDesign.js
Normal file
341
src/pages/nearle/_shared/ordersDesign.js
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
// ============================================================================
|
||||||
|
// ordersDesign.js — shared design-system primitives for the operator pages.
|
||||||
|
//
|
||||||
|
// Source of truth for the look-and-feel established on the Orders Details
|
||||||
|
// screen (src/pages/nearle/reports/ordersDetails.js). All other report and
|
||||||
|
// list pages import from here so the entire admin panel reads as one product.
|
||||||
|
//
|
||||||
|
// Exports:
|
||||||
|
// DT, BRAND, BRAND_LIGHT — palette tokens
|
||||||
|
// tint / soft / ring / edge — alpha helpers (08 / 18 / 26 / 55 hex)
|
||||||
|
// formatNumberToRupees — INR currency formatter
|
||||||
|
// STATUS_META — semantic per-status meta (label/color/icon)
|
||||||
|
// StatusBadge — filled pill, white text on solid color
|
||||||
|
// TimelineCell — time-dominant (bold) / date-secondary (muted)
|
||||||
|
// MetricPill — small money/number pill for table cells
|
||||||
|
// SoftPaper — autocomplete dropdown surface
|
||||||
|
// AccentAvatar — colored circular icon avatar
|
||||||
|
// pillFieldSx — rounded pill text-field styling
|
||||||
|
// PrimaryButtonSx / SecondaryButtonSx — unified 32-px action-button system
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Avatar, Box, Paper, Stack, Typography } from '@mui/material';
|
||||||
|
import {
|
||||||
|
MdLocalShipping,
|
||||||
|
MdHourglassEmpty,
|
||||||
|
MdCheckCircle,
|
||||||
|
MdCancel,
|
||||||
|
MdAccessTime,
|
||||||
|
MdHistoryToggleOff,
|
||||||
|
MdAssignmentTurnedIn
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Design tokens
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const DT = {
|
||||||
|
radiusPill: 999,
|
||||||
|
radiusCard: 16,
|
||||||
|
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||||
|
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||||
|
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||||
|
textPrimary: '#0f172a',
|
||||||
|
textSecondary: '#64748b',
|
||||||
|
textMuted: '#94a3b8',
|
||||||
|
borderSubtle: '#e2e8f0',
|
||||||
|
divider: '#f1f5f9',
|
||||||
|
surface: '#ffffff',
|
||||||
|
surfaceAlt: '#f8fafc'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BRAND = '#662582';
|
||||||
|
export const BRAND_LIGHT = '#9255AB';
|
||||||
|
|
||||||
|
const dtA = (c, suffix) => `${c}${suffix}`;
|
||||||
|
export const tint = (c) => dtA(c, '08');
|
||||||
|
export const soft = (c) => dtA(c, '18');
|
||||||
|
export const ring = (c) => dtA(c, '26');
|
||||||
|
export const edge = (c) => dtA(c, '55');
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Money formatter
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export function formatNumberToRupees(value) {
|
||||||
|
return new Intl.NumberFormat('en-IN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'INR',
|
||||||
|
minimumFractionDigits: 2
|
||||||
|
}).format(value || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Status meta — colors per brand standard:
|
||||||
|
// green=delivered, amber=pending, blue=created/processing,
|
||||||
|
// red=cancelled, dark-red=failed, purple=on-hold.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const STATUS_META = {
|
||||||
|
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
|
||||||
|
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
|
accepted: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
|
||||||
|
ready: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
|
||||||
|
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdCheckCircle },
|
||||||
|
picked: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
|
||||||
|
active: { label: 'Active', color: '#0ea5e9', icon: MdLocalShipping },
|
||||||
|
modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
|
||||||
|
onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
||||||
|
'on hold': { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
||||||
|
closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
|
||||||
|
completed: { label: 'Completed', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
skipped: { label: 'Skipped', color: '#f97316', icon: MdCancel },
|
||||||
|
failed: { label: 'Failed', color: '#991b1b', icon: MdCancel },
|
||||||
|
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// StatusBadge — filled pill, white text on solid color, high-contrast.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const StatusBadge = ({ status, minWidth = 86 }) => {
|
||||||
|
if (!status) return null;
|
||||||
|
const meta = STATUS_META[String(status).toLowerCase()] || {
|
||||||
|
label: status,
|
||||||
|
color: DT.textMuted,
|
||||||
|
icon: MdHistoryToggleOff
|
||||||
|
};
|
||||||
|
const Icon = meta.icon;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 1.125,
|
||||||
|
py: 0.375,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: meta.color,
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 0.2,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
minWidth,
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: `0 1px 2px ${ring(meta.color)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={12} /> {meta.label}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// TimelineCell — time large/bold/high-contrast, date small/muted/secondary.
|
||||||
|
// No decorative dot.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const TimelineCell = ({ value, utc: useUtc = false }) => {
|
||||||
|
if (!value) {
|
||||||
|
return (
|
||||||
|
<Typography sx={{ fontSize: 12, color: DT.textMuted, fontWeight: 700 }}>—</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const d = useUtc ? dayjs(value).utc() : dayjs(value);
|
||||||
|
return (
|
||||||
|
<Stack spacing={0} sx={{ lineHeight: 1.1 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: 800,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
lineHeight: 1.15
|
||||||
|
}}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{d.format('hh:mm A')}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: DT.textMuted,
|
||||||
|
lineHeight: 1.2
|
||||||
|
}}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{d.format('DD MMM YYYY')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MetricPill — small money / number pill used in table cells.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const MetricPill = ({ value, color, icon, isMoney = false }) => {
|
||||||
|
const n = Number(value);
|
||||||
|
const display = isMoney ? formatNumberToRupees(n) : Number.isFinite(n) ? n : value || 0;
|
||||||
|
const isZero = !Number.isFinite(n) || n === 0;
|
||||||
|
if (isZero) {
|
||||||
|
return (
|
||||||
|
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700 }}>
|
||||||
|
{display}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 0.875,
|
||||||
|
py: 0.25,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(color),
|
||||||
|
border: `1px solid ${edge(color)}`,
|
||||||
|
color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 800,
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{display}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// SoftPaper — autocomplete dropdown surface.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const SoftPaper = (props) => (
|
||||||
|
<Paper
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
mt: 0.75,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: DT.shadowPop,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
overflow: 'hidden'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// AccentAvatar — colored circular icon used inside filter chips & headers.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
bgcolor: selected ? color : soft(color),
|
||||||
|
color: selected ? '#fff' : color,
|
||||||
|
transition: 'background-color 0.15s, color 0.15s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// pillFieldSx — rounded pill style for Autocomplete/TextField inputs.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const pillFieldSx = (color) => ({
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
borderRadius: DT.radiusPill + 'px',
|
||||||
|
bgcolor: tint(color),
|
||||||
|
fontWeight: 600,
|
||||||
|
'& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
|
||||||
|
'&:hover fieldset': { borderColor: color },
|
||||||
|
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
|
||||||
|
'&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Unified action-button styles — both render at 32px height with same typography.
|
||||||
|
// SecondaryButtonSx → outlined (white bg, neutral border)
|
||||||
|
// PrimaryButtonSx → filled brand (purple bg, white text)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const SecondaryButtonSx = (active = false) => ({
|
||||||
|
height: 32,
|
||||||
|
px: 1.25,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
borderColor: active ? edge(BRAND) : DT.borderSubtle,
|
||||||
|
color: active ? BRAND : DT.textPrimary,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: '#fff',
|
||||||
|
borderColor: BRAND,
|
||||||
|
color: BRAND,
|
||||||
|
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||||||
|
},
|
||||||
|
'&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PrimaryButtonSx = {
|
||||||
|
height: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
px: 1.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: BRAND,
|
||||||
|
color: '#fff',
|
||||||
|
textTransform: 'none',
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: 700,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
boxShadow: `0 2px 6px ${ring(BRAND)}`,
|
||||||
|
'&:hover': { bgcolor: BRAND_LIGHT, boxShadow: `0 4px 12px ${ring(BRAND)}` },
|
||||||
|
'&:focus-visible': { boxShadow: `0 0 0 3px ${ring(BRAND)}` },
|
||||||
|
'& .MuiButton-startIcon': { mr: 0.75, '& svg': { fontSize: 16 } }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// PageHeaderShellSx — the gradient header strip used by every operator page.
|
||||||
|
// Spread onto the outer <Paper> element.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const PageHeaderShellSx = {
|
||||||
|
mb: { xs: 1, md: 1.25 },
|
||||||
|
px: { xs: 1.5, sm: 2 },
|
||||||
|
py: { xs: 1, sm: 1.25 },
|
||||||
|
borderRadius: 2,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||||
|
boxShadow: DT.shadowMd
|
||||||
|
};
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// CompactKpiCardSx — visual envelope for a compact KPI card.
|
||||||
|
// Caller supplies the colored left bar via the `accent` arg.
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
export const CompactKpiCardSx = (accentColor) => ({
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
px: { xs: 1.25, sm: 1.5 },
|
||||||
|
py: { xs: 0.875, sm: 1.125 },
|
||||||
|
borderRadius: 2,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
background: '#fff',
|
||||||
|
transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-1px)',
|
||||||
|
boxShadow: DT.shadowMd,
|
||||||
|
borderColor: edge(accentColor)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// MdAccessTime re-exported so callers can use the inline time icon
|
||||||
|
// without an extra react-icons import.
|
||||||
|
export { MdAccessTime };
|
||||||
@@ -147,9 +147,9 @@ export const fetchOrdersSummary = async ({ queryKey }) => {
|
|||||||
// ==============================|| getreportlocationsummary (orders summary)||============================== //
|
// ==============================|| getreportlocationsummary (orders summary)||============================== //
|
||||||
export const getreportlocationsummary = async ({ queryKey }) => {
|
export const getreportlocationsummary = async ({ queryKey }) => {
|
||||||
console.log('queryKey for getreportlocationsummary', queryKey);
|
console.log('queryKey for getreportlocationsummary', queryKey);
|
||||||
const [startdate, enddate, locationId] = queryKey;
|
const [startdate, enddate, locationId, debouncedSearch] = queryKey;
|
||||||
const response = await axios.get(
|
const response = await axios.get(
|
||||||
`${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?tenantid=${tenid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}`
|
`${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?tenantid=${tenid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}&keyword=${debouncedSearch}`
|
||||||
);
|
);
|
||||||
console.log('getreportlocationsummary', response.data.details);
|
console.log('getreportlocationsummary', response.data.details);
|
||||||
|
|
||||||
@@ -184,10 +184,12 @@ export const gettenantlocations = async ({ queryKey }) => {
|
|||||||
const [, searchLocation] = queryKey;
|
const [, searchLocation] = queryKey;
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations?tenantid=${tenid}&keyword=${searchLocation}`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations?tenantid=${tenid}&keyword=${searchLocation}`);
|
||||||
return response.data?.details || []; // safe fallback
|
return response.data?.details || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Must return an array — downstream consumers do `.map`/`.length` and a
|
||||||
|
// string here crashes the entire Locations page.
|
||||||
console.error('Error fetching tenant locations:', error);
|
console.error('Error fetching tenant locations:', error);
|
||||||
return error.message;
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -292,10 +294,140 @@ export const fetchCount = async ({ queryKey }) => {
|
|||||||
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
|
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
|
||||||
|
|
||||||
export const fetchRidersLogs = async ({ queryKey }) => {
|
export const fetchRidersLogs = async ({ queryKey }) => {
|
||||||
const [tenantid, startdate] = queryKey;
|
const [appId, startdate, riderSearch = ''] = queryKey;
|
||||||
|
// const riderLogsResponse = await axios.get(
|
||||||
|
// `${process.env.REACT_APP_URL}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${
|
||||||
|
// riderSearch || ''
|
||||||
|
// }`
|
||||||
|
// );
|
||||||
const riderLogsResponse = await axios.get(
|
const riderLogsResponse = await axios.get(
|
||||||
`${process.env.REACT_APP_URL}/partners/getriderlogs/?tenantid=${tenantid}&fromdate=${startdate || ''}`
|
`https://jupiter.nearle.app/live/api/v2/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${
|
||||||
|
startdate || ''
|
||||||
|
}&keyword=${riderSearch || ''}`
|
||||||
);
|
);
|
||||||
console.log('fetchRidersLogs', riderLogsResponse.data.details);
|
console.log('fetchRidersLogs', riderLogsResponse.data.details);
|
||||||
return riderLogsResponse.data.details;
|
return riderLogsResponse.data.details;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ==============================|| Dispatch / Preview APIs (ported from xpressconsole) ||============================== //
|
||||||
|
|
||||||
|
// Returns the rider's latest periodic log entry — battery, GPS, status,
|
||||||
|
// current order. Used by the Rider Info modal on the Dispatch page.
|
||||||
|
export const getRiderPeriodicLogs = async (userid) => {
|
||||||
|
const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
if (response.data && response.data.status) return response.data.data;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetches the riders dropdown list for an app location.
|
||||||
|
export const fetchRidersList = async ({ queryKey }) => {
|
||||||
|
try {
|
||||||
|
const [, appId] = queryKey;
|
||||||
|
const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
|
||||||
|
const response = data?.details
|
||||||
|
? data.details.map((val) => ({
|
||||||
|
...val,
|
||||||
|
label: `${val.firstname} ${val.lastname} | ${val.contactno}`
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
OpenToast(err.message, 'error', 2000);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optimise the orders (bike solver).
|
||||||
|
export const createOptimisationDeliveries = async (deliveryData) => {
|
||||||
|
const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Server-side step reconciliation after manual edits in Preview.
|
||||||
|
export const reconcileSteps = async ({ riders }) => {
|
||||||
|
const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/reconcile-steps`, { riders });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Batch efficiency analysis — batch ∈ 'morning' | 'afternoon' | 'evening'.
|
||||||
|
export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
|
||||||
|
const response = await axios.post(
|
||||||
|
`https://routes.workolik.com/api/v1/batch/efficiency`,
|
||||||
|
{ batch, tenant_id: tenantId },
|
||||||
|
{
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
validateStatus: () => true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Final commit of dispatched deliveries — coerces userid/rider_id to int at
|
||||||
|
// the boundary so a string upstream can't cause a 500 unmarshal error.
|
||||||
|
export const finalCreatedeliveries = async (deliveryData) => {
|
||||||
|
const toInt = (v) => {
|
||||||
|
const n = Number(v);
|
||||||
|
return Number.isFinite(n) ? n : v;
|
||||||
|
};
|
||||||
|
const deliveries = (deliveryData.deliveries || []).map((d) => ({
|
||||||
|
...d,
|
||||||
|
userid: toInt(d.userid),
|
||||||
|
rider_id: toInt(d.rider_id)
|
||||||
|
}));
|
||||||
|
const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveries);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto rider assignment via either the bike solver or the auto/multi-trip
|
||||||
|
// solver. Body shape differs per mode; absent_riders is merged through.
|
||||||
|
export const createAutomationDeliveries = async (variables) => {
|
||||||
|
const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : [];
|
||||||
|
|
||||||
|
const url =
|
||||||
|
variables.selectedMode.value == 1
|
||||||
|
? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
|
||||||
|
: `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
|
||||||
|
|
||||||
|
const body =
|
||||||
|
variables.selectedMode.value == 1
|
||||||
|
? { deliveries: variables.deliveries, absent_riders: absentRiders }
|
||||||
|
: { ...(variables.data || {}), absent_riders: absentRiders };
|
||||||
|
|
||||||
|
const response = await axios.post(url, body);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Push a notification to a rider after assignment.
|
||||||
|
export const notifyRider = async (riderToken) => {
|
||||||
|
if (!riderToken) {
|
||||||
|
throw new Error('Invalid rider token');
|
||||||
|
}
|
||||||
|
const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
|
||||||
|
token: riderToken,
|
||||||
|
notification: {
|
||||||
|
title: 'NearleXpress',
|
||||||
|
body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
|
||||||
|
sound: 'ring',
|
||||||
|
image: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Paginated deliveries fetch — supports both "All zones" (appId === 0) and
|
||||||
|
// per-zone scoping. Returns { rows, nextPage } for useInfiniteQuery.
|
||||||
|
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
|
||||||
|
let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
|
||||||
|
currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
|
||||||
|
const url =
|
||||||
|
appId === 0
|
||||||
|
? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
|
||||||
|
: `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
|
||||||
|
const response = await axios.get(url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
rows: response.data.details,
|
||||||
|
nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
976
src/pages/nearle/dispatch/CompareDataPanel.js
Normal file
976
src/pages/nearle/dispatch/CompareDataPanel.js
Normal file
@@ -0,0 +1,976 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
MdPublic,
|
||||||
|
MdSwapHoriz,
|
||||||
|
MdExpandMore,
|
||||||
|
MdCheckCircle,
|
||||||
|
MdAccessTime,
|
||||||
|
MdStraighten,
|
||||||
|
MdErrorOutline,
|
||||||
|
MdFormatListBulleted,
|
||||||
|
MdTimer,
|
||||||
|
MdWarning,
|
||||||
|
MdClose,
|
||||||
|
MdSpeed,
|
||||||
|
MdStar,
|
||||||
|
MdFlag,
|
||||||
|
MdHourglassBottom
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import {
|
||||||
|
stepColor,
|
||||||
|
getStatusStyle,
|
||||||
|
FINAL_STATUSES,
|
||||||
|
SKIPPED_STATUSES,
|
||||||
|
ordinal
|
||||||
|
} from './dispatchShared';
|
||||||
|
|
||||||
|
// Right-side data panel rendered in Compare mode. Pure presentation +
|
||||||
|
// memoized derivations: feed it the comparison state from Dispatch and
|
||||||
|
// the panel handles its own layout (compliance score, day overview, route
|
||||||
|
// sequence with cascade grouping, KPIs, highlights, trips, focused-step
|
||||||
|
// details, deviations, full step list).
|
||||||
|
//
|
||||||
|
// Props:
|
||||||
|
// focusedRider — the rider whose day is being compared
|
||||||
|
// compareDeltas — per-step actual deltas (see useMemo
|
||||||
|
// in Dispatch.js)
|
||||||
|
// compareSummary — day rollup: actualKm/onTime/etc
|
||||||
|
// actualOrdered — compareDeltas sorted by sequenceStep (visit
|
||||||
|
// order). Used by the route-sequence section.
|
||||||
|
// focusedCompareStep — currently focused step (1..N) or null
|
||||||
|
// setFocusedCompareStep — setter; pass a function-updater for toggle
|
||||||
|
// sequenceOpen — whether the "Route sequence" section is open
|
||||||
|
// setSequenceOpen — setter for sequenceOpen
|
||||||
|
// expandedSeqGroups — Set of expanded sequence-diff group indices
|
||||||
|
// setExpandedSeqGroups — setter (Set state)
|
||||||
|
// onClose — called when the user clicks the × header btn
|
||||||
|
function CompareDataPanel({
|
||||||
|
focusedRider,
|
||||||
|
compareDeltas,
|
||||||
|
compareSummary,
|
||||||
|
actualOrdered,
|
||||||
|
focusedCompareStep,
|
||||||
|
setFocusedCompareStep,
|
||||||
|
sequenceOpen,
|
||||||
|
setSequenceOpen,
|
||||||
|
expandedSeqGroups,
|
||||||
|
setExpandedSeqGroups,
|
||||||
|
onClose
|
||||||
|
}) {
|
||||||
|
// All derivations live in a single useMemo so the cost of re-running
|
||||||
|
// them is paid only when an upstream input actually changes — not on
|
||||||
|
// every parent render (e.g. cursor moving over the map, sync toggle
|
||||||
|
// toggling, etc.). Keeping them grouped also makes the data contract
|
||||||
|
// visible at a glance.
|
||||||
|
const view = useMemo(() => {
|
||||||
|
const sum = compareSummary;
|
||||||
|
const totalSteps = sum.onTime + sum.late;
|
||||||
|
const deviations = compareDeltas.filter((d) => d.anomaly);
|
||||||
|
const delivered = compareDeltas.filter((d) =>
|
||||||
|
FINAL_STATUSES.has(String(d.orderstatus || '').toLowerCase())
|
||||||
|
).length;
|
||||||
|
const skipped = compareDeltas.filter((d) =>
|
||||||
|
SKIPPED_STATUSES.has(String(d.orderstatus || '').toLowerCase())
|
||||||
|
).length;
|
||||||
|
const stepDeltaPct =
|
||||||
|
sum.kmDeltaPct == null
|
||||||
|
? ''
|
||||||
|
: sum.kmDeltaPct > 25
|
||||||
|
? 'is-over'
|
||||||
|
: sum.kmDeltaPct < -5
|
||||||
|
? 'is-under'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Compliance score (0-100): 60% delivered + 25% on-time + 15% no-deviation.
|
||||||
|
const totalForScore = compareDeltas.length || 1;
|
||||||
|
const onTimeForScore = sum.onTime + sum.late || 1;
|
||||||
|
const score = Math.round(
|
||||||
|
(delivered / totalForScore) * 60 +
|
||||||
|
(sum.onTime / onTimeForScore) * 25 +
|
||||||
|
((totalForScore - sum.anomalies) / totalForScore) * 15
|
||||||
|
);
|
||||||
|
const scoreColor = score >= 85 ? '#16a34a' : score >= 65 ? '#f59e0b' : '#dc2626';
|
||||||
|
const scoreLabel = score >= 85 ? 'Excellent' : score >= 65 ? 'Acceptable' : 'Needs review';
|
||||||
|
|
||||||
|
// KPIs derived from delivery timestamps.
|
||||||
|
const withActual = compareDeltas.filter((d) => d.actualTs);
|
||||||
|
const firstDelivery = withActual.reduce(
|
||||||
|
(acc, d) => (!acc || d.actualTs.isBefore(acc) ? d.actualTs : acc),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const lastDelivery = withActual.reduce(
|
||||||
|
(acc, d) => (!acc || d.actualTs.isAfter(acc) ? d.actualTs : acc),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const activeMin =
|
||||||
|
firstDelivery && lastDelivery
|
||||||
|
? Math.max(0, lastDelivery.diff(firstDelivery, 'minute'))
|
||||||
|
: 0;
|
||||||
|
const avgPerStop =
|
||||||
|
compareDeltas.length > 1
|
||||||
|
? Math.round(activeMin / (compareDeltas.length - 1))
|
||||||
|
: 0;
|
||||||
|
const avgSpeed =
|
||||||
|
activeMin > 0 ? (sum.actualKm / (activeMin / 60)).toFixed(1) : null;
|
||||||
|
|
||||||
|
// Best / worst step.
|
||||||
|
const readyDeltas = compareDeltas.filter(
|
||||||
|
(d) => !d.isLoading && d.coordsCount > 0
|
||||||
|
);
|
||||||
|
const bestStep =
|
||||||
|
readyDeltas
|
||||||
|
.filter((d) => d.timeDeltaMin != null && !d.anomaly)
|
||||||
|
.sort((a, b) => a.timeDeltaMin - b.timeDeltaMin)[0] || null;
|
||||||
|
const worstStep =
|
||||||
|
readyDeltas
|
||||||
|
.filter((d) => d.anomaly)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const sa =
|
||||||
|
Math.abs(a.kmDeltaPct || 0) + (a.timeDeltaMin > 0 ? a.timeDeltaMin : 0);
|
||||||
|
const sb =
|
||||||
|
Math.abs(b.kmDeltaPct || 0) + (b.timeDeltaMin > 0 ? b.timeDeltaMin : 0);
|
||||||
|
return sb - sa;
|
||||||
|
})[0] || null;
|
||||||
|
|
||||||
|
// Route sequence — which actual-visit positions don't match the
|
||||||
|
// dispatch-planned step.
|
||||||
|
const outOfOrderSteps = actualOrdered.filter((d, i) => {
|
||||||
|
const planned = d.order?.step;
|
||||||
|
return planned != null && planned !== i + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cascade-aware grouping of out-of-order steps: consecutive entries
|
||||||
|
// with the same `delta` collapse into one "N consecutive shifted +K"
|
||||||
|
// card so a single bad first stop doesn't paint 12 noisy rows.
|
||||||
|
const seqRuns = [];
|
||||||
|
outOfOrderSteps.forEach((d) => {
|
||||||
|
const planned = d.order?.step;
|
||||||
|
const actualPos =
|
||||||
|
actualOrdered.findIndex((x) => x.sequenceStep === d.sequenceStep) + 1;
|
||||||
|
const delta = actualPos - planned;
|
||||||
|
const last = seqRuns[seqRuns.length - 1];
|
||||||
|
if (last && last.delta === delta && last.lastActualPos + 1 === actualPos) {
|
||||||
|
last.items.push({ d, planned, actualPos, delta });
|
||||||
|
last.lastActualPos = actualPos;
|
||||||
|
} else {
|
||||||
|
seqRuns.push({
|
||||||
|
delta,
|
||||||
|
items: [{ d, planned, actualPos, delta }],
|
||||||
|
lastActualPos: actualPos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trip-by-trip rollup.
|
||||||
|
const tripBuckets = {};
|
||||||
|
focusedRider.orders.forEach((o) => {
|
||||||
|
const t = o.trip_number || 1;
|
||||||
|
if (!tripBuckets[t]) tripBuckets[t] = [];
|
||||||
|
tripBuckets[t].push(o);
|
||||||
|
});
|
||||||
|
const tripList = Object.entries(tripBuckets)
|
||||||
|
.sort(([a], [b]) => Number(a) - Number(b))
|
||||||
|
.map(([tNum, tOrders]) => ({
|
||||||
|
tNum,
|
||||||
|
count: tOrders.length,
|
||||||
|
actualKm: tOrders.reduce(
|
||||||
|
(s, o) => s + parseFloat(o.actualkms || o.kms || 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
delivered: tOrders.filter((o) =>
|
||||||
|
FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
|
||||||
|
).length
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
sum,
|
||||||
|
totalSteps,
|
||||||
|
deviations,
|
||||||
|
delivered,
|
||||||
|
skipped,
|
||||||
|
stepDeltaPct,
|
||||||
|
score,
|
||||||
|
scoreColor,
|
||||||
|
scoreLabel,
|
||||||
|
firstDelivery,
|
||||||
|
lastDelivery,
|
||||||
|
activeMin,
|
||||||
|
avgPerStop,
|
||||||
|
avgSpeed,
|
||||||
|
bestStep,
|
||||||
|
worstStep,
|
||||||
|
outOfOrderSteps,
|
||||||
|
seqRuns,
|
||||||
|
tripList
|
||||||
|
};
|
||||||
|
}, [focusedRider, compareDeltas, compareSummary, actualOrdered]);
|
||||||
|
|
||||||
|
const focused =
|
||||||
|
focusedCompareStep != null
|
||||||
|
? compareDeltas.find((d) => d.sequenceStep === focusedCompareStep)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const toggleSeqGroup = (idx) => {
|
||||||
|
setExpandedSeqGroups((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(idx)) next.delete(idx);
|
||||||
|
else next.add(idx);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusStep = (sequenceStep) => {
|
||||||
|
setFocusedCompareStep((prev) => (prev === sequenceStep ? null : sequenceStep));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Renders a single shifted-step diff card (used both stand-alone and
|
||||||
|
// nested under an expanded group).
|
||||||
|
const renderDiffRow = (item, focusable = true) => {
|
||||||
|
const { d, planned, actualPos, delta } = item;
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`diff-${d.sequenceStep}`}
|
||||||
|
className={`cdp-seq-diff${
|
||||||
|
focusedCompareStep === d.sequenceStep ? ' is-focused' : ''
|
||||||
|
}${focusable ? '' : ' is-nested'}`}
|
||||||
|
onClick={() => focusStep(d.sequenceStep)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="cdp-seq-diff-num"
|
||||||
|
style={{ background: stepColor((planned || d.sequenceStep) - 1) }}
|
||||||
|
>
|
||||||
|
{planned || d.sequenceStep}
|
||||||
|
</span>
|
||||||
|
<div className="cdp-seq-diff-body">
|
||||||
|
<div className="cdp-seq-diff-title">
|
||||||
|
{d.deliverycustomer || `Step ${planned || d.sequenceStep}`}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-seq-diff-sub">
|
||||||
|
Visited <strong>{ordinal(actualPos)}</strong>{' '}
|
||||||
|
· planned <strong>{ordinal(planned)}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="cdp-seq-diff-tag">
|
||||||
|
{delta > 0 ? `+${delta}` : `${delta}`}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
sum,
|
||||||
|
totalSteps,
|
||||||
|
deviations,
|
||||||
|
delivered,
|
||||||
|
skipped,
|
||||||
|
stepDeltaPct,
|
||||||
|
score,
|
||||||
|
scoreColor,
|
||||||
|
scoreLabel,
|
||||||
|
firstDelivery,
|
||||||
|
lastDelivery,
|
||||||
|
activeMin,
|
||||||
|
avgPerStop,
|
||||||
|
avgSpeed,
|
||||||
|
bestStep,
|
||||||
|
worstStep,
|
||||||
|
outOfOrderSteps,
|
||||||
|
seqRuns,
|
||||||
|
tripList
|
||||||
|
} = view;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside id="compare-data-panel" className="compare-data-panel">
|
||||||
|
<div className="cdp-head">
|
||||||
|
<div className="cdp-head-title">
|
||||||
|
<span
|
||||||
|
className="cdp-rider-dot"
|
||||||
|
style={{ background: focusedRider.color }}
|
||||||
|
/>
|
||||||
|
<div className="cdp-head-text">
|
||||||
|
<div className="cdp-rider-name">{focusedRider.riderName}</div>
|
||||||
|
<div className="cdp-head-badge">PLANNED vs ACTUAL</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="cdp-close"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Exit compare"
|
||||||
|
aria-label="Exit compare"
|
||||||
|
>
|
||||||
|
<MdClose />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="cdp-scroll">
|
||||||
|
{/* Compliance score — headline gauge blending delivery, on-time,
|
||||||
|
and route-fidelity into one number. */}
|
||||||
|
<section className="cdp-section cdp-score-section">
|
||||||
|
<div className="cdp-score-wrap">
|
||||||
|
<div
|
||||||
|
className="cdp-score-ring"
|
||||||
|
style={{
|
||||||
|
background: `conic-gradient(${scoreColor} ${score * 3.6}deg, rgba(15,23,42,0.08) 0deg)`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="cdp-score-inner">
|
||||||
|
<div className="cdp-score-value" style={{ color: scoreColor }}>
|
||||||
|
{score}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-score-unit">/100</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-score-body">
|
||||||
|
<div className="cdp-score-label" style={{ color: scoreColor }}>
|
||||||
|
{scoreLabel}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-score-title">Compliance score</div>
|
||||||
|
<div className="cdp-score-sub">
|
||||||
|
{delivered}/{compareDeltas.length} delivered
|
||||||
|
{sum.anomalies > 0
|
||||||
|
? ` · ${sum.anomalies} deviation${sum.anomalies > 1 ? 's' : ''}`
|
||||||
|
: ''}
|
||||||
|
{sum.late > 0 ? ` · ${sum.late} late` : ''}
|
||||||
|
{skipped > 0 ? ` · ${skipped} skipped` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon"><MdPublic /></span>
|
||||||
|
<span className="cdp-section-title">Day overview</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tiles">
|
||||||
|
<div className="cdp-tile">
|
||||||
|
<div className="cdp-tile-label">
|
||||||
|
<MdStraighten /> Distance
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tile-value">
|
||||||
|
{sum.actualKm.toFixed(1)}
|
||||||
|
<span className="cdp-tile-unit">km</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tile-sub">actual</div>
|
||||||
|
</div>
|
||||||
|
<div className={`cdp-tile${sum.anomalies > 0 ? ' is-warn' : ''}`}>
|
||||||
|
<div className="cdp-tile-label">
|
||||||
|
<MdWarning /> Deviation
|
||||||
|
</div>
|
||||||
|
<div className={`cdp-tile-value ${stepDeltaPct}`}>
|
||||||
|
{sum.kmDeltaPct != null
|
||||||
|
? `${sum.kmDeltaPct > 0 ? '+' : ''}${sum.kmDeltaPct.toFixed(0)}%`
|
||||||
|
: '—'}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tile-sub">
|
||||||
|
{sum.anomalies > 0 ? `${sum.anomalies} flagged` : 'within plan'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`cdp-tile${sum.late > 0 ? ' is-warn' : ''}`}>
|
||||||
|
<div className="cdp-tile-label">
|
||||||
|
<MdAccessTime /> On-time
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tile-value">
|
||||||
|
{sum.onTime}
|
||||||
|
{totalSteps > 0 && (
|
||||||
|
<span className="cdp-tile-unit">/{totalSteps}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-tile-sub">
|
||||||
|
{sum.late > 0 ? `${sum.late} late` : 'all on schedule'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Route sequence — collapsible, default open. Shows planned vs
|
||||||
|
actual visit order with cascade-aware diff grouping. */}
|
||||||
|
{compareDeltas.length > 0 && (
|
||||||
|
<section className="cdp-section cdp-seq-section">
|
||||||
|
<div
|
||||||
|
className="cdp-section-head cdp-section-head-clickable"
|
||||||
|
onClick={() => setSequenceOpen((v) => !v)}
|
||||||
|
role="button"
|
||||||
|
aria-expanded={sequenceOpen}
|
||||||
|
title={sequenceOpen ? 'Collapse route sequence' : 'Expand route sequence'}
|
||||||
|
>
|
||||||
|
<span className="cdp-section-icon">
|
||||||
|
<MdSwapHoriz />
|
||||||
|
</span>
|
||||||
|
<span className="cdp-section-title">Route sequence</span>
|
||||||
|
<span
|
||||||
|
className={`cdp-seq-status${outOfOrderSteps.length > 0 ? ' is-warn' : ' is-good'}`}
|
||||||
|
>
|
||||||
|
{outOfOrderSteps.length > 0
|
||||||
|
? `${outOfOrderSteps.length} out of order`
|
||||||
|
: 'In order'}
|
||||||
|
</span>
|
||||||
|
<span className={`cdp-seq-toggle${sequenceOpen ? ' is-open' : ''}`}>
|
||||||
|
<MdExpandMore />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{sequenceOpen && (
|
||||||
|
<div className="cdp-seq">
|
||||||
|
{outOfOrderSteps.length > 0 ? (
|
||||||
|
<ul className="cdp-seq-diffs">
|
||||||
|
{seqRuns.map((run, runIdx) => {
|
||||||
|
if (run.items.length === 1) {
|
||||||
|
return renderDiffRow(run.items[0]);
|
||||||
|
}
|
||||||
|
const first = run.items[0];
|
||||||
|
const last = run.items[run.items.length - 1];
|
||||||
|
const isOpen = expandedSeqGroups.has(runIdx);
|
||||||
|
const deltaStr =
|
||||||
|
run.delta > 0 ? `+${run.delta}` : `${run.delta}`;
|
||||||
|
const groupFocused = run.items.some(
|
||||||
|
(it) => it.d.sequenceStep === focusedCompareStep
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<React.Fragment key={`run-${runIdx}-${first.d.sequenceStep}`}>
|
||||||
|
<li
|
||||||
|
className={`cdp-seq-diff is-group${isOpen ? ' is-expanded' : ''}${groupFocused ? ' is-focused' : ''}`}
|
||||||
|
onClick={() => toggleSeqGroup(runIdx)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
>
|
||||||
|
<span className="cdp-seq-group-num">
|
||||||
|
<span
|
||||||
|
className="cdp-seq-group-num-bg"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(135deg, ${stepColor((first.planned || 1) - 1)}, ${stepColor((last.planned || 1) - 1)})`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="cdp-seq-group-num-label">
|
||||||
|
{run.items.length}×
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div className="cdp-seq-diff-body">
|
||||||
|
<div className="cdp-seq-diff-title">
|
||||||
|
{run.items.length} consecutive steps shifted{' '}
|
||||||
|
<span className="cdp-seq-group-delta">{deltaStr}</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-seq-diff-sub">
|
||||||
|
Planned {ordinal(first.planned)}–{ordinal(last.planned)}{' '}
|
||||||
|
visited{' '}
|
||||||
|
<strong>
|
||||||
|
{ordinal(first.actualPos)}–{ordinal(last.actualPos)}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="cdp-seq-diff-tag">{deltaStr}</span>
|
||||||
|
<span
|
||||||
|
className={`cdp-seq-group-toggle${isOpen ? ' is-open' : ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<MdExpandMore />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{isOpen && (
|
||||||
|
<li className="cdp-seq-group-children-wrap">
|
||||||
|
<ul className="cdp-seq-group-children">
|
||||||
|
{run.items.map((it) => renderDiffRow(it, false))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="cdp-seq-good">
|
||||||
|
<MdCheckCircle /> Rider followed the planned route in order.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timing — clock-style timeline. First/last delivery render as
|
||||||
|
digital clock faces flanking a duration centerpiece. Tiny
|
||||||
|
"Started" / "Finished" captions give the row a narrative.
|
||||||
|
Below: avg-per-stop with a dotted stops-row visualization,
|
||||||
|
and avg speed with a 0-60 gauge bar. */}
|
||||||
|
{(firstDelivery || lastDelivery) && (
|
||||||
|
<section className="cdp-section cdp-timing-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon"><MdTimer /></span>
|
||||||
|
<span className="cdp-section-title">Timing</span>
|
||||||
|
{activeMin > 0 && (
|
||||||
|
<span className="cdp-timing-active-tag">
|
||||||
|
<span className="cdp-timing-active-pulse" />
|
||||||
|
Day window
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-clock">
|
||||||
|
<div className="cdp-clock-card is-start">
|
||||||
|
<div className="cdp-clock-label">
|
||||||
|
<MdFlag /> First delivery
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-face">
|
||||||
|
<span className="cdp-clock-time">
|
||||||
|
{firstDelivery ? firstDelivery.format('hh:mm') : '—'}
|
||||||
|
</span>
|
||||||
|
<span className="cdp-clock-period">
|
||||||
|
{firstDelivery ? firstDelivery.format('A') : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-caption">Started</div>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-track" aria-hidden="true">
|
||||||
|
<span className="cdp-clock-track-line" />
|
||||||
|
<span className="cdp-clock-track-dot is-start" />
|
||||||
|
<span className="cdp-clock-track-dot is-end" />
|
||||||
|
<div className="cdp-clock-duration">
|
||||||
|
<span className="cdp-clock-duration-icon">
|
||||||
|
<MdHourglassBottom />
|
||||||
|
</span>
|
||||||
|
<span className="cdp-clock-duration-val">
|
||||||
|
{activeMin > 0
|
||||||
|
? activeMin >= 60
|
||||||
|
? `${Math.floor(activeMin / 60)}h ${activeMin % 60}m`
|
||||||
|
: `${activeMin}m`
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
<span className="cdp-clock-duration-sub">active</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-card is-end">
|
||||||
|
<div className="cdp-clock-label">
|
||||||
|
<MdCheckCircle /> Last delivery
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-face">
|
||||||
|
<span className="cdp-clock-time">
|
||||||
|
{lastDelivery ? lastDelivery.format('hh:mm') : '—'}
|
||||||
|
</span>
|
||||||
|
<span className="cdp-clock-period">
|
||||||
|
{lastDelivery ? lastDelivery.format('A') : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-clock-caption">Finished</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stats">
|
||||||
|
<div className="cdp-timing-stat">
|
||||||
|
<div className="cdp-timing-stat-head">
|
||||||
|
<div className="cdp-timing-stat-icon">
|
||||||
|
<MdAccessTime />
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stat-body">
|
||||||
|
<div className="cdp-timing-stat-value">
|
||||||
|
{avgPerStop > 0 ? `${avgPerStop}` : '—'}
|
||||||
|
{avgPerStop > 0 && (
|
||||||
|
<span className="cdp-timing-stat-unit">min</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stat-label">Avg / stop</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{compareDeltas.length > 0 && (
|
||||||
|
<div className="cdp-timing-stat-viz cdp-stops-dots" aria-hidden="true">
|
||||||
|
{Array.from({ length: Math.min(compareDeltas.length, 12) }).map((_, i) => (
|
||||||
|
<span key={`dot-${i}`} className="cdp-stop-dot" />
|
||||||
|
))}
|
||||||
|
<span className="cdp-timing-stat-viz-label">
|
||||||
|
{compareDeltas.length} stop{compareDeltas.length === 1 ? '' : 's'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{avgSpeed != null && (
|
||||||
|
<div className="cdp-timing-stat">
|
||||||
|
<div className="cdp-timing-stat-head">
|
||||||
|
<div className="cdp-timing-stat-icon">
|
||||||
|
<MdSpeed />
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stat-body">
|
||||||
|
<div className="cdp-timing-stat-value">
|
||||||
|
{avgSpeed}
|
||||||
|
<span className="cdp-timing-stat-unit">km/h</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stat-label">Avg speed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-timing-stat-viz cdp-speed-gauge" aria-hidden="true">
|
||||||
|
<div className="cdp-speed-gauge-track">
|
||||||
|
<div
|
||||||
|
className="cdp-speed-gauge-fill"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(100, (parseFloat(avgSpeed) / 60) * 100)}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-speed-gauge-scale">
|
||||||
|
<span>0</span>
|
||||||
|
<span>30</span>
|
||||||
|
<span>60 km/h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Highlights — best/worst step quick-pick. Full-width cards stacked
|
||||||
|
vertically: a colored rail on the left side encodes good/bad,
|
||||||
|
the customer name is the headline, the step number sits as a
|
||||||
|
right-aligned chip, and the metric line uses bold pills. */}
|
||||||
|
{(bestStep || worstStep) && (
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon"><MdStar /></span>
|
||||||
|
<span className="cdp-section-title">Highlights</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-highlights">
|
||||||
|
{bestStep && (
|
||||||
|
<div
|
||||||
|
className="cdp-highlight is-best"
|
||||||
|
onClick={() => focusStep(bestStep.sequenceStep)}
|
||||||
|
role="button"
|
||||||
|
title="Focus this step"
|
||||||
|
>
|
||||||
|
<span className="cdp-highlight-rail" aria-hidden="true" />
|
||||||
|
<div className="cdp-highlight-content">
|
||||||
|
<div className="cdp-highlight-top">
|
||||||
|
<span className="cdp-highlight-label">
|
||||||
|
<span className="cdp-highlight-chip">
|
||||||
|
<MdCheckCircle />
|
||||||
|
</span>
|
||||||
|
Fastest stop
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cdp-highlight-step-chip"
|
||||||
|
style={{
|
||||||
|
background: stepColor(bestStep.sequenceStep - 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Step {bestStep.sequenceStep}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-highlight-title">
|
||||||
|
{bestStep.deliverycustomer || `Step ${bestStep.sequenceStep}`}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-highlight-meta">
|
||||||
|
<span className="cdp-highlight-pill is-good">
|
||||||
|
{bestStep.timeDeltaMin != null
|
||||||
|
? bestStep.timeDeltaMin === 0
|
||||||
|
? 'On schedule'
|
||||||
|
: `${bestStep.timeDeltaMin > 0 ? '+' : ''}${bestStep.timeDeltaMin} min vs plan`
|
||||||
|
: 'On schedule'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{worstStep && (
|
||||||
|
<div
|
||||||
|
className="cdp-highlight is-worst"
|
||||||
|
onClick={() => focusStep(worstStep.sequenceStep)}
|
||||||
|
role="button"
|
||||||
|
title="Focus this step"
|
||||||
|
>
|
||||||
|
<span className="cdp-highlight-rail" aria-hidden="true" />
|
||||||
|
<div className="cdp-highlight-content">
|
||||||
|
<div className="cdp-highlight-top">
|
||||||
|
<span className="cdp-highlight-label">
|
||||||
|
<span className="cdp-highlight-chip">
|
||||||
|
<MdWarning />
|
||||||
|
</span>
|
||||||
|
Biggest deviation
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="cdp-highlight-step-chip"
|
||||||
|
style={{
|
||||||
|
background: stepColor(worstStep.sequenceStep - 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Step {worstStep.sequenceStep}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-highlight-title">
|
||||||
|
{worstStep.deliverycustomer || `Step ${worstStep.sequenceStep}`}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-highlight-meta">
|
||||||
|
{worstStep.kmDeltaPct != null && (
|
||||||
|
<span className="cdp-highlight-pill is-bad">
|
||||||
|
{worstStep.kmDeltaPct > 0 ? '+' : ''}
|
||||||
|
{worstStep.kmDeltaPct.toFixed(0)}% route
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{worstStep.timeDeltaMin != null && worstStep.timeDeltaMin > 0 && (
|
||||||
|
<span className="cdp-highlight-pill is-bad">
|
||||||
|
+{worstStep.timeDeltaMin}m late
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Trip breakdown — only when rider ran >1 trip. */}
|
||||||
|
{tripList.length > 1 && (
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon"><MdSwapHoriz /></span>
|
||||||
|
<span className="cdp-section-title">Trips ({tripList.length})</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-trips">
|
||||||
|
{tripList.map((t) => (
|
||||||
|
<div key={`trip-${t.tNum}`} className="cdp-trip">
|
||||||
|
<div className="cdp-trip-head">
|
||||||
|
<span className="cdp-trip-badge">Trip {t.tNum}</span>
|
||||||
|
<span className="cdp-trip-meta">
|
||||||
|
{t.delivered}/{t.count} delivered
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="cdp-trip-stats">
|
||||||
|
<span title="Distance">
|
||||||
|
<MdStraighten />
|
||||||
|
{t.actualKm.toFixed(1)}km
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Focused-step deep-dive — appears only when a step is selected. */}
|
||||||
|
{focused && (() => {
|
||||||
|
const color = stepColor(focused.sequenceStep - 1);
|
||||||
|
const timeDeltaCls =
|
||||||
|
focused.timeDeltaMin != null
|
||||||
|
? focused.timeDeltaMin > 10
|
||||||
|
? 'is-over'
|
||||||
|
: focused.timeDeltaMin < -2
|
||||||
|
? 'is-under'
|
||||||
|
: ''
|
||||||
|
: '';
|
||||||
|
const statusStyle = getStatusStyle(focused.orderstatus);
|
||||||
|
return (
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon"><MdSwapHoriz /></span>
|
||||||
|
<span className="cdp-section-title">
|
||||||
|
Step {focused.sequenceStep} details
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="cdp-section-clear"
|
||||||
|
onClick={() => setFocusedCompareStep(null)}
|
||||||
|
title="Clear step focus"
|
||||||
|
>
|
||||||
|
Show all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className={`compare-delta${focused.anomaly ? ' is-anomaly' : ''}`}>
|
||||||
|
<div className="compare-delta-title">
|
||||||
|
<span
|
||||||
|
className="compare-delta-step-badge"
|
||||||
|
style={{ background: color }}
|
||||||
|
>
|
||||||
|
{focused.sequenceStep}
|
||||||
|
</span>
|
||||||
|
<div className="compare-delta-title-text">
|
||||||
|
<div className="compare-delta-title-main">
|
||||||
|
{focused.deliverycustomer || `Step ${focused.sequenceStep}`}
|
||||||
|
</div>
|
||||||
|
<div className="compare-delta-title-sub">
|
||||||
|
{focused.pickupcustomer ? `from ${focused.pickupcustomer} · ` : ''}
|
||||||
|
Order #{focused.orderid}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{focused.orderstatus && (
|
||||||
|
<span
|
||||||
|
className="compare-delta-status"
|
||||||
|
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||||
|
>
|
||||||
|
{statusStyle.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="compare-delta-grid">
|
||||||
|
<div className={`compare-delta-cell${focused.anomaly ? ' is-anomaly' : ''}`}>
|
||||||
|
<span className="compare-delta-cell-label">Distance</span>
|
||||||
|
<span className="compare-delta-cell-val">
|
||||||
|
{focused.actualKm.toFixed(2)}{' '}
|
||||||
|
<span className="compare-delta-cell-unit">km</span>
|
||||||
|
</span>
|
||||||
|
<span className="compare-delta-cell-sub">actual</span>
|
||||||
|
</div>
|
||||||
|
<div className="compare-delta-cell">
|
||||||
|
<span className="compare-delta-cell-label">Time</span>
|
||||||
|
<span className={`compare-delta-cell-val ${timeDeltaCls}`}>
|
||||||
|
{focused.timeDeltaMin != null
|
||||||
|
? `${focused.timeDeltaMin > 0 ? '+' : ''}${focused.timeDeltaMin} min`
|
||||||
|
: '—'}
|
||||||
|
</span>
|
||||||
|
<span className="compare-delta-cell-sub">
|
||||||
|
{focused.actualTs && focused.expectedTs
|
||||||
|
? `${focused.actualTs.format('HH:mm')} vs ${focused.expectedTs.format('HH:mm')}`
|
||||||
|
: focused.actualTs
|
||||||
|
? `delivered ${focused.actualTs.format('HH:mm')}`
|
||||||
|
: 'in flight'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{/* Deviations list — anomaly-only steps. */}
|
||||||
|
{deviations.length > 0 && (
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon cdp-icon-warn">
|
||||||
|
<MdErrorOutline />
|
||||||
|
</span>
|
||||||
|
<span className="cdp-section-title">
|
||||||
|
Deviations ({deviations.length})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="cdp-dev-list">
|
||||||
|
{deviations.map((d) => {
|
||||||
|
const color = stepColor(d.sequenceStep - 1);
|
||||||
|
const kmSign = d.kmDelta >= 0 ? '+' : '';
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`dev-${d.sequenceStep}`}
|
||||||
|
className={`cdp-dev-item${focusedCompareStep === d.sequenceStep ? ' is-focused' : ''}`}
|
||||||
|
onClick={() => focusStep(d.sequenceStep)}
|
||||||
|
>
|
||||||
|
<span className="cdp-dev-num" style={{ background: color }}>
|
||||||
|
{d.sequenceStep}
|
||||||
|
</span>
|
||||||
|
<div className="cdp-dev-body">
|
||||||
|
<div className="cdp-dev-title">
|
||||||
|
{d.deliverycustomer || `Step ${d.sequenceStep}`}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-dev-meta">
|
||||||
|
{d.kmDeltaPct != null && (
|
||||||
|
<span className="cdp-dev-chip is-over">
|
||||||
|
{kmSign}{d.kmDeltaPct.toFixed(0)}% route
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{d.timeDeltaMin != null && d.timeDeltaMin > 10 && (
|
||||||
|
<span className="cdp-dev-chip is-over">
|
||||||
|
+{d.timeDeltaMin}m late
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Full step list. */}
|
||||||
|
<section className="cdp-section">
|
||||||
|
<div className="cdp-section-head">
|
||||||
|
<span className="cdp-section-icon">
|
||||||
|
<MdFormatListBulleted />
|
||||||
|
</span>
|
||||||
|
<span className="cdp-section-title">
|
||||||
|
Steps ({compareDeltas.length})
|
||||||
|
</span>
|
||||||
|
<span className="cdp-section-sub">
|
||||||
|
{delivered}/{compareDeltas.length} delivered
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ul className="cdp-step-list">
|
||||||
|
{compareDeltas.map((d) => {
|
||||||
|
const color = stepColor(d.sequenceStep - 1);
|
||||||
|
const statusLow = String(d.orderstatus || '').toLowerCase();
|
||||||
|
const isDelivered = FINAL_STATUSES.has(statusLow);
|
||||||
|
const isSkipped = SKIPPED_STATUSES.has(statusLow);
|
||||||
|
const isCorrect = isDelivered && !d.anomaly;
|
||||||
|
const isFocused = focusedCompareStep === d.sequenceStep;
|
||||||
|
const statusStyle = getStatusStyle(d.orderstatus);
|
||||||
|
const timeCls =
|
||||||
|
d.timeDeltaMin != null
|
||||||
|
? d.timeDeltaMin > 10
|
||||||
|
? 'is-over'
|
||||||
|
: d.timeDeltaMin < -2
|
||||||
|
? 'is-under'
|
||||||
|
: ''
|
||||||
|
: '';
|
||||||
|
const stepCls = [
|
||||||
|
'cdp-step',
|
||||||
|
isFocused ? 'is-focused' : '',
|
||||||
|
d.anomaly ? 'is-anomaly' : '',
|
||||||
|
isCorrect ? 'is-correct' : '',
|
||||||
|
isSkipped ? 'is-skipped' : '',
|
||||||
|
d.isLoading ? 'is-loading' : ''
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`step-${d.sequenceStep}`}
|
||||||
|
className={stepCls}
|
||||||
|
onClick={() => focusStep(d.sequenceStep)}
|
||||||
|
>
|
||||||
|
<span className="cdp-step-num" style={{ background: color }}>
|
||||||
|
{d.sequenceStep}
|
||||||
|
{isCorrect && (
|
||||||
|
<span className="cdp-step-check">
|
||||||
|
<MdCheckCircle />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{d.anomaly && (
|
||||||
|
<span className="cdp-step-flag">
|
||||||
|
<MdErrorOutline />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="cdp-step-body">
|
||||||
|
<div className="cdp-step-title-row">
|
||||||
|
<span className="cdp-step-title">
|
||||||
|
{d.deliverycustomer || `Step ${d.sequenceStep}`}
|
||||||
|
</span>
|
||||||
|
{d.orderstatus && (
|
||||||
|
<span
|
||||||
|
className="cdp-step-status"
|
||||||
|
style={{ background: statusStyle.bg, color: statusStyle.fg }}
|
||||||
|
>
|
||||||
|
{statusStyle.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-step-sub">
|
||||||
|
{d.pickupcustomer ? `from ${d.pickupcustomer} · ` : ''}
|
||||||
|
Order #{d.orderid}
|
||||||
|
</div>
|
||||||
|
<div className="cdp-step-deltas">
|
||||||
|
<span className="cdp-step-delta" title="Distance">
|
||||||
|
<MdStraighten />
|
||||||
|
{d.actualKm.toFixed(1)}km
|
||||||
|
</span>
|
||||||
|
<span className={`cdp-step-delta ${timeCls}`} title="Delivery time">
|
||||||
|
<MdAccessTime />
|
||||||
|
{d.actualTs ? d.actualTs.format('HH:mm') : '—'}
|
||||||
|
{d.timeDeltaMin != null && (
|
||||||
|
<small>
|
||||||
|
{' '}{d.timeDeltaMin > 0 ? '+' : ''}{d.timeDeltaMin}m
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CompareDataPanel;
|
||||||
10297
src/pages/nearle/dispatch/Dispatch.css
Normal file
10297
src/pages/nearle/dispatch/Dispatch.css
Normal file
File diff suppressed because it is too large
Load Diff
6010
src/pages/nearle/dispatch/Dispatch.js
Normal file
6010
src/pages/nearle/dispatch/Dispatch.js
Normal file
File diff suppressed because it is too large
Load Diff
806
src/pages/nearle/dispatch/Preview.js
Normal file
806
src/pages/nearle/dispatch/Preview.js
Normal file
@@ -0,0 +1,806 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Autocomplete,
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Chip,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
Tabs,
|
||||||
|
TextField,
|
||||||
|
Tooltip,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||||
|
import { HiOutlineArrowLeft } from 'react-icons/hi';
|
||||||
|
import { IoReload } from 'react-icons/io5';
|
||||||
|
import { MdTwoWheeler, MdSwapHoriz } from 'react-icons/md';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createAutomationDeliveries,
|
||||||
|
createOptimisationDeliveries,
|
||||||
|
fetchRidersList,
|
||||||
|
finalCreatedeliveries,
|
||||||
|
notifyRider,
|
||||||
|
reconcileSteps
|
||||||
|
} from '../api/api';
|
||||||
|
import { OpenToast } from 'components/nearle_components/OpenToast';
|
||||||
|
import CSVExport from 'components/third-party/ReactTable';
|
||||||
|
import CircularLoader from 'components/nearle_components/CircularLoader';
|
||||||
|
import Dispatch from './Dispatch';
|
||||||
|
import { stepColor } from './dispatchShared';
|
||||||
|
|
||||||
|
const tuningTypes = [
|
||||||
|
{ tuneid: 1, type: 'Balanced', value: 'balanced' },
|
||||||
|
{ tuneid: 2, type: 'Aggressive Speed', value: 'aggressive_speed' },
|
||||||
|
{ tuneid: 3, type: 'Fuel Saver', value: 'fuel_saver' },
|
||||||
|
{ tuneid: 4, type: 'Zone Strict', value: 'zone_strict' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for
|
||||||
|
// the Reconcile tab UI and the reconcile-API payload.
|
||||||
|
const extractRiders = (previewData) => {
|
||||||
|
if (!previewData) return [];
|
||||||
|
const map = new Map();
|
||||||
|
// De-dupe by orderid across the whole tree. A rider can legitimately appear
|
||||||
|
// in multiple zones (one per delivery suburb), so the same rider_id is
|
||||||
|
// visited more than once. Without this guard, any stale copy left behind
|
||||||
|
// by applyReconcileResponse gets concatenated into the rider's orders and
|
||||||
|
// the same orderid is sent twice to /deliveries/createdeliveries.
|
||||||
|
const seenOrderIds = new Set();
|
||||||
|
const push = (riderId, riderName, orders) => {
|
||||||
|
if (riderId == null) return;
|
||||||
|
const key = String(riderId);
|
||||||
|
if (!map.has(key)) {
|
||||||
|
map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] });
|
||||||
|
}
|
||||||
|
const entry = map.get(key);
|
||||||
|
(orders || []).forEach((o) => {
|
||||||
|
const oid = o?.orderid != null ? String(o.orderid) : null;
|
||||||
|
if (oid) {
|
||||||
|
if (seenOrderIds.has(oid)) return;
|
||||||
|
seenOrderIds.add(oid);
|
||||||
|
}
|
||||||
|
entry.orders.push(o);
|
||||||
|
});
|
||||||
|
if (!entry.rider_name && riderName) entry.rider_name = riderName;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(previewData.zones) && previewData.zones.length) {
|
||||||
|
previewData.zones.forEach((z) => {
|
||||||
|
(z.riders || []).forEach((r) => {
|
||||||
|
const id = r.rider_id ?? r.userid;
|
||||||
|
const name = r.rider_name || r.username || `Rider ${id}`;
|
||||||
|
push(id, name, r.orders);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (Array.isArray(previewData.details)) {
|
||||||
|
previewData.details.forEach((o) => {
|
||||||
|
const id = o.rider_id ?? o.userid;
|
||||||
|
const name = o.rider_name || o.ridername || `Rider ${id}`;
|
||||||
|
push(id, name, [o]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Array.from(map.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reverse of extractRiders — flatten rider-grouped list into a details-style
|
||||||
|
// array (used as the Assign Orders payload).
|
||||||
|
const flattenRiders = (riders) => {
|
||||||
|
const out = [];
|
||||||
|
riders.forEach((r) => {
|
||||||
|
// Go backend types Deliveries.userid as int — coerce here so any
|
||||||
|
// upstream string (AI response, riders API, change-rider edit) gets
|
||||||
|
// normalised before the JSON body is built.
|
||||||
|
const ridNum = Number(r.rider_id);
|
||||||
|
const rid = Number.isFinite(ridNum) ? ridNum : r.rider_id;
|
||||||
|
(r.orders || []).forEach((o) => {
|
||||||
|
out.push({
|
||||||
|
...o,
|
||||||
|
rider_id: rid,
|
||||||
|
userid: rid,
|
||||||
|
rider_name: r.rider_name,
|
||||||
|
rider: r.rider_name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move one order from oldRiderId -> newRiderId inside dispatchPreviewData.
|
||||||
|
// Mutates both the zones[].riders[].orders[] tree (so the Dispatch tab
|
||||||
|
// renders the change) AND the flat details[] list (so Assign Orders picks
|
||||||
|
// it up). Returns a NEW preview object (immutable update).
|
||||||
|
const moveOrderInPreviewData = (preview, { orderId, newRiderId, newRiderName }) => {
|
||||||
|
if (!preview) return preview;
|
||||||
|
const next = JSON.parse(JSON.stringify(preview));
|
||||||
|
|
||||||
|
// 1) Update flat details list
|
||||||
|
if (Array.isArray(next.details)) {
|
||||||
|
next.details = next.details.map((o) =>
|
||||||
|
String(o.orderid) === String(orderId)
|
||||||
|
? { ...o, rider_id: newRiderId, userid: newRiderId, rider_name: newRiderName, rider: newRiderName }
|
||||||
|
: o
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Move within zones[].riders[].orders[]
|
||||||
|
if (Array.isArray(next.zones)) {
|
||||||
|
let movedOrder = null;
|
||||||
|
let homeZoneIdx = -1;
|
||||||
|
|
||||||
|
for (let zi = 0; zi < next.zones.length && !movedOrder; zi++) {
|
||||||
|
const zone = next.zones[zi];
|
||||||
|
if (!Array.isArray(zone.riders)) continue;
|
||||||
|
for (let ri = 0; ri < zone.riders.length && !movedOrder; ri++) {
|
||||||
|
const r = zone.riders[ri];
|
||||||
|
if (!Array.isArray(r.orders)) continue;
|
||||||
|
const oi = r.orders.findIndex((o) => String(o.orderid) === String(orderId));
|
||||||
|
if (oi !== -1) {
|
||||||
|
movedOrder = r.orders[oi];
|
||||||
|
r.orders.splice(oi, 1);
|
||||||
|
homeZoneIdx = zi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movedOrder) {
|
||||||
|
const updated = {
|
||||||
|
...movedOrder,
|
||||||
|
rider_id: newRiderId,
|
||||||
|
userid: newRiderId,
|
||||||
|
rider_name: newRiderName,
|
||||||
|
rider: newRiderName
|
||||||
|
};
|
||||||
|
let placed = false;
|
||||||
|
for (const zone of next.zones) {
|
||||||
|
if (!Array.isArray(zone.riders)) continue;
|
||||||
|
const target = zone.riders.find(
|
||||||
|
(r) => String(r.rider_id ?? r.userid) === String(newRiderId)
|
||||||
|
);
|
||||||
|
if (target) {
|
||||||
|
target.orders = target.orders || [];
|
||||||
|
target.orders.push(updated);
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!placed && homeZoneIdx >= 0) {
|
||||||
|
next.zones[homeZoneIdx].riders.push({
|
||||||
|
rider_id: newRiderId,
|
||||||
|
userid: newRiderId,
|
||||||
|
rider_name: newRiderName,
|
||||||
|
orders: [updated]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
|
||||||
|
// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
|
||||||
|
// zone containment), then rebuilds the flat details list from the new tree.
|
||||||
|
const applyReconcileResponse = (preview, response) => {
|
||||||
|
if (!preview || !Array.isArray(response?.riders)) return preview;
|
||||||
|
const next = JSON.parse(JSON.stringify(preview));
|
||||||
|
|
||||||
|
const newOrdersByRider = new Map(
|
||||||
|
response.riders.map((r) => [String(r.rider_id), r.orders || []])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Array.isArray(next.zones) && next.zones.length) {
|
||||||
|
// Pass 1: wipe every existing copy of a responding rider's orders across
|
||||||
|
// ALL zones. The server's reconciled list is the single source of truth,
|
||||||
|
// and a rider can be present in multiple zones (one per delivery suburb).
|
||||||
|
// The previous "update first match, delete from map" loop left stale
|
||||||
|
// copies in the other zones, which extractRiders then concatenated into
|
||||||
|
// duplicate orderids — surfacing as duplicate deliveries on Assign.
|
||||||
|
next.zones.forEach((zone) => {
|
||||||
|
if (!Array.isArray(zone.riders)) return;
|
||||||
|
zone.riders.forEach((r) => {
|
||||||
|
const key = String(r.rider_id ?? r.userid);
|
||||||
|
if (newOrdersByRider.has(key)) r.orders = [];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pass 2: drop the reconciled orders onto the first zone that already
|
||||||
|
// lists the rider. If the rider isn't anywhere in the tree, append a
|
||||||
|
// fresh rider entry to zone[0].
|
||||||
|
newOrdersByRider.forEach((orders, riderKey) => {
|
||||||
|
let placed = false;
|
||||||
|
for (const zone of next.zones) {
|
||||||
|
if (!Array.isArray(zone.riders)) continue;
|
||||||
|
const target = zone.riders.find(
|
||||||
|
(r) => String(r.rider_id ?? r.userid) === riderKey
|
||||||
|
);
|
||||||
|
if (target) {
|
||||||
|
target.orders = orders;
|
||||||
|
placed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!placed) {
|
||||||
|
const target = next.zones[0];
|
||||||
|
target.riders = target.riders || [];
|
||||||
|
target.riders.push({
|
||||||
|
rider_id: Number(riderKey) || riderKey,
|
||||||
|
rider_name: orders[0]?.rider_name || `Rider ${riderKey}`,
|
||||||
|
orders
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next.zones = [
|
||||||
|
{
|
||||||
|
zone_name: 'Reconciled',
|
||||||
|
riders: response.riders.map((r) => ({
|
||||||
|
rider_id: r.rider_id,
|
||||||
|
rider_name: r.rider_name || `Rider ${r.rider_id}`,
|
||||||
|
orders: r.orders || []
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild flat details from the updated zones->riders->orders tree.
|
||||||
|
const flatDetails = [];
|
||||||
|
next.zones.forEach((zone) => {
|
||||||
|
(zone.riders || []).forEach((r) => {
|
||||||
|
(r.orders || []).forEach((o) => {
|
||||||
|
flatDetails.push({
|
||||||
|
...o,
|
||||||
|
rider_id: r.rider_id,
|
||||||
|
userid: r.rider_id,
|
||||||
|
rider_name: r.rider_name,
|
||||||
|
rider: r.rider_name
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
next.details = flatDetails;
|
||||||
|
|
||||||
|
return next;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Preview = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const stateData = location.state || {};
|
||||||
|
|
||||||
|
// SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes
|
||||||
|
// through this state. The Dispatch tab renders from it, the Reconcile tab
|
||||||
|
// derives its rider list from it, and Assign Orders sends a flattened copy
|
||||||
|
// of it to the API.
|
||||||
|
const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
|
||||||
|
|
||||||
|
// The AI response arrives via location.state, which the browser stores in
|
||||||
|
// history.state and persists across reloads. That means a reload of
|
||||||
|
// /dispatch/preview would re-hydrate the stale snapshot — including any
|
||||||
|
// pending edits the user thought they discarded. Bounce to /orders when
|
||||||
|
// there's no fresh response, and wipe the history snapshot once consumed
|
||||||
|
// so a later reload / back-forward also bounces instead of re-using it.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stateData.dispatchPreviewData) {
|
||||||
|
navigate('/nearle/orders', { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window !== 'undefined' && window.history?.state) {
|
||||||
|
window.history.replaceState({ ...window.history.state, usr: null }, '');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
const [csvExportData, setCsvExportData] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [tabValue, setTabValue] = useState(0);
|
||||||
|
|
||||||
|
const [reconcileLoading, setReconcileLoading] = useState(false);
|
||||||
|
const [hasReconciled, setHasReconciled] = useState(false);
|
||||||
|
|
||||||
|
// Tracks riders whose orders have been edited since the last AI response
|
||||||
|
// or successful reconcile. Only these are sent to the reconcile API — the
|
||||||
|
// server-side step re-ordering only needs to see what actually changed.
|
||||||
|
const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set());
|
||||||
|
|
||||||
|
// Change-rider dialog state
|
||||||
|
const [changeDialogOpen, setChangeDialogOpen] = useState(false);
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState(null);
|
||||||
|
const [selectedOldRiderId, setSelectedOldRiderId] = useState(null);
|
||||||
|
const [selectedNewRider, setSelectedNewRider] = useState(null);
|
||||||
|
|
||||||
|
const aiMode = stateData.aiMode ?? 1;
|
||||||
|
const selectedMode = stateData.selectedMode || null;
|
||||||
|
const deliveryData = stateData.deliveryData || [];
|
||||||
|
const autoRiders = stateData.autoRiders || [];
|
||||||
|
const absentRidersPayload = stateData.absentRidersPayload || [];
|
||||||
|
const rider = stateData.rider || null;
|
||||||
|
|
||||||
|
const appId = useMemo(() => {
|
||||||
|
if (stateData.appId) return stateData.appId;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const v = localStorage.getItem('applocationid');
|
||||||
|
return v ? Number(v) : 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [stateData.appId]);
|
||||||
|
|
||||||
|
const { data: ridersList } = useQuery({
|
||||||
|
queryKey: ['ridersList', appId],
|
||||||
|
queryFn: fetchRidersList,
|
||||||
|
enabled: !!appId,
|
||||||
|
staleTime: 5 * 60 * 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Derived: rider list for the Reconcile tab. Recomputes whenever the cache
|
||||||
|
// (dispatchPreviewData) changes — so Change Rider / Reconcile both reflect
|
||||||
|
// here without a separate state.
|
||||||
|
const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]);
|
||||||
|
|
||||||
|
// Derived: flat orders list used for the Assign Orders payload + CSV export.
|
||||||
|
// Always reflects the latest cache state.
|
||||||
|
const finaldeliveryList = useMemo(() => {
|
||||||
|
const flat = flattenRiders(reconcileRiders);
|
||||||
|
if (flat.length) return computeDeliveryAmounts(flat);
|
||||||
|
if (Array.isArray(dispatchPreviewData?.details)) {
|
||||||
|
return computeDeliveryAmounts(dispatchPreviewData.details);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [reconcileRiders, dispatchPreviewData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filtered = finaldeliveryList.map((item) => ({
|
||||||
|
zone_name: item.zone_name,
|
||||||
|
ordernotes: item.ordernotes,
|
||||||
|
rider: item.rider,
|
||||||
|
step: item.step,
|
||||||
|
ordertype: item.ordertype,
|
||||||
|
orderamount: item.orderamount,
|
||||||
|
riderkms: item.riderkms,
|
||||||
|
cumulativekms: item.cumulativekms,
|
||||||
|
baseprice: item.baseprice,
|
||||||
|
minkm: item.minkm,
|
||||||
|
priceperkm: item.priceperkm,
|
||||||
|
kms: item.kms,
|
||||||
|
actualkms: item.actualkms,
|
||||||
|
rider_charge: item.rider_charge,
|
||||||
|
deliveryamt: item.deliveryamt,
|
||||||
|
deliverycharges: item.deliverycharges,
|
||||||
|
profit: item.profit
|
||||||
|
}));
|
||||||
|
setCsvExportData(filtered);
|
||||||
|
}, [finaldeliveryList]);
|
||||||
|
|
||||||
|
const notifyRiderMutation = useMutation({
|
||||||
|
mutationFn: notifyRider,
|
||||||
|
onSuccess: () => OpenToast('Notification sent Successfully', 'success', 2000),
|
||||||
|
onError: (error) => OpenToast(error.message, 'error', 2000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDeliveryMutation = useMutation({
|
||||||
|
mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
|
||||||
|
onSuccess: (data) => {
|
||||||
|
OpenToast('Orders Optimised Successfully', 'success', 2000);
|
||||||
|
// Brand new response = brand new source of truth.
|
||||||
|
setDispatchPreviewData(data);
|
||||||
|
setHasReconciled(false);
|
||||||
|
setDirtyRiderIds(new Set());
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
OpenToast(error.message, 'error', 4000);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onSettled: () => setIsLoading(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
const createFinalDeliveryMutation = useMutation({
|
||||||
|
mutationFn: finalCreatedeliveries,
|
||||||
|
onSuccess: () => {
|
||||||
|
OpenToast('Delivery Created Successfully', 'success', 2000);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
|
||||||
|
navigate('/nearle/deliveries');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
OpenToast(error.message, 'error', 4000);
|
||||||
|
setIsLoading(false);
|
||||||
|
},
|
||||||
|
onSettled: () => setIsLoading(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
const reconcileMutation = useMutation({
|
||||||
|
mutationFn: reconcileSteps,
|
||||||
|
onMutate: () => setReconcileLoading(true),
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (Array.isArray(data?.riders)) {
|
||||||
|
// Merge: applyReconcileResponse replaces orders for riders present
|
||||||
|
// in the response and leaves the rest of the cache untouched.
|
||||||
|
setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
|
||||||
|
setHasReconciled(true);
|
||||||
|
// Clear only the riders we just reconciled from the dirty set, so
|
||||||
|
// any unrelated edits made meanwhile are preserved.
|
||||||
|
setDirtyRiderIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
data.riders.forEach((r) => next.delete(String(r.rider_id)));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
OpenToast('Steps reconciled — preview updated', 'success', 2000);
|
||||||
|
} else {
|
||||||
|
OpenToast('Reconcile returned no rider data', 'warning', 3000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
OpenToast(error.message || 'Reconcile failed', 'error', 4000);
|
||||||
|
},
|
||||||
|
onSettled: () => setReconcileLoading(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCreateDelivery = (tune) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
if (aiMode == 0) {
|
||||||
|
createDeliveryMutation.mutate({ deliveries: deliveryData });
|
||||||
|
} else if (selectedMode && selectedMode?.value == 1) {
|
||||||
|
createDeliveryMutation.mutate({
|
||||||
|
deliveries: deliveryData,
|
||||||
|
hypertuning_params: tune || null,
|
||||||
|
selectedMode,
|
||||||
|
absent_riders: absentRidersPayload
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createDeliveryMutation.mutate({
|
||||||
|
data: {
|
||||||
|
orders: deliveryData,
|
||||||
|
riders: autoRiders,
|
||||||
|
config: { pay_type: 'hourly', base_pay: 300.0, strategy: 'multi_trip' },
|
||||||
|
absent_riders: absentRidersPayload
|
||||||
|
},
|
||||||
|
selectedMode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinalCreateDelivery = () => {
|
||||||
|
if (!finaldeliveryList?.length) {
|
||||||
|
OpenToast('No deliveries to assign', 'error', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReconcile = () => {
|
||||||
|
if (!reconcileRiders.length) {
|
||||||
|
OpenToast('No riders to reconcile', 'warning', 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Only send riders that were edited since the last AI response / reconcile.
|
||||||
|
// Their step ordering is the only thing that can be stale — untouched
|
||||||
|
// riders are skipped to keep the payload small.
|
||||||
|
const dirty = reconcileRiders.filter((r) =>
|
||||||
|
dirtyRiderIds.has(String(r.rider_id))
|
||||||
|
);
|
||||||
|
if (!dirty.length) {
|
||||||
|
OpenToast('No edits to reconcile', 'info', 2500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reconcileMutation.mutate({
|
||||||
|
riders: dirty.map((r) => ({
|
||||||
|
rider_id: r.rider_id,
|
||||||
|
orders: r.orders
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openChangeRider = (oldRider, order) => {
|
||||||
|
const oldId =
|
||||||
|
oldRider?.rider_id ?? oldRider?.id ?? order?.rider_id ?? order?.userid ?? null;
|
||||||
|
setSelectedOldRiderId(oldId);
|
||||||
|
setSelectedOrder(order);
|
||||||
|
setSelectedNewRider(null);
|
||||||
|
setChangeDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmChangeRider = () => {
|
||||||
|
if (!selectedNewRider || !selectedOrder) return;
|
||||||
|
// Backend expects an int — coerce at the boundary so a string from the
|
||||||
|
// riders API doesn't propagate into the Assign Orders payload.
|
||||||
|
const newRiderId = Number(selectedNewRider.userid);
|
||||||
|
const newRiderName =
|
||||||
|
selectedNewRider.label ||
|
||||||
|
`${selectedNewRider.firstname || ''} ${selectedNewRider.lastname || ''}`.trim() ||
|
||||||
|
`Rider ${newRiderId}`;
|
||||||
|
|
||||||
|
setDispatchPreviewData((prev) =>
|
||||||
|
moveOrderInPreviewData(prev, {
|
||||||
|
orderId: selectedOrder.orderid,
|
||||||
|
oldRiderId: selectedOldRiderId,
|
||||||
|
newRiderId,
|
||||||
|
newRiderName
|
||||||
|
})
|
||||||
|
);
|
||||||
|
// Both riders' step sequences are now potentially stale: the old rider
|
||||||
|
// lost a stop, the new rider gained one. Mark both as dirty so the next
|
||||||
|
// Reconcile sends exactly these two.
|
||||||
|
setDirtyRiderIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (selectedOldRiderId != null) next.add(String(selectedOldRiderId));
|
||||||
|
if (newRiderId != null && Number.isFinite(newRiderId)) next.add(String(newRiderId));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setHasReconciled(false);
|
||||||
|
setChangeDialogOpen(false);
|
||||||
|
OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100vh', overflow: 'hidden', position: 'relative' }}>
|
||||||
|
<Backdrop
|
||||||
|
sx={{ position: 'absolute', color: '#fff', zIndex: (theme) => theme.zIndex.modal + 1 }}
|
||||||
|
open={isLoading}
|
||||||
|
>
|
||||||
|
<CircularLoader color="inherit" />
|
||||||
|
</Backdrop>
|
||||||
|
|
||||||
|
<Box sx={{ py: 1.25, px: 2, borderBottom: '1px solid #eef2f6' }}>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<Tooltip title="Back to orders" placement="top">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => navigate('/nearle/orders')}
|
||||||
|
sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
|
||||||
|
>
|
||||||
|
<HiOutlineArrowLeft size={20} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Typography variant="h3" fontWeight={600}>
|
||||||
|
Assign Orders
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<Autocomplete
|
||||||
|
options={tuningTypes || []}
|
||||||
|
getOptionLabel={(option) => option.type}
|
||||||
|
sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Hyper Tuning" />}
|
||||||
|
onChange={(e, val, reason) => {
|
||||||
|
if (reason === 'clear') handleCreateDelivery(null);
|
||||||
|
else handleCreateDelivery(val.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<IoReload />}
|
||||||
|
onClick={() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
handleCreateDelivery('reshuffle');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Re-Assign
|
||||||
|
</Button>
|
||||||
|
<CSVExport
|
||||||
|
data={csvExportData}
|
||||||
|
filename={`Orders_Detail_${dayjs().format('YYYY-MM-DD_HHmmss')}.csv`}
|
||||||
|
label=" CSV"
|
||||||
|
style={{ m: 1 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ px: 2, borderBottom: '1px solid #eef2f6' }}>
|
||||||
|
<Tabs value={tabValue} onChange={(e, v) => setTabValue(v)} sx={{ minHeight: 40 }}>
|
||||||
|
<Tab label="Dispatch" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
|
||||||
|
<Tab label="Reconcile" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||||
|
{tabValue === 0 && dispatchPreviewData && (
|
||||||
|
<Dispatch
|
||||||
|
// The key forces a full re-mount when the cache reference changes
|
||||||
|
// (after Change Rider / Reconcile / Re-Assign) so Dispatch's
|
||||||
|
// internal state (focused rider, view mode, etc.) recomputes
|
||||||
|
// against the new orders. Without this, internal memos can stick
|
||||||
|
// to the previous data shape.
|
||||||
|
key={dispatchPreviewData?.__cacheKey || JSON.stringify(reconcileRiders.length)}
|
||||||
|
data={dispatchPreviewData}
|
||||||
|
embedded
|
||||||
|
onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tabValue === 1 && (
|
||||||
|
<Box sx={{ flex: 1, overflow: 'auto', p: 2, bgcolor: '#f8fafc' }}>
|
||||||
|
{reconcileRiders.length === 0 ? (
|
||||||
|
<Typography sx={{ color: '#94a3b8', textAlign: 'center', mt: 4 }}>
|
||||||
|
No rider data available to reconcile.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1.75}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
bgcolor: hasReconciled ? '#ecfdf5' : '#fffbeb',
|
||||||
|
border: `1px solid ${hasReconciled ? '#a7f3d0' : '#fde68a'}`,
|
||||||
|
color: hasReconciled ? '#065f46' : '#92400e',
|
||||||
|
borderRadius: '10px',
|
||||||
|
px: 1.5,
|
||||||
|
py: 1,
|
||||||
|
fontSize: 13
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasReconciled
|
||||||
|
? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.'
|
||||||
|
: 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{reconcileRiders.map((r) => {
|
||||||
|
const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
|
||||||
|
return (
|
||||||
|
<Card key={r.rider_id} sx={{ p: 2, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.25 }}>
|
||||||
|
<Stack direction="row" alignItems="center" gap={1.25}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: '8px',
|
||||||
|
bgcolor: '#eef2ff',
|
||||||
|
color: '#4f46e5',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdTwoWheeler size={18} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 700, fontSize: 14, color: '#1e293b' }}>
|
||||||
|
{r.rider_name}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontSize: 11.5, color: '#64748b' }}>
|
||||||
|
ID: {r.rider_id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" gap={1}>
|
||||||
|
<Chip size="small" label={`${r.orders.length} stops`} sx={{ fontWeight: 600 }} />
|
||||||
|
<Chip size="small" label={`${totalKms.toFixed(1)} km`} variant="outlined" />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" gap={1.25} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
|
{r.orders.map((o, idx) => {
|
||||||
|
const stepNum = o.step ?? idx + 1;
|
||||||
|
const color = stepColor(Number(stepNum) - 1);
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
key={`${o.orderid}-${idx}`}
|
||||||
|
title={
|
||||||
|
<Box>
|
||||||
|
<div>Order #{o.orderid}</div>
|
||||||
|
<div>{o.deliveryaddress || o.deliverysuburb || ''}</div>
|
||||||
|
<div style={{ marginTop: 4, opacity: 0.8 }}>Click to change rider</div>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
onClick={() => openChangeRider(r, o)}
|
||||||
|
sx={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: color,
|
||||||
|
color: '#fff',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: 14,
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow:
|
||||||
|
'0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)',
|
||||||
|
transition: 'transform 0.15s',
|
||||||
|
'&:hover': { transform: 'scale(1.08)' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stepNum}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', pt: 1.5, pb: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
startIcon={<MdSwapHoriz />}
|
||||||
|
onClick={handleReconcile}
|
||||||
|
disabled={reconcileLoading || dirtyRiderIds.size === 0}
|
||||||
|
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
|
||||||
|
>
|
||||||
|
{reconcileLoading
|
||||||
|
? 'Reconciling...'
|
||||||
|
: dirtyRiderIds.size === 0
|
||||||
|
? 'Reconcile'
|
||||||
|
: `Reconcile (${dirtyRiderIds.size})`}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ px: 2, py: 1.25, borderTop: '1px solid #eef2f6' }}>
|
||||||
|
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
startIcon={<ArrowBackIcon />}
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={handleFinalCreateDelivery}>
|
||||||
|
Assign Orders
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Dialog open={changeDialogOpen} onClose={() => setChangeDialogOpen(false)} maxWidth="xs" fullWidth>
|
||||||
|
<DialogTitle sx={{ fontWeight: 700 }}>Change Rider</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography sx={{ mb: 2, fontSize: 13, color: 'text.secondary' }}>
|
||||||
|
Move order #{selectedOrder?.orderid} (step {selectedOrder?.step ?? '—'}) to:
|
||||||
|
</Typography>
|
||||||
|
<Autocomplete
|
||||||
|
options={ridersList || []}
|
||||||
|
getOptionLabel={(o) =>
|
||||||
|
o?.label || `${o?.firstname || ''} ${o?.lastname || ''}`.trim() || ''
|
||||||
|
}
|
||||||
|
value={selectedNewRider}
|
||||||
|
onChange={(e, val) => setSelectedNewRider(val)}
|
||||||
|
renderInput={(params) => <TextField {...params} label="New rider" placeholder="Pick a rider" />}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||||
|
<Button onClick={() => setChangeDialogOpen(false)}>Cancel</Button>
|
||||||
|
<Button variant="contained" disabled={!selectedNewRider} onClick={confirmChangeRider}>
|
||||||
|
Change Rider
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mirrors the orders.js deliveryamt recalc — applied at render-time so the
|
||||||
|
// Assign payload always reflects the current cache without a useEffect.
|
||||||
|
function computeDeliveryAmounts(list) {
|
||||||
|
return list.map((item) => {
|
||||||
|
const cumulativeKms = Number(item.cumulativekms || 0);
|
||||||
|
const minKm = Number(item.minkm || 0);
|
||||||
|
const basePrice = Number(item.baseprice || 0);
|
||||||
|
const pricePerKm = Number(item.priceperkm || 0);
|
||||||
|
if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
|
||||||
|
return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Preview;
|
||||||
87
src/pages/nearle/dispatch/dispatchShared.js
Normal file
87
src/pages/nearle/dispatch/dispatchShared.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Shared constants and pure helpers for the Dispatch page and its
|
||||||
|
// extracted sub-components (CompareDataPanel, etc.). Lives outside
|
||||||
|
// Dispatch.js so we don't create a circular import between the host
|
||||||
|
// component and the child views.
|
||||||
|
|
||||||
|
// Status palette — single source of truth for the status pill colors
|
||||||
|
// rendered on rider cards, order rows, step lists, and tooltips.
|
||||||
|
export const STATUS_STYLES = {
|
||||||
|
created: { label: 'Created', bg: '#3b82f6', fg: '#fff' },
|
||||||
|
pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' },
|
||||||
|
accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' },
|
||||||
|
arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' },
|
||||||
|
picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' },
|
||||||
|
active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' },
|
||||||
|
delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' },
|
||||||
|
skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' },
|
||||||
|
cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' }
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStatusStyle = (status) =>
|
||||||
|
STATUS_STYLES[String(status || '').toLowerCase()] || {
|
||||||
|
label: status || 'Unknown',
|
||||||
|
bg: '#64748b',
|
||||||
|
fg: '#fff'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Order-status sets used for completion / skipped decisions across the
|
||||||
|
// rider list, the planned-route renderer, and the compare data panel.
|
||||||
|
export const FINAL_STATUSES = new Set(['delivered']);
|
||||||
|
export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
|
||||||
|
|
||||||
|
// An order is "active" (currently in progress) when it's neither completed
|
||||||
|
// (delivered) nor skipped/cancelled. The Active view uses this to collapse a
|
||||||
|
// rider down to the single delivery they're working on right now.
|
||||||
|
export const isActiveDelivery = (o) => {
|
||||||
|
const s = String(o?.orderstatus || '').toLowerCase();
|
||||||
|
return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A rider's single in-progress delivery: the first non-final, non-skipped
|
||||||
|
// stop in (trip, step) order. Returns null when the rider has nothing active
|
||||||
|
// (everything delivered/cancelled, or GPS-only with no orders).
|
||||||
|
export const getActiveOrder = (orders) => {
|
||||||
|
if (!Array.isArray(orders) || !orders.length) return null;
|
||||||
|
const sorted = [...orders].sort((a, b) => {
|
||||||
|
const tA = a.trip_number || 1;
|
||||||
|
const tB = b.trip_number || 1;
|
||||||
|
if (tA !== tB) return tA - tB;
|
||||||
|
return (a.step || 0) - (b.step || 0);
|
||||||
|
});
|
||||||
|
return sorted.find(isActiveDelivery) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-step palette — wider and more deliberately spaced than the rider
|
||||||
|
// palette so a 10-stop day reads as 10 distinct colors on the compare
|
||||||
|
// map's polylines + pins.
|
||||||
|
export const STEP_PALETTE = [
|
||||||
|
'#2563eb', // blue-600
|
||||||
|
'#dc2626', // red-600
|
||||||
|
'#16a34a', // green-600
|
||||||
|
'#ea580c', // orange-600
|
||||||
|
'#9333ea', // purple-600
|
||||||
|
'#0891b2', // cyan-600
|
||||||
|
'#ca8a04', // yellow-600
|
||||||
|
'#db2777', // pink-600
|
||||||
|
'#0f766e', // teal-700
|
||||||
|
'#7c3aed', // violet-600
|
||||||
|
'#65a30d', // lime-600
|
||||||
|
'#0284c7', // sky-600
|
||||||
|
'#b91c1c', // red-700
|
||||||
|
'#15803d', // green-700
|
||||||
|
'#a16207', // yellow-700
|
||||||
|
'#86198f' // fuchsia-800
|
||||||
|
];
|
||||||
|
|
||||||
|
export const stepColor = (i) =>
|
||||||
|
STEP_PALETTE[((i % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length];
|
||||||
|
|
||||||
|
// Pure helper — converts 1, 2, 3, 21 → "1st", "2nd", "3rd", "21st". Used
|
||||||
|
// by the compare data panel for the route-sequence diff list ("Visited
|
||||||
|
// 4th · planned 2nd").
|
||||||
|
export const ordinal = (n) => {
|
||||||
|
if (n == null) return '';
|
||||||
|
const s = ['th', 'st', 'nd', 'rd'];
|
||||||
|
const v = n % 100;
|
||||||
|
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -108,7 +108,7 @@ const InvoicePreview = () => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/invoice');
|
navigate('/nearle/invoice');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FaArrowLeft size={'large'} />
|
<FaArrowLeft size={'large'} />
|
||||||
|
|||||||
@@ -1,43 +1,55 @@
|
|||||||
import React, { useState, useEffect, Fragment, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Drawer,
|
Drawer,
|
||||||
IconButton,
|
IconButton,
|
||||||
Toolbar,
|
|
||||||
Typography,
|
Typography,
|
||||||
AppBar,
|
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
Divider,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
useTheme,
|
useTheme,
|
||||||
ListItemAvatar,
|
|
||||||
Avatar,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TableCell,
|
TableCell,
|
||||||
Chip,
|
|
||||||
Stack,
|
Stack,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableHead,
|
TableHead,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
Tabs,
|
CircularProgress,
|
||||||
Tab,
|
InputBase,
|
||||||
CircularProgress
|
Paper,
|
||||||
|
Avatar,
|
||||||
|
ButtonBase
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import {
|
||||||
import SearchBar from 'components/nearle_components/SearchBar';
|
MdMenu,
|
||||||
|
MdSearch,
|
||||||
|
MdClear,
|
||||||
|
MdPlace,
|
||||||
|
MdStorefront,
|
||||||
|
MdMyLocation,
|
||||||
|
MdAccessTime,
|
||||||
|
MdLocalShipping,
|
||||||
|
MdHourglassEmpty,
|
||||||
|
MdCheckCircle,
|
||||||
|
MdCancel,
|
||||||
|
MdReceiptLong
|
||||||
|
} from 'react-icons/md';
|
||||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||||
import { fetchOrders1, gettenantlocations } from '../api/api';
|
import { fetchOrders1, gettenantlocations } from '../api/api';
|
||||||
import Loader from 'components/Loader';
|
import Loader from 'components/Loader';
|
||||||
import CircularLoader from 'components/nearle_components/CircularLoader';
|
import CircularLoader from 'components/nearle_components/CircularLoader';
|
||||||
import { Empty, Skeleton } from 'antd';
|
import { Empty, Skeleton } from 'antd';
|
||||||
import MainCard from 'components/MainCard';
|
import {
|
||||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
DT,
|
||||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
BRAND,
|
||||||
import { CancelOutlined, CheckCircleOutline } from '@mui/icons-material';
|
BRAND_LIGHT,
|
||||||
|
tint,
|
||||||
|
soft,
|
||||||
|
ring,
|
||||||
|
edge,
|
||||||
|
StatusBadge,
|
||||||
|
AccentAvatar
|
||||||
|
} from '../_shared/ordersDesign';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
var utc = require('dayjs/plugin/utc');
|
var utc = require('dayjs/plugin/utc');
|
||||||
@@ -45,6 +57,15 @@ dayjs.extend(utc);
|
|||||||
|
|
||||||
const drawerWidth = 300;
|
const drawerWidth = 300;
|
||||||
|
|
||||||
|
// Status filter tabs — colors aligned with STATUS_META in the shared design system
|
||||||
|
// (blue=created, amber=pending, green=delivered, red=cancelled).
|
||||||
|
const STATUS_TABS = [
|
||||||
|
{ label: 'Created', value: 'created', color: '#3b82f6', icon: MdLocalShipping },
|
||||||
|
{ label: 'Pending', value: 'pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
|
{ label: 'Delivered', value: 'delivered', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
{ label: 'Cancelled', value: 'cancelled', color: '#ef4444', icon: MdCancel }
|
||||||
|
];
|
||||||
|
|
||||||
const ResponsiveLocationDrawer = () => {
|
const ResponsiveLocationDrawer = () => {
|
||||||
const loadMoreRef = useRef();
|
const loadMoreRef = useRef();
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
@@ -71,6 +92,14 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
const [searchword, setSearchword] = useState('');
|
const [searchword, setSearchword] = useState('');
|
||||||
const [debouncedSearchword, setDebouncedSearchword] = useState('');
|
const [debouncedSearchword, setDebouncedSearchword] = useState('');
|
||||||
|
|
||||||
|
// Per-status counts keyed by tab value, so the filter pills can show a badge.
|
||||||
|
const statusCounts = {
|
||||||
|
created: createdLenght,
|
||||||
|
pending: pendingLenght,
|
||||||
|
delivered: deliveredlenght,
|
||||||
|
cancelled: cancelledLenght
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
setDebouncedSearchLocation(searchLocation);
|
setDebouncedSearchLocation(searchLocation);
|
||||||
@@ -87,38 +116,11 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [searchword]);
|
}, [searchword]);
|
||||||
|
|
||||||
const statusMap = [
|
const handleChangetab = (i) => {
|
||||||
{
|
|
||||||
label: 'Created',
|
|
||||||
value: 'created',
|
|
||||||
count: createdLenght,
|
|
||||||
icon: <AccessTimeIcon color="primary" fontSize="small" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Pending',
|
|
||||||
value: 'pending',
|
|
||||||
count: pendingLenght,
|
|
||||||
icon: <LocalShippingOutlinedIcon color="primary" fontSize="small" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Delivered',
|
|
||||||
value: 'delivered',
|
|
||||||
count: deliveredlenght,
|
|
||||||
icon: <CheckCircleOutline color="primary" fontSize="small" />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Cancelled',
|
|
||||||
value: 'cancelled',
|
|
||||||
count: cancelledLenght,
|
|
||||||
icon: <CancelOutlined color="primary" fontSize="small" />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleChangetab = (e, i) => {
|
|
||||||
setSearchword('');
|
setSearchword('');
|
||||||
setRowsPerPage(10);
|
setRowsPerPage(10);
|
||||||
setTabvalue(i);
|
setTabvalue(i);
|
||||||
setCurrentStatus(statusMap[i].value);
|
setCurrentStatus(STATUS_TABS[i].value);
|
||||||
setPage(0);
|
setPage(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,8 +133,15 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
queryKey: ['locations', debouncedSearchLocation],
|
queryKey: ['locations', debouncedSearchLocation],
|
||||||
queryFn: gettenantlocations
|
queryFn: gettenantlocations
|
||||||
});
|
});
|
||||||
|
// Auto-pick a sensible default whenever the locations list changes:
|
||||||
|
// • Nothing selected yet → pick the first item.
|
||||||
|
// • Current selection has been filtered out → also pick the first item
|
||||||
|
// (otherwise the orders panel queries a locationid that's no longer
|
||||||
|
// in the visible list, returning nothing and confusing the operator).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!searchLocation) locations?.length > 0 ? setSelectedLocation(locations[0]) : null;
|
if (!Array.isArray(locations) || locations.length === 0) return;
|
||||||
|
const stillVisible = selectedLocation && locations.some((l) => l.locationid === selectedLocation.locationid);
|
||||||
|
if (!stillVisible) setSelectedLocation(locations[0]);
|
||||||
}, [locations]);
|
}, [locations]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -168,7 +177,7 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
|
root: document.querySelector('.MuiTableContainer-root'),
|
||||||
rootMargin: '0px',
|
rootMargin: '0px',
|
||||||
threshold: 1.0
|
threshold: 1.0
|
||||||
}
|
}
|
||||||
@@ -231,6 +240,139 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
errMessage && console.log(errMessage);
|
errMessage && console.log(errMessage);
|
||||||
}, [errMessage]);
|
}, [errMessage]);
|
||||||
|
|
||||||
|
// Brand-styled scrollbar reused on the sidebar + table.
|
||||||
|
const scrollbarSx = {
|
||||||
|
'&::-webkit-scrollbar': { width: 8, height: 8 },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
backgroundColor: edge(BRAND),
|
||||||
|
borderRadius: 8,
|
||||||
|
'&:hover': { backgroundColor: BRAND }
|
||||||
|
},
|
||||||
|
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||||
|
};
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
// Sidebar — searchable location list. Shared between the desktop persistent
|
||||||
|
// drawer and the mobile temporary drawer.
|
||||||
|
// --------------------------------------------------------------------------
|
||||||
|
const sidebarContent = (
|
||||||
|
<Stack sx={{ height: '100%', bgcolor: '#fff' }}>
|
||||||
|
{/* Sidebar header */}
|
||||||
|
<Box sx={{ px: 1.5, pt: 1.5, pb: 1 }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.25 }}>
|
||||||
|
<Avatar
|
||||||
|
variant="rounded"
|
||||||
|
sx={{ width: 30, height: 30, bgcolor: BRAND, color: '#fff', borderRadius: 1.5, boxShadow: `0 4px 12px ${ring(BRAND)}` }}
|
||||||
|
>
|
||||||
|
<MdStorefront size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<Stack spacing={0}>
|
||||||
|
<Typography sx={{ fontSize: 13.5, fontWeight: 800, color: DT.textPrimary, lineHeight: 1.1 }}>Locations</Typography>
|
||||||
|
<Typography sx={{ fontSize: 10.5, fontWeight: 600, color: DT.textMuted }}>
|
||||||
|
{Array.isArray(locations) ? `${locations.length} active` : '—'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Search pill */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(BRAND),
|
||||||
|
border: `1.5px solid ${edge(BRAND)}`,
|
||||||
|
transition: 'all 0.18s',
|
||||||
|
'&:focus-within': { borderColor: BRAND, boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
||||||
|
<InputBase
|
||||||
|
placeholder="Search location"
|
||||||
|
value={searchLocation}
|
||||||
|
onChange={(e) => setSearchLocation(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{searchLocation && (
|
||||||
|
<IconButton size="small" onClick={() => setSearchLocation('')} sx={{ p: 0.25, color: BRAND }}>
|
||||||
|
<MdClear size={14} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Location list */}
|
||||||
|
<Box sx={{ flex: 1, overflowY: 'auto', px: 1, pb: 1, ...scrollbarSx }}>
|
||||||
|
{locationIsLoading &&
|
||||||
|
Array.from({ length: 8 }).map((_, i) => (
|
||||||
|
<Box key={i} sx={{ px: 1, py: 1 }}>
|
||||||
|
<Skeleton avatar active paragraph={{ rows: 1 }} title={false} />
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{!locationIsLoading && Array.isArray(locations) && locations.length === 0 && (
|
||||||
|
<Box sx={{ py: 5 }}>
|
||||||
|
<Empty description="No locations" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
{locations?.map((row, index) => {
|
||||||
|
const isSelected = row.locationid === selectedLocation?.locationid;
|
||||||
|
return (
|
||||||
|
<ButtonBase
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedLocation(row)}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
textAlign: 'left',
|
||||||
|
gap: 1,
|
||||||
|
px: 1,
|
||||||
|
py: 0.875,
|
||||||
|
borderRadius: 2,
|
||||||
|
position: 'relative',
|
||||||
|
transition: 'background-color 0.14s, box-shadow 0.14s',
|
||||||
|
bgcolor: isSelected ? tint(BRAND) : 'transparent',
|
||||||
|
boxShadow: isSelected ? `inset 3px 0 0 ${BRAND}` : 'none',
|
||||||
|
'&:hover': { bgcolor: isSelected ? tint(BRAND) : DT.surfaceAlt }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccentAvatar color={BRAND} selected={isSelected} size={36}>
|
||||||
|
{row.locationname?.[0]?.toUpperCase() || '?'}
|
||||||
|
</AccentAvatar>
|
||||||
|
<Stack spacing={0} sx={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{ fontSize: 13, fontWeight: 700, color: isSelected ? BRAND : DT.textPrimary, lineHeight: 1.2 }}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{row.locationname}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.375}>
|
||||||
|
<MdPlace size={11} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||||
|
<Typography sx={{ fontSize: 11, fontWeight: 600, color: DT.textSecondary }} noWrap>
|
||||||
|
{row.suburb || '—'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{locationIsLoading && (
|
{locationIsLoading && (
|
||||||
@@ -239,9 +381,8 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
|
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative', bgcolor: DT.surfaceAlt }}>
|
||||||
{/* ---------------- LOCAL DRAWER ---------------- */}
|
{/* ---------------- LOCATION SIDEBAR ---------------- */}
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||||
open={open}
|
open={open}
|
||||||
@@ -255,246 +396,252 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
height: '100%',
|
height: '100%',
|
||||||
overflowY: 'auto',
|
overflow: 'hidden',
|
||||||
|
borderRight: `1px solid ${DT.borderSubtle}`,
|
||||||
transition: 'transform 0.35s ease-in-out',
|
transition: 'transform 0.35s ease-in-out',
|
||||||
zIndex: 10,
|
zIndex: 10
|
||||||
|
|
||||||
/* vertical scrollbar */
|
|
||||||
'&::-webkit-scrollbar:vertical': {
|
|
||||||
width: '7px',
|
|
||||||
opacity: 0,
|
|
||||||
transition: 'opacity 0.3s'
|
|
||||||
},
|
|
||||||
|
|
||||||
/* horizontal scrollbar */
|
|
||||||
'&::-webkit-scrollbar:horizontal': {
|
|
||||||
height: '6px', // thinner horizontal bar
|
|
||||||
opacity: 0,
|
|
||||||
transition: 'opacity 0.3s'
|
|
||||||
},
|
|
||||||
|
|
||||||
/* show scrollbar when hovering drawer */
|
|
||||||
'&:hover::-webkit-scrollbar': {
|
|
||||||
opacity: 1
|
|
||||||
},
|
|
||||||
|
|
||||||
/* thumb styling */
|
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
borderRadius: '8px'
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-thumb:hover': {
|
|
||||||
backgroundColor: theme.palette.primary.dark
|
|
||||||
},
|
|
||||||
|
|
||||||
/* track styling */
|
|
||||||
'&::-webkit-scrollbar-track': {
|
|
||||||
backgroundColor: theme.palette.primary.lighter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ position: 'sticky', top: 0, zIndex: 11, border: 'none' }}>
|
{sidebarContent}
|
||||||
<SearchBar
|
|
||||||
value={searchLocation}
|
|
||||||
placeholder="Search Location"
|
|
||||||
onChange={(e) => setSearchLocation(e.target.value)}
|
|
||||||
sx={{
|
|
||||||
width: 'auto',
|
|
||||||
height: 60,
|
|
||||||
bgcolor: 'white',
|
|
||||||
|
|
||||||
'& .MuiOutlinedInput-notchedOutline': {
|
|
||||||
border: 'none',
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: theme.palette.secondary.light
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<List sx={{ border: 'none', mt: -1 }}>
|
|
||||||
{locations?.map((row, index) => (
|
|
||||||
<React.Fragment key={index}>
|
|
||||||
<ListItem
|
|
||||||
sx={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
bgcolor: row.locationid == selectedLocation?.locationid ? theme.palette.secondary[200] : 'none',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: theme.palette.secondary.lighter
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedLocation(row);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar
|
|
||||||
sx={{
|
|
||||||
bgcolor: 'primary.main', // background color
|
|
||||||
color: 'white' // text color
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.locationname[0].toUpperCase()}
|
|
||||||
</Avatar>{' '}
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={row.locationname} secondary={row.suburb} />
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
{/* -------------- LOCAL PAGE APPBAR -------------- */}
|
|
||||||
<AppBar
|
|
||||||
elevation={0}
|
|
||||||
position="absolute"
|
|
||||||
sx={{
|
|
||||||
top: 0,
|
|
||||||
left: open && isDesktop ? `${drawerWidth}px` : 0,
|
|
||||||
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
|
|
||||||
transition: 'left 0.3s ease, width 0.3s ease',
|
|
||||||
zIndex: 1100, // BELOW drawer, ABOVE content
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: theme.palette.secondary.light
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
|
||||||
<Stack
|
|
||||||
sx={{ width: '100%', borderBottom: '1px soild red' }}
|
|
||||||
display={'flex'}
|
|
||||||
flexDirection={'row'}
|
|
||||||
alignItems={'center'}
|
|
||||||
justifyContent={'space-between'}
|
|
||||||
flexWrap={'wrap'}
|
|
||||||
>
|
|
||||||
<Stack display={'flex'} flexDirection={'row'} alignItems={'center'}>
|
|
||||||
<IconButton color="primary" onClick={toggleDrawer} sx={{ mr: 1 }}>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<Typography variant="h5" color={'primary'} sx={{ whiteSpace: 'nowrap', ml: 2 }}>
|
{/* ---------------- MAIN PANEL ---------------- */}
|
||||||
{selectedLocation?.locationname}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Stack>
|
|
||||||
<SearchBar
|
|
||||||
value={searchword}
|
|
||||||
placeholder={'Search Order Details'}
|
|
||||||
onChange={(e) => setSearchword(e.target.value)}
|
|
||||||
sx={{
|
|
||||||
width: 'auto',
|
|
||||||
height: 40,
|
|
||||||
bgcolor: 'white',
|
|
||||||
maxWidth: 800,
|
|
||||||
borderRadius: 1
|
|
||||||
// '& .MuiOutlinedInput-notchedOutline': {
|
|
||||||
// border: 'none'
|
|
||||||
// }
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
{/* ---------------- PAGE SCROLLABLE CONTENT ---------------- */}
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
overflow: 'auto',
|
height: '100%',
|
||||||
pt: '64px', // Height of AppBar
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
pl: isDesktop && open ? `${drawerWidth}px` : 0,
|
pl: isDesktop && open ? `${drawerWidth}px` : 0,
|
||||||
transition: 'padding-left 0.3s ease',
|
transition: 'padding-left 0.3s ease'
|
||||||
mt: -1
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack
|
{/* ---------------- GRADIENT HEADER ---------------- */}
|
||||||
display={'flex'}
|
<Paper
|
||||||
flexDirection={'row'}
|
elevation={0}
|
||||||
justifyContent={'space-between'}
|
|
||||||
alignItems={'center'}
|
|
||||||
flexWrap={'wrap-reverse'}
|
|
||||||
gap={2}
|
|
||||||
sx={{
|
sx={{
|
||||||
border: '1px solid ',
|
flexShrink: 0,
|
||||||
borderBottom: 0,
|
px: { xs: 1.25, sm: 1.75 },
|
||||||
borderColor: 'bg.main',
|
py: { xs: 1, sm: 1.25 },
|
||||||
p: 1.5
|
borderRadius: 0,
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Tabs Wrapper */}
|
<Stack
|
||||||
|
direction={{ xs: 'column', md: 'row' }}
|
||||||
|
alignItems={{ xs: 'stretch', md: 'center' }}
|
||||||
|
justifyContent="space-between"
|
||||||
|
spacing={{ xs: 1, md: 1.5 }}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||||
|
<Tooltip title={open ? 'Hide locations' : 'Show locations'} arrow>
|
||||||
|
<IconButton
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
sx={{
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
border: `1px solid ${DT.borderSubtle}`,
|
||||||
|
color: BRAND,
|
||||||
|
'&:hover': { bgcolor: tint(BRAND), borderColor: BRAND }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdMenu size={18} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tabs value={tabvalue} onChange={handleChangetab} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile>
|
<Avatar
|
||||||
{statusMap.map((item, index) => (
|
variant="rounded"
|
||||||
<Tab
|
sx={{ width: 36, height: 36, bgcolor: BRAND, color: '#fff', borderRadius: 1.5, boxShadow: `0 4px 12px ${ring(BRAND)}` }}
|
||||||
key={index}
|
>
|
||||||
label={
|
<MdMyLocation size={19} />
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
</Avatar>
|
||||||
{item.icon}
|
<Stack spacing={0.125}>
|
||||||
<span>{item.label}</span>
|
<Typography
|
||||||
<Chip label={item.count} color="primary" variant="light" size="small" />
|
variant="h3"
|
||||||
</Stack>
|
sx={{
|
||||||
}
|
fontWeight: 800,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
fontSize: { xs: '1.05rem', sm: '1.2rem', md: '1.3rem' }
|
||||||
|
}}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{selectedLocation?.locationname || 'Select a location'}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.75}>
|
||||||
|
<Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: '#10b981', boxShadow: '0 0 0 3px rgba(16,185,129,0.18)' }} />
|
||||||
|
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600 }} noWrap>
|
||||||
|
{selectedLocation?.suburb ? `${selectedLocation.suburb} · ` : ''}Live · {dayjs(startdate).format('DD MMM YYYY')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Order search pill */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
border: `1.5px solid ${edge(BRAND)}`,
|
||||||
|
minWidth: { xs: '100%', md: 280 },
|
||||||
|
maxWidth: { md: 360 },
|
||||||
|
transition: 'all 0.18s',
|
||||||
|
'&:focus-within': { borderColor: BRAND, boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
||||||
|
<InputBase
|
||||||
|
placeholder="Search order details"
|
||||||
|
value={searchword}
|
||||||
|
onChange={(e) => setSearchword(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
{searchword && (
|
||||||
</Tabs>
|
<IconButton size="small" onClick={() => setSearchword('')} sx={{ p: 0.25, color: BRAND }}>
|
||||||
</Stack>
|
<MdClear size={14} />
|
||||||
<MainCard
|
</IconButton>
|
||||||
content={false}
|
)}
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* ---------------- STATUS FILTER PILLS ---------------- */}
|
||||||
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
overflow: 'hidden',
|
flexShrink: 0,
|
||||||
height: 'calc(100vh - 200px)', // adjust as needed
|
px: { xs: 1, sm: 1.5 },
|
||||||
|
py: 1,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column'
|
gap: 0.75,
|
||||||
|
overflowX: 'auto',
|
||||||
|
...scrollbarSx
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Fragment>
|
{STATUS_TABS.map((item, index) => {
|
||||||
{/* Scrollable table container */}
|
const isActive = tabvalue === index;
|
||||||
|
const Icon = item.icon;
|
||||||
|
const count = statusCounts[item.value];
|
||||||
|
return (
|
||||||
|
<ButtonBase
|
||||||
|
key={index}
|
||||||
|
onClick={() => handleChangetab(index)}
|
||||||
|
sx={{
|
||||||
|
flexShrink: 0,
|
||||||
|
gap: 0.75,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.625,
|
||||||
|
borderRadius: 999,
|
||||||
|
fontWeight: 700,
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
border: `1.5px solid ${isActive ? item.color : DT.borderSubtle}`,
|
||||||
|
bgcolor: isActive ? item.color : '#fff',
|
||||||
|
color: isActive ? '#fff' : DT.textSecondary,
|
||||||
|
boxShadow: isActive ? `0 4px 12px ${ring(item.color)}` : 'none',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: item.color,
|
||||||
|
color: isActive ? '#fff' : item.color,
|
||||||
|
bgcolor: isActive ? item.color : tint(item.color)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={14} />
|
||||||
|
<Typography sx={{ fontSize: 12.5, fontWeight: 700, lineHeight: 1 }}>{item.label}</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minWidth: 20,
|
||||||
|
height: 18,
|
||||||
|
px: 0.625,
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 10.5,
|
||||||
|
fontWeight: 800,
|
||||||
|
bgcolor: isActive ? 'rgba(255,255,255,0.25)' : soft(item.color),
|
||||||
|
color: isActive ? '#fff' : item.color
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{count ?? 0}
|
||||||
|
</Box>
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ---------------- ORDERS TABLE ---------------- */}
|
||||||
|
<Box sx={{ flex: 1, overflow: 'hidden', p: { xs: 1, sm: 1.5 } }}>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
borderRadius: 2,
|
||||||
|
border: `1px solid ${DT.borderSubtle}`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: '#fff',
|
||||||
|
boxShadow: DT.shadowSoft
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TableContainer
|
<TableContainer
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
sx={{
|
sx={{ flex: 1, overflow: 'auto', ...scrollbarSx }}
|
||||||
width: '100%',
|
|
||||||
flex: 1,
|
|
||||||
overflow: 'auto',
|
|
||||||
borderBottom: 1,
|
|
||||||
maxHeight: 'calc(100vh - 225px)',
|
|
||||||
borderColor: 'divider',
|
|
||||||
'&::-webkit-scrollbar': { width: '12px' },
|
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
backgroundColor: theme.palette.primary.main,
|
|
||||||
borderRadius: '8px'
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-thumb:hover': {
|
|
||||||
backgroundColor: theme.palette.primary.dark
|
|
||||||
},
|
|
||||||
'&::-webkit-scrollbar-track': {
|
|
||||||
backgroundColor: theme.palette.primary.lighter
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Table stickyHeader>
|
<Table stickyHeader size="small">
|
||||||
{/* HEADER */}
|
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>S.No</TableCell>
|
sx={{
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Orders</TableCell>
|
'& th': {
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Pickup</TableCell>
|
backgroundColor: DT.surfaceAlt,
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Drop</TableCell>
|
color: DT.textSecondary,
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Notes</TableCell>
|
fontSize: 10.5,
|
||||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Status</TableCell>
|
fontWeight: 800,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
py: 0.75,
|
||||||
|
px: 1
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell sx={{ width: 36 }}>#</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 150 }}>Order</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 150 }}>Pickup</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 150 }}>Drop</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 140 }}>Notes</TableCell>
|
||||||
|
<TableCell sx={{ width: 120 }}>Status</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
{/* BODY */}
|
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{/* LOADING STATE */}
|
{/* LOADING STATE */}
|
||||||
{loading &&
|
{loading &&
|
||||||
[...Array(10)].map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
<TableRow key={index}>
|
<TableRow key={index}>
|
||||||
{[...Array(6)].map((__, i) => (
|
{Array.from({ length: 6 }).map((__, i) => (
|
||||||
<TableCell key={i}>
|
<TableCell key={i} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
||||||
<Skeleton animation="wave" />
|
<Skeleton.Input active size="small" style={{ width: '100%', height: 18 }} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -503,8 +650,16 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
{/* EMPTY STATE */}
|
{/* EMPTY STATE */}
|
||||||
{!loading && rows?.length === 0 && (
|
{!loading && rows?.length === 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} sx={{ minWidth: '100%', height: 500 }} align="center">
|
<TableCell colSpan={6} sx={{ py: 7, borderBottom: 'none' }}>
|
||||||
<Empty description={'No Orders'} />
|
<Stack alignItems="center" spacing={1.25}>
|
||||||
|
<Avatar variant="rounded" sx={{ width: 56, height: 56, bgcolor: soft('#94a3b8'), color: DT.textMuted, borderRadius: 2 }}>
|
||||||
|
<MdReceiptLong size={26} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No orders found</Typography>
|
||||||
|
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
||||||
|
{searchword ? 'Try a different keyword or clear the search.' : 'No orders in this status for the selected location.'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@@ -512,83 +667,101 @@ const ResponsiveLocationDrawer = () => {
|
|||||||
{/* DATA ROWS */}
|
{/* DATA ROWS */}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
rows?.map((row, index) => (
|
rows?.map((row, index) => (
|
||||||
<TableRow key={index} sx={{ cursor: 'pointer' }}>
|
<TableRow
|
||||||
<TableCell>{page * rowsPerPage + index + 1}</TableCell>
|
key={index}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||||||
|
'& td': { borderBottom: `1px solid ${DT.divider}`, py: 0.75, px: 1, verticalAlign: 'top' },
|
||||||
|
'&:hover': { backgroundColor: tint(BRAND), boxShadow: `inset 3px 0 0 ${BRAND}` }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Typography sx={{ fontWeight: 700, fontSize: 12, color: DT.textMuted }}>{page * rowsPerPage + index + 1}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
{/* Order Info */}
|
{/* Order Info */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant="body2" noWrap>
|
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||||
{row.orderid}
|
{row.orderid}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" noWrap>
|
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.125 }}>
|
||||||
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
|
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||||
</Typography>
|
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
||||||
<Typography variant="caption" noWrap>
|
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
||||||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
</Typography>
|
||||||
</Typography>
|
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
||||||
|
· {dayjs(row.deliverydate).utc().format('DD MMM YY')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Pickup */}
|
{/* Pickup */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack spacing={0.125}>
|
||||||
<Avatar sx={{ width: 25, height: 25 }} />
|
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||||
<Stack>
|
{row.pickupcustomer || '—'}
|
||||||
<Typography variant="caption">{row.pickupcustomer}</Typography>
|
</Typography>
|
||||||
<Typography variant="caption">{row.pickupcontactno}</Typography>
|
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||||
<Tooltip title={row.pickupaddress}>
|
{row.pickupcontactno}
|
||||||
<Typography variant="caption">{row.pickupsuburb || row.pickupaddress.slice(0, 20)}</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
<Tooltip title={row.pickupaddress || ''}>
|
||||||
</Stack>
|
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||||
|
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Drop */}
|
{/* Drop */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack spacing={0.125}>
|
||||||
<Avatar sx={{ width: 25, height: 25 }} />
|
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||||
<Stack>
|
{row.deliverycustomer || '—'}
|
||||||
<Typography variant="caption">{row.deliverycustomer}</Typography>
|
</Typography>
|
||||||
<Typography variant="caption">{row.deliverycontactno}</Typography>
|
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||||
<Tooltip title={row.deliveryaddress}>
|
{row.deliverycontactno}
|
||||||
<Typography variant="caption">{row.deliverysuburb || row.deliveryaddress.slice(0, 20)}</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
<Tooltip title={row.deliveryaddress || ''}>
|
||||||
</Stack>
|
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||||
|
{row.deliverysuburb || (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')}
|
||||||
|
</Typography>
|
||||||
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Notes */}
|
||||||
<TableCell>{row.ordernotes}</TableCell>
|
<TableCell>
|
||||||
|
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.35 }}>
|
||||||
|
{row.ordernotes || '—'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="row" spacing={1}>
|
<StatusBadge status={row.orderstatus} />
|
||||||
{row.orderstatus === 'pending' && <Chip label="Pending" color="warning" size="small" />}
|
|
||||||
{row.orderstatus === 'confirmed' && <Chip label="Confirmed" color="success" size="small" />}
|
|
||||||
{row.orderstatus === 'cancelled' && <Chip label="Cancelled" color="error" size="small" />}
|
|
||||||
{row.orderstatus === 'delivered' && <Chip label="Completed" color="primary" size="small" />}
|
|
||||||
{row.orderstatus === 'processing' && <Chip label="Processing" color="primary" size="small" />}
|
|
||||||
{row.orderstatus === 'ready' && <Chip label="Accepted" color="info" size="small" />}
|
|
||||||
{row.orderstatus === 'active' && <Chip label="Picked" color="info" size="small" />}
|
|
||||||
{row.orderstatus === 'closed' && <Chip label="Closed" color="info" size="small" />}
|
|
||||||
{row.orderstatus === 'created' && <Chip label="Created" color="secondary" size="small" />}
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{rows?.length != 0 && (
|
{rows?.length != 0 && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} rowSpan={3}>
|
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
|
||||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
<Stack ref={loadMoreRef} alignItems="center" justifyContent="center" sx={{ height: 40 }}>
|
||||||
{isFetchingNextPage ? <CircularProgress /> : hasNextPage ? <CircularProgress /> : 'No More Orders'}
|
{isFetchingNextPage || hasNextPage ? (
|
||||||
</div>
|
<CircularProgress size={20} sx={{ color: BRAND }} />
|
||||||
|
) : (
|
||||||
|
<Typography sx={{ fontSize: 11.5, fontWeight: 700, color: DT.textMuted }}>No more orders</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Fragment>
|
</Paper>
|
||||||
</MainCard>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const Login = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (localStorage.getItem('authname')) {
|
if (localStorage.getItem('authname')) {
|
||||||
navigate('/nearle/orders');
|
navigate('/nearle/dispatch');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ const Login = () => {
|
|||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
navigate('/nearle/orders');
|
navigate('/nearle/dispatch');
|
||||||
} else {
|
} else {
|
||||||
OpenToast(res.data.message, 'warning', 2000);
|
OpenToast(res.data.message, 'warning', 2000);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
288
src/pages/nearle/orders/OrdersRedesign.css
Normal file
288
src/pages/nearle/orders/OrdersRedesign.css
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
/* OrdersRedesign.css — premium aesthetics + micro-animations
|
||||||
|
Ported from xpressconsole; trimmed to only the classes the
|
||||||
|
redesigned multipleOrders.js (and its peers) consume here. */
|
||||||
|
|
||||||
|
/* ---------- Page background ---------- */
|
||||||
|
.orders-workspace-bg {
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Cards ---------- */
|
||||||
|
.orders-card {
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1px solid #eef2f6 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(62, 73, 84, 0.04),
|
||||||
|
0 4px 12px -2px rgba(62, 73, 84, 0.02) !important;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.orders-card:hover {
|
||||||
|
box-shadow: 0 16px 35px -8px rgba(62, 73, 84, 0.08),
|
||||||
|
0 6px 16px -3px rgba(62, 73, 84, 0.03) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-card .MuiOutlinedInput-root {
|
||||||
|
font-size: 13px !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
.orders-card .MuiOutlinedInput-input {
|
||||||
|
font-size: 13px !important;
|
||||||
|
padding-top: 9px !important;
|
||||||
|
padding-bottom: 9px !important;
|
||||||
|
}
|
||||||
|
.orders-card .MuiInputLabel-root {
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
.orders-card .MuiInputLabel-root.MuiInputLabel-shrink {
|
||||||
|
font-size: 11.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Section title bar (gradient strip + h-title) ---------- */
|
||||||
|
.section-title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-title-bar::before {
|
||||||
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: linear-gradient(180deg, #1890ff, #096dd9);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.section-title-bar--accent::before {
|
||||||
|
background: linear-gradient(180deg, #a855f7, #65387a);
|
||||||
|
}
|
||||||
|
.section-title-bar .MuiTypography-root {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Delivery preferences card ---------- */
|
||||||
|
.delivery-prefs-card {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fbfcff 100%) !important;
|
||||||
|
border: 1px solid #eef2f6 !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
}
|
||||||
|
.delivery-prefs-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.delivery-prefs-title {
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.delivery-prefs-sub {
|
||||||
|
font-size: 10.5px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.55px;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.delivery-prefs-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.delivery-prefs-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
.delivery-prefs-label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 10.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.55px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Pricing summary card ---------- */
|
||||||
|
.pricing-summary-card {
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
|
||||||
|
border: 1px solid #eef2f6 !important;
|
||||||
|
border-radius: 14px !important;
|
||||||
|
padding: 14px 16px !important;
|
||||||
|
}
|
||||||
|
.pricing-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
.pricing-title {
|
||||||
|
font-size: 14px !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
color: #1e293b !important;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.pricing-subtitle {
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Total charge badge ---------- */
|
||||||
|
.total-charge-badge {
|
||||||
|
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.10) 100%);
|
||||||
|
border: 1px solid rgba(101, 56, 122, 0.18);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.total-charge-left {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.total-charge-icon {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #65387A;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.total-charge-label {
|
||||||
|
font-size: 11.5px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
color: #65387A;
|
||||||
|
}
|
||||||
|
.total-charge-val {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #65387A;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Gradient action button ---------- */
|
||||||
|
.gradient-btn-create {
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #65387a 100%) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
letter-spacing: 0.01em !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
padding: 8px 18px !important;
|
||||||
|
min-height: 38px !important;
|
||||||
|
box-shadow: 0 4px 12px -3px rgba(24, 144, 255, 0.30),
|
||||||
|
0 2px 4px rgba(101, 56, 122, 0.10) !important;
|
||||||
|
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||||
|
border: none !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 8px !important;
|
||||||
|
}
|
||||||
|
.gradient-btn-create .MuiButton-startIcon,
|
||||||
|
.gradient-btn-create .MuiButton-endIcon {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.gradient-btn-create:hover {
|
||||||
|
filter: brightness(1.04);
|
||||||
|
box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40),
|
||||||
|
0 3px 8px rgba(101, 56, 122, 0.18) !important;
|
||||||
|
}
|
||||||
|
.gradient-btn-create:active { filter: brightness(0.98); }
|
||||||
|
.gradient-btn-create.Mui-disabled,
|
||||||
|
.gradient-btn-create:disabled {
|
||||||
|
background: #e2e8f0 !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Compact header inputs (Location/Client/Business) ---------- */
|
||||||
|
.header-compact-tf .MuiOutlinedInput-root {
|
||||||
|
border-radius: 10px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
padding-left: 10px !important;
|
||||||
|
font-size: 12.5px !important;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.header-compact-tf .MuiOutlinedInput-input {
|
||||||
|
padding-top: 6px !important;
|
||||||
|
padding-bottom: 6px !important;
|
||||||
|
font-size: 12.5px !important;
|
||||||
|
}
|
||||||
|
.header-compact-tf .MuiInputLabel-root {
|
||||||
|
font-size: 11.5px !important;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b !important;
|
||||||
|
}
|
||||||
|
.header-compact-tf .MuiInputLabel-shrink {
|
||||||
|
transform: translate(12px, -7px) scale(0.82) !important;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
.header-compact-tf .MuiOutlinedInput-notchedOutline { border-color: #e2e8f0; }
|
||||||
|
.header-compact-tf:hover .MuiOutlinedInput-notchedOutline { border-color: #cbd5e1; }
|
||||||
|
.header-compact-tf .Mui-focused .MuiOutlinedInput-notchedOutline { border-width: 1.5px !important; }
|
||||||
|
|
||||||
|
.header-compact-input .MuiAutocomplete-endAdornment {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
right: 8px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
height: auto;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.header-compact-input .MuiAutocomplete-endAdornment .MuiSvgIcon-root {
|
||||||
|
font-size: 16px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.header-compact-input .MuiAutocomplete-clearIndicator,
|
||||||
|
.header-compact-input .MuiAutocomplete-popupIndicator {
|
||||||
|
padding: 3px !important;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
|
.header-compact-input .MuiAutocomplete-clearIndicator:hover,
|
||||||
|
.header-compact-input .MuiAutocomplete-popupIndicator:hover {
|
||||||
|
background: rgba(148, 163, 184, 0.12) !important;
|
||||||
|
color: #475569 !important;
|
||||||
|
}
|
||||||
|
.header-compact-input .MuiAutocomplete-popupIndicator { margin-right: 0; }
|
||||||
|
.header-compact-input .MuiOutlinedInput-root {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
padding-right: 60px !important;
|
||||||
|
}
|
||||||
|
.header-compact-input .MuiAutocomplete-input {
|
||||||
|
padding: 4px 4px 4px 0 !important;
|
||||||
|
height: auto !important;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -97,7 +97,91 @@ import {
|
|||||||
// DeleteTwoTone
|
// DeleteTwoTone
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import {
|
||||||
|
MdLocalShipping,
|
||||||
|
MdHourglassEmpty,
|
||||||
|
MdCheckCircle,
|
||||||
|
MdCancel,
|
||||||
|
MdAccessTime,
|
||||||
|
MdHistoryToggleOff,
|
||||||
|
MdAssignmentTurnedIn,
|
||||||
|
MdEdit,
|
||||||
|
MdArrowBack,
|
||||||
|
MdReceiptLong
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import { Paper, Box } from '@mui/material';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Design tokens — shared with the rest of the redesigned operator pages.
|
||||||
|
// ============================================================================
|
||||||
|
const DT = {
|
||||||
|
radiusPill: 999,
|
||||||
|
radiusCard: 16,
|
||||||
|
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||||
|
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||||
|
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||||
|
textPrimary: '#0f172a',
|
||||||
|
textSecondary: '#64748b',
|
||||||
|
textMuted: '#94a3b8',
|
||||||
|
borderSubtle: '#e2e8f0',
|
||||||
|
divider: '#f1f5f9',
|
||||||
|
surface: '#ffffff',
|
||||||
|
surfaceAlt: '#f8fafc'
|
||||||
|
};
|
||||||
|
const dtA = (c, suffix) => `${c}${suffix}`;
|
||||||
|
const tint = (c) => dtA(c, '08');
|
||||||
|
const soft = (c) => dtA(c, '18');
|
||||||
|
const ring = (c) => dtA(c, '26');
|
||||||
|
const edge = (c) => dtA(c, '55');
|
||||||
|
|
||||||
|
const BRAND = '#662582';
|
||||||
|
const BRAND_LIGHT = '#9255AB';
|
||||||
|
|
||||||
|
// Semantic per-status palette — also drives StatusBadge.
|
||||||
|
const STATUS_META = {
|
||||||
|
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
|
assigned: { label: 'Assigned', color: '#0ea5e9', icon: MdAssignmentTurnedIn },
|
||||||
|
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
modified: { label: 'Modified', color: '#06b6d4', icon: MdHistoryToggleOff },
|
||||||
|
processing: { label: 'Processing', color: BRAND, icon: MdAccessTime },
|
||||||
|
active: { label: 'Active', color: '#8b5cf6', icon: MdLocalShipping },
|
||||||
|
closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
|
||||||
|
completed: { label: 'Completed', color: '#10b981', icon: MdCheckCircle },
|
||||||
|
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatusBadge = ({ status, size = 'md' }) => {
|
||||||
|
if (!status) return null;
|
||||||
|
const meta = STATUS_META[String(status).toLowerCase()] || {
|
||||||
|
label: status,
|
||||||
|
color: DT.textMuted,
|
||||||
|
icon: MdHistoryToggleOff
|
||||||
|
};
|
||||||
|
const Icon = meta.icon;
|
||||||
|
const px = size === 'lg' ? 1.25 : 1;
|
||||||
|
const py = size === 'lg' ? 0.5 : 0.375;
|
||||||
|
const fs = size === 'lg' ? 12 : 11;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(meta.color),
|
||||||
|
border: `1px solid ${edge(meta.color)}`,
|
||||||
|
color: meta.color,
|
||||||
|
fontSize: fs,
|
||||||
|
fontWeight: 800,
|
||||||
|
whiteSpace: 'nowrap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon size={12} /> {meta.label}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Details = () => {
|
const Details = () => {
|
||||||
// const [searchParams] = useSearchParams();
|
// const [searchParams] = useSearchParams();
|
||||||
@@ -854,59 +938,105 @@ const Details = () => {
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={() => handleClose(false)}
|
onClose={() => handleClose(false)}
|
||||||
maxWidth="xs"
|
maxWidth="xs"
|
||||||
|
PaperProps={{ sx: { borderRadius: 3 } }}
|
||||||
>
|
>
|
||||||
<DialogContent sx={{ mt: 2, my: 1 }}>
|
<Box
|
||||||
<Stack alignItems="center" spacing={3.5}>
|
sx={{
|
||||||
<Avatar color="error" sx={{ width: 72, height: 72, fontSize: '1.75rem' }}>
|
p: 2.5,
|
||||||
|
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
|
||||||
<DeleteFilled />
|
<DeleteFilled />
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Grid >
|
<Stack>
|
||||||
<Chip label={orderid.slice(4)} variant="combined" color='warning' size='small' />
|
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
||||||
</Grid>
|
Cancel Order
|
||||||
<Stack spacing={2}>
|
</Typography>
|
||||||
{/* <Typography variant="h4" align="center">
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
Are you sure you want to cancel this order?
|
Confirm to permanently cancel this order
|
||||||
</Typography> */}
|
|
||||||
{(invoiceeligible) &&
|
|
||||||
|
|
||||||
|
|
||||||
<Alert color="warning" variant="border" icon={<WarningFilled />}>
|
|
||||||
<AlertTitle>Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed.</AlertTitle>
|
|
||||||
{/* <Typography variant="h6"> This is an warning alert.</Typography> */}
|
|
||||||
<Link href='https://thelegendarystaff.com/' target='_blank' >Terms & Condition link</Link>
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
<Typography variant="h4" align="center">
|
|
||||||
Please type in the order number to confirm.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<TextField
|
|
||||||
type='text'
|
|
||||||
onChange={(e) => {
|
|
||||||
console.log(e.target.value)
|
|
||||||
setDeletepassword(e.target.value)
|
|
||||||
}}
|
|
||||||
error={deletepassword !== orderid.slice(4)}
|
|
||||||
// error={true}
|
|
||||||
value={deletepassword}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
<DialogContent sx={{ pt: 3 }}>
|
||||||
|
<Stack alignItems="center" spacing={2.5}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint('#f59e0b'),
|
||||||
|
border: `1px solid ${edge('#f59e0b')}`,
|
||||||
|
color: '#f59e0b',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 800
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdReceiptLong size={12} /> {orderid.slice(4)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Stack direction="row" spacing={2} sx={{ width: 1 }}>
|
{(invoiceeligible) &&
|
||||||
<Button fullWidth color="error" variant="contained" onClick={() => {
|
<Alert color="warning" variant="border" icon={<WarningFilled />}>
|
||||||
if (deletepassword === orderid.slice(4)) {
|
<AlertTitle>Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed.</AlertTitle>
|
||||||
cancelorder();
|
<Link href='https://thelegendarystaff.com/' target='_blank' >Terms & Condition link</Link>
|
||||||
handleClose(true);
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
}} autoFocus>
|
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
Yes, Cancel
|
Please type in the order number to confirm.
|
||||||
</Button>
|
</Typography>
|
||||||
<Button fullWidth onClick={() => handleClose(false)} color="secondary" variant="outlined">
|
<TextField
|
||||||
|
type="text"
|
||||||
|
fullWidth
|
||||||
|
onChange={(e) => setDeletepassword(e.target.value)}
|
||||||
|
error={deletepassword !== orderid.slice(4)}
|
||||||
|
value={deletepassword}
|
||||||
|
placeholder={orderid.slice(4)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleClose(false)}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
py: 1,
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
color: DT.textSecondary,
|
||||||
|
fontWeight: 700,
|
||||||
|
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
|
||||||
|
}}
|
||||||
|
>
|
||||||
No
|
No
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
autoFocus
|
||||||
|
onClick={() => {
|
||||||
|
if (deletepassword === orderid.slice(4)) {
|
||||||
|
cancelorder();
|
||||||
|
handleClose(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
py: 1,
|
||||||
|
bgcolor: '#ef4444',
|
||||||
|
fontWeight: 700,
|
||||||
|
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
|
||||||
|
'&:hover': { bgcolor: '#dc2626' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes, Cancel
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -925,40 +1055,116 @@ const Details = () => {
|
|||||||
// fullScreen
|
// fullScreen
|
||||||
TransitionComponent={PopupTransition}>
|
TransitionComponent={PopupTransition}>
|
||||||
|
|
||||||
<DialogTitle>
|
<DialogTitle
|
||||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
sx={{
|
||||||
<Stack direction={{ sm: 'row', xs: 'column' }} spacing={2} alignItems={'center'}>
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||||
<Typography variant='h3'>Assign Roles</Typography>
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
p: { xs: 2, sm: 2.5 }
|
||||||
<Chip label={clientname} variant="light" color="primary" />
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={1.5}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ bgcolor: BRAND, color: '#fff', width: 40, height: 40, boxShadow: `0 6px 18px ${ring(BRAND)}` }}>
|
||||||
|
<MdAssignmentTurnedIn size={20} />
|
||||||
|
</Avatar>
|
||||||
|
<Stack>
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 800, color: DT.textPrimary, lineHeight: 1.1 }}>
|
||||||
|
Assign Roles
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
|
{clientname} · {currentrole}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Chip label={orderid} variant="combined" color='warning' size={'large'} />
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint('#f59e0b'),
|
||||||
|
border: `1px solid ${edge('#f59e0b')}`,
|
||||||
|
color: '#f59e0b',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 800
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdReceiptLong size={12} /> {orderid}
|
||||||
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Grid container sx={{ p: 1 }} spacing={2}>
|
|
||||||
|
|
||||||
|
<Grid container sx={{ pt: 2 }} spacing={1.5}>
|
||||||
<Grid item sm={6} xs={12}>
|
<Grid item sm={6} xs={12}>
|
||||||
|
<Box
|
||||||
{/* <Chip label={currentrole} variant="combined" color="primary" size='normal' /> */}
|
onClick={() => setTabstatus((e) => (e === 0 ? 1 : 0))}
|
||||||
<Tabs
|
sx={{
|
||||||
value={tabstatus}
|
display: 'inline-flex',
|
||||||
// onChange={handleChangetab}
|
alignItems: 'center',
|
||||||
onChange={() => setTabstatus((e) => (e === 0) ? 1 : 0)}
|
gap: 0.75,
|
||||||
variant="scrollable" scrollButtons="auto" >
|
px: 1.25,
|
||||||
{/* <Tab label="All" /> */}
|
py: 0.625,
|
||||||
<Tab label={currentrole} />
|
borderRadius: 999,
|
||||||
</Tabs>
|
cursor: 'pointer',
|
||||||
|
bgcolor: BRAND,
|
||||||
|
color: '#fff',
|
||||||
|
fontWeight: 800,
|
||||||
|
fontSize: 12,
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ width: 22, height: 22, bgcolor: 'rgba(255,255,255,0.22)', color: '#fff' }}>
|
||||||
|
<MdAssignmentTurnedIn size={12} />
|
||||||
|
</Avatar>
|
||||||
|
{currentrole || 'Role'}
|
||||||
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item sm={6} xs={12}>
|
<Grid item sm={6} xs={12}>
|
||||||
<Stack direction={'row'} justifyContent={{ xs: 'flex-start', sm: 'flex-end' }}
|
<Stack direction="row" justifyContent={{ xs: 'flex-start', sm: 'flex-end' }} spacing={1} flexWrap="wrap" useFlexGap>
|
||||||
// alignItems={{xs:'flex-end',sm:'center'}}
|
{[
|
||||||
sx={{ height: '100%' }} spacing={2}>
|
{ label: 'Required', value: currentshiftobj.shifts, color: BRAND },
|
||||||
<Chip sx={{ width: '130px' }} label={`Required:${currentshiftobj.shifts}`} variant="combined" color='primary' size='normal' />
|
{ label: 'Assigned', value: currentshiftobj.assigned, color: '#10b981' },
|
||||||
<Chip sx={{ width: '130px' }} label={`Assigned: ${currentshiftobj.assigned}`} variant="combined" color='success' size='normal' />
|
{ label: 'Remaining', value: currentshiftobj.remaining, color: '#ef4444' }
|
||||||
<Chip sx={{ width: '130px' }} label={`Remaining: ${currentshiftobj.remaining}`} variant="combined" color='error' size='normal' />
|
].map((c) => (
|
||||||
|
<Box
|
||||||
|
key={c.label}
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.625,
|
||||||
|
px: 1,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(c.color),
|
||||||
|
border: `1px solid ${edge(c.color)}`,
|
||||||
|
color: c.color,
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: 800
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c.label}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minWidth: 22,
|
||||||
|
height: 18,
|
||||||
|
px: 0.5,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 800,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
color: c.color,
|
||||||
|
border: `1px solid ${edge(c.color)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{c.value ?? 0}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
@@ -1256,215 +1462,194 @@ const Details = () => {
|
|||||||
<Typography variant="h3">Details</Typography>
|
<Typography variant="h3">Details</Typography>
|
||||||
</Grid> */}
|
</Grid> */}
|
||||||
|
|
||||||
<CardActions
|
<Paper
|
||||||
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
top: '60px',
|
top: '60px',
|
||||||
// top:0,
|
zIndex: 5,
|
||||||
bgcolor: theme.palette.background.default,
|
mb: 2,
|
||||||
zIndex: 1,
|
p: { xs: 1.5, sm: 2, md: 2.5 },
|
||||||
// borderBottom: `1px solid ${theme.palette.divider}`,
|
borderRadius: DT.radiusCard / 8,
|
||||||
width: '100%'
|
border: '1px solid',
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||||
|
boxShadow: DT.shadowMd
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid item xs={12} >
|
<Stack
|
||||||
<Stack direction={{ md: 'row', xs: 'column' }} justifyContent="space-between" alignItems="flex-end"
|
direction={{ xs: 'column', md: 'row' }}
|
||||||
sx={{ width: '100%', p: 1 }}
|
alignItems={{ xs: 'flex-start', md: 'center' }}
|
||||||
>
|
justifyContent="space-between"
|
||||||
|
spacing={{ xs: 1.5, md: 2 }}
|
||||||
<Stack direction='row' spacing={2} alignItems='center'
|
>
|
||||||
justifyContent='flex-start'
|
<Stack direction="row" alignItems="center" spacing={{ xs: 1.25, sm: 1.75 }}>
|
||||||
sx={{ width: { xs: '100%', md: '0' } }}
|
<IconButton
|
||||||
|
onClick={() => history.back()}
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#fff',
|
||||||
|
border: `1px solid ${DT.borderSubtle}`,
|
||||||
|
borderRadius: 999,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
'&:hover': { bgcolor: tint(BRAND), borderColor: edge(BRAND), color: BRAND }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<MdArrowBack size={18} />
|
||||||
<IconButton
|
</IconButton>
|
||||||
onClick={() => history.back()}
|
<Avatar
|
||||||
// onClick={()=>}
|
sx={{
|
||||||
|
width: { xs: 40, sm: 48 },
|
||||||
|
height: { xs: 40, sm: 48 },
|
||||||
|
bgcolor: BRAND,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdReceiptLong size={22} />
|
||||||
|
</Avatar>
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
variant="h3"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 800,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
fontSize: { xs: '1.25rem', sm: '1.5rem', md: '1.75rem' }
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ArrowBackIcon />
|
Order Details
|
||||||
</IconButton>
|
</Typography>
|
||||||
{/* <Link to="/dashboard">Test me</Link> */}
|
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap sx={{ mt: 0.75 }}>
|
||||||
<Stack direction='column' alignItems='flex-start'>
|
<Box
|
||||||
<Typography variant="h3">Details</Typography>
|
sx={{
|
||||||
<Stack direction="row" spacing={1}>
|
display: 'inline-flex',
|
||||||
{/* <Typography noWrap color="secondary"></Typography> */}
|
alignItems: 'center',
|
||||||
<Chip label={(orderid === '') ? <Skeleton sx={{ width: '80px', bgcolor: '#fff9c4' }} animation="wave" /> : orderid} variant="combined" color='warning' size='small' />
|
gap: 0.5,
|
||||||
{/* <Typography variant="subtitle1">Date</Typography> */}
|
px: 1,
|
||||||
{/* <Typography color="secondary">{orderdate}</Typography> */}
|
py: 0.375,
|
||||||
<Chip label={(orderdate === '') ? <Skeleton sx={{ width: '80px', bgcolor: '#b3e5fc' }} animation="wave" /> : orderdate} variant="combined" color="primary" size='small' />
|
borderRadius: 999,
|
||||||
|
bgcolor: tint('#f59e0b'),
|
||||||
|
border: `1px solid ${edge('#f59e0b')}`,
|
||||||
|
color: '#f59e0b',
|
||||||
{(orderstatus === 'pending') &&
|
fontSize: 11,
|
||||||
|
fontWeight: 800
|
||||||
<Chip label="Pending" color="error" size="small" />
|
}}
|
||||||
}
|
|
||||||
{(orderstatus === 'cancelled') &&
|
|
||||||
|
|
||||||
|
|
||||||
<Chip label="Cancelled" color="secondary" size="small" />
|
|
||||||
}
|
|
||||||
{(orderstatus === 'completed') &&
|
|
||||||
|
|
||||||
<Chip label="Completed" color="primary" size="small" />
|
|
||||||
}
|
|
||||||
{(orderstatus === 'processing') &&
|
|
||||||
<Chip label="Processing" color="primary" size="small" />
|
|
||||||
}
|
|
||||||
{(orderstatus === 'assigned') &&
|
|
||||||
<Chip label="Assigned" color="warning" size="small" />
|
|
||||||
}
|
|
||||||
{(orderstatus === 'confirmed') &&
|
|
||||||
<Chip label="Confirmed" color="success" size="small" />
|
|
||||||
}
|
|
||||||
|
|
||||||
{(orderstatus === 'active') &&
|
|
||||||
<Chip label="Active" color="info" size="small" />
|
|
||||||
}
|
|
||||||
{(orderstatus === 'closed') &&
|
|
||||||
<Chip label="Closed" color="info" size="small" />
|
|
||||||
}
|
|
||||||
|
|
||||||
{(orderstatus === 'modified') &&
|
|
||||||
<Chip label="Modified"
|
|
||||||
color='secondary' size="small" variant='contained' />
|
|
||||||
}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
|
|
||||||
<Stack direction="row" spacing={2}
|
|
||||||
sx={{ mt: { md: 0, xs: 2 } }}
|
|
||||||
>
|
|
||||||
|
|
||||||
{/* <Typography>{dayjs(startdate).$d.toString()}</Typography> */}
|
|
||||||
{/* <Typography>{startdate}</Typography> */}
|
|
||||||
{/* <Typography> {dayjs().$d.toString()}</Typography> */}
|
|
||||||
|
|
||||||
{(((orderstatus === 'pending')
|
|
||||||
|| (orderstatus === 'assigned')
|
|
||||||
|| (orderstatus === 'confirmed')
|
|
||||||
|| (orderstatus === 'modified'))
|
|
||||||
|
|
||||||
|
|
||||||
// && (dayjs(startdate).$d > dayjs().$d)
|
|
||||||
) &&
|
|
||||||
|
|
||||||
|
|
||||||
<Tooltip title='Edit'>
|
|
||||||
|
|
||||||
<Button1
|
|
||||||
variant="outlined"
|
|
||||||
color="info"
|
|
||||||
sx={{ borderRadius: '40px' }}
|
|
||||||
startIcon={<BorderColorIcon color='info' />}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
// if (dayjs(startdate).$d > dayjs().$d) {
|
|
||||||
if (dayjs(dayjs().format('MM-DD-YYYY')).isBefore(dayjs(dayjs(startdate).format('MM-DD-YYYY')))) {
|
|
||||||
navigate(`/editorder`
|
|
||||||
, {
|
|
||||||
state: {
|
|
||||||
orderheaderid: orderheaderid,
|
|
||||||
tenantid: tenantid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
enqueueSnackbar('Order cannot be edited.\n Order date is not valid at this time',
|
|
||||||
|
|
||||||
{
|
|
||||||
|
|
||||||
variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
|
||||||
autoHideDuration: 4000,
|
|
||||||
style: { whiteSpace: "pre-line" }
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit Order
|
|
||||||
</Button1>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* {(((orderstatus === 'pending')
|
|
||||||
|| (orderstatus === 'modified'))
|
|
||||||
&& assignedpendingcount === 0) &&
|
|
||||||
<>
|
|
||||||
<Button1
|
|
||||||
variant="outlined"
|
|
||||||
color="primary"
|
|
||||||
sx={{ borderRadius: '40px' }}
|
|
||||||
startIcon={<SendIcon />}
|
|
||||||
onClick={() => {
|
|
||||||
fetchassignedstaffs();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Notify Staff
|
|
||||||
</Button1>
|
|
||||||
</>
|
|
||||||
} */}
|
|
||||||
{(orderstatus !== 'cancelled' && orderstatus !== '' && orderstatus !== 'completed' && orderstatus !== 'closed') &&
|
|
||||||
<>
|
|
||||||
<Button1
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={() => {
|
|
||||||
console.log(dayjs(startdate).diff(dayjs(), 'm') / 60)
|
|
||||||
if ((dayjs(startdate).diff(dayjs(), 'm') / 60) > 24) {
|
|
||||||
setInvoiceeligible(false)
|
|
||||||
setOpen(true)
|
|
||||||
} else {
|
|
||||||
setInvoiceeligible(true)
|
|
||||||
setOpen(true)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}}
|
|
||||||
sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 } }}
|
|
||||||
startIcon={<CancelOutlinedIcon />}
|
|
||||||
>
|
|
||||||
Cancel Order
|
|
||||||
</Button1>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{(orderstatus === 'cancelled') &&
|
|
||||||
<>
|
|
||||||
<Chip label={`Order Cancelled on ${cancelleddate}`} variant="combined" color='error' />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{/* {(orderstatus === 'completed') &&
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
onClick={() => {
|
|
||||||
navigate(`/invoice/create`, {
|
|
||||||
state: {
|
|
||||||
orderheaderid: orderheaderid,
|
|
||||||
tenantid: tenantid
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 } }}
|
|
||||||
>
|
>
|
||||||
Raise Invoice
|
<MdReceiptLong size={11} />
|
||||||
</Button>
|
{orderid === '' ? <Skeleton sx={{ width: 80 }} animation="wave" /> : orderid}
|
||||||
} */}
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 1,
|
||||||
|
py: 0.375,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(BRAND),
|
||||||
|
border: `1px solid ${edge(BRAND)}`,
|
||||||
|
color: BRAND,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 800
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdAccessTime size={11} />
|
||||||
|
{orderdate === '' ? <Skeleton sx={{ width: 80 }} animation="wave" /> : orderdate}
|
||||||
|
</Box>
|
||||||
|
<StatusBadge status={orderstatus} />
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid>
|
|
||||||
</CardActions>
|
<Stack direction="row" spacing={1.5} alignItems="center" flexWrap="wrap" useFlexGap>
|
||||||
|
{((orderstatus === 'pending') ||
|
||||||
|
(orderstatus === 'assigned') ||
|
||||||
|
(orderstatus === 'confirmed') ||
|
||||||
|
(orderstatus === 'modified')) && (
|
||||||
|
<Tooltip title="Edit">
|
||||||
|
<Button1
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
borderColor: edge(BRAND),
|
||||||
|
color: BRAND,
|
||||||
|
fontWeight: 700,
|
||||||
|
px: 2,
|
||||||
|
'&:hover': { borderColor: BRAND, bgcolor: tint(BRAND), boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||||
|
}}
|
||||||
|
startIcon={<MdEdit size={16} />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (dayjs(dayjs().format('MM-DD-YYYY')).isBefore(dayjs(dayjs(startdate).format('MM-DD-YYYY')))) {
|
||||||
|
navigate(`/editorder`, {
|
||||||
|
state: {
|
||||||
|
orderheaderid: orderheaderid,
|
||||||
|
tenantid: tenantid
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar('Order cannot be edited.\n Order date is not valid at this time', {
|
||||||
|
variant: 'error',
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: 4000,
|
||||||
|
style: { whiteSpace: 'pre-line' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit Order
|
||||||
|
</Button1>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{orderstatus !== 'cancelled' && orderstatus !== '' && orderstatus !== 'completed' && orderstatus !== 'closed' && (
|
||||||
|
<Button1
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
borderColor: edge('#ef4444'),
|
||||||
|
color: '#ef4444',
|
||||||
|
fontWeight: 700,
|
||||||
|
px: 2,
|
||||||
|
'&:hover': { borderColor: '#ef4444', bgcolor: tint('#ef4444'), boxShadow: `0 0 0 3px ${ring('#ef4444')}` }
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if ((dayjs(startdate).diff(dayjs(), 'm') / 60) > 24) {
|
||||||
|
setInvoiceeligible(false);
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setInvoiceeligible(true);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
startIcon={<CancelOutlinedIcon />}
|
||||||
|
>
|
||||||
|
Cancel Order
|
||||||
|
</Button1>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{orderstatus === 'cancelled' && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint('#ef4444'),
|
||||||
|
border: `1px solid ${edge('#ef4444')}`,
|
||||||
|
color: '#ef4444',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 800
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdCancel size={12} /> Cancelled on {cancelleddate}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
|
||||||
{/* Dialog window */}
|
{/* Dialog window */}
|
||||||
|
|||||||
1657
src/pages/nearle/orders/miltiUploadBackup.js
Normal file
1657
src/pages/nearle/orders/miltiUploadBackup.js
Normal file
File diff suppressed because it is too large
Load Diff
898
src/pages/nearle/orders/multiOrderBackup.js
Normal file
898
src/pages/nearle/orders/multiOrderBackup.js
Normal file
@@ -0,0 +1,898 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Loader from 'components/Loader';
|
||||||
|
import { useEffect, useState, Fragment } from 'react';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import MainCard from 'components/MainCard';
|
||||||
|
import axios from 'axios';
|
||||||
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
|
import { Empty } from 'antd';
|
||||||
|
import MyLocationIcon from '@mui/icons-material/MyLocation';
|
||||||
|
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||||||
|
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||||||
|
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
var utc = require('dayjs/plugin/utc');
|
||||||
|
dayjs.extend(utc);
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { GoogleMap, LoadScript, Marker } from '@react-google-maps/api';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
InputAdornment,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
Stack,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Autocomplete,
|
||||||
|
Divider,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
Checkbox,
|
||||||
|
DialogActions,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
OutlinedInput,
|
||||||
|
FormGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableCell,
|
||||||
|
TableBody,
|
||||||
|
TableRow,
|
||||||
|
Paper,
|
||||||
|
TableHead,
|
||||||
|
Box
|
||||||
|
} from '@mui/material';
|
||||||
|
import CircularLoader from 'components/nearle_components/CircularLoader';
|
||||||
|
// import RidersPinPointOSM from './RidersPinPointOSM';
|
||||||
|
import RidersPinPoint from './ridersPinPoint';
|
||||||
|
|
||||||
|
const MultipleOrders = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [btnLoading, setBtnLoading] = useState(false);
|
||||||
|
const [appId, setAppId] = useState(0);
|
||||||
|
|
||||||
|
const [tenantLocations, setTenantlocations] = useState([]);
|
||||||
|
const userid = localStorage.getItem('userid');
|
||||||
|
const tenId = localStorage.getItem('tenantid');
|
||||||
|
const [tid, setTid] = useState(0);
|
||||||
|
const [isLocation, setIsLocation] = useState(false);
|
||||||
|
const [basePrice, setBasePrice] = useState(0);
|
||||||
|
const [pricePerKm, setPricePerKm] = useState(0);
|
||||||
|
const [minKm, setMinKm] = useState(0);
|
||||||
|
const [pickCust, setPickCust] = useState(null);
|
||||||
|
const [dropCust, setDropCust] = useState([]);
|
||||||
|
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
|
||||||
|
const [searchCustList, setSearchCustList] = useState('');
|
||||||
|
const [customerlist, setCustomerlist] = useState([]);
|
||||||
|
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
|
||||||
|
const [timeslotarr, setTimeslotarr] = useState([]);
|
||||||
|
const [starttime, setStatrttime] = useState();
|
||||||
|
const [endtime, setEndtime] = useState();
|
||||||
|
const [alertmessage, setAlertmessage] = useState('');
|
||||||
|
const [otherinstructions, setOtherinstructions] = useState('');
|
||||||
|
const [admintoken, setAdmintoken] = useState();
|
||||||
|
const [totaldist, settotaldist] = useState(0);
|
||||||
|
const [totalAmt, settotalAmt] = useState(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showMap, setShowMap] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dropCust && console.log('dropCust', dropCust);
|
||||||
|
}, [dropCust]);
|
||||||
|
|
||||||
|
// =============================================== || opentoast || ===============================================
|
||||||
|
const opentoast = (message, variant, time) => {
|
||||||
|
enqueueSnackbar(message, {
|
||||||
|
variant: variant,
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: time ? time : 1500
|
||||||
|
});
|
||||||
|
console.log(alertmessage);
|
||||||
|
};
|
||||||
|
// ==============================|| fetchAppLocations ||============================== //
|
||||||
|
const fetchAppLocations = async () => {
|
||||||
|
try {
|
||||||
|
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
|
||||||
|
console.log('fetchAppLocations', locationRes.data.details);
|
||||||
|
} catch (err) {
|
||||||
|
console.log('locationRes', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAppLocations();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ============================================= || fetchTenantPricing || =============================================
|
||||||
|
|
||||||
|
const fetchTenantPricing = async (id) => {
|
||||||
|
try {
|
||||||
|
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tenId}`);
|
||||||
|
console.log('pricingResponse', pricingResponse.data.details);
|
||||||
|
setBasePrice(pricingResponse.data.details.baseprice);
|
||||||
|
setPricePerKm(pricingResponse.data.details.priceperkm);
|
||||||
|
setMinKm(pricingResponse.data.details.minkm);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('fetchTenantPricing error', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTenantPricing();
|
||||||
|
}, []);
|
||||||
|
// ============================================= || gettenantlocations (branches) || =============================================
|
||||||
|
const gettenantlocations = async (id) => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
|
||||||
|
console.log('gettenantlocations', res.data.details);
|
||||||
|
if (res.data.details.length == 1) {
|
||||||
|
setIsLocation(true);
|
||||||
|
setTenantlocations(res.data.details);
|
||||||
|
setPickCust(res.data.details[0]);
|
||||||
|
} else {
|
||||||
|
setTenantlocations(res.data.details);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('gettenantlocations', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
gettenantlocations(tenId);
|
||||||
|
}, []);
|
||||||
|
// ========================================================= || clientdetails || =========================================================
|
||||||
|
const clientdetails = async () => {
|
||||||
|
try {
|
||||||
|
let url =
|
||||||
|
searchCustList == ''
|
||||||
|
? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenId}&pageno=1&pagesize=10`
|
||||||
|
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenId}&keyword=${searchCustList}`;
|
||||||
|
await axios
|
||||||
|
.get(url)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.status) {
|
||||||
|
console.log('clientdetails', res.data.details);
|
||||||
|
|
||||||
|
setCustomerlist(res.data.details);
|
||||||
|
let arr = [];
|
||||||
|
res.data.details.map((val) => {
|
||||||
|
arr.push({
|
||||||
|
label: `${val.firstname} | ${val.contactno}`,
|
||||||
|
...val
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
opentoast('server error', 'warning');
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (tenId) {
|
||||||
|
clientdetails();
|
||||||
|
}
|
||||||
|
}, [searchCustList.length > 3, searchCustList == '', tenId]);
|
||||||
|
|
||||||
|
// ========================================================= || calculateTotal(dist , charge) || =========================================================
|
||||||
|
const calculateTotal = () => {
|
||||||
|
let a1 = 0;
|
||||||
|
let a2 = 0;
|
||||||
|
dropCust?.map((customer) => {
|
||||||
|
a1 += customer.distance;
|
||||||
|
a2 += customer.totalcharge;
|
||||||
|
});
|
||||||
|
settotaldist(a1);
|
||||||
|
settotalAmt(a2);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
dropCust && calculateTotal();
|
||||||
|
}, [dropCust]);
|
||||||
|
|
||||||
|
// ========================================================= || handleCheckboxChange || =========================================================
|
||||||
|
const handleCheckboxChange = async (event, customer) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
console.log('event', event.target.checked);
|
||||||
|
console.log('customer', customer);
|
||||||
|
if (event.target.checked) {
|
||||||
|
// If the checkbox is checked, calculate the distance and add the customer
|
||||||
|
try {
|
||||||
|
const obj = await calculateDistance(customer);
|
||||||
|
console.log('return of calculateDistance', obj);
|
||||||
|
|
||||||
|
const { roundedDistance, totalcharge } = obj;
|
||||||
|
// Create a new customer object with the distance property
|
||||||
|
const updatedCustomer = {
|
||||||
|
...customer,
|
||||||
|
distance: roundedDistance,
|
||||||
|
totalcharge: totalcharge
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the updated customer object to dropCust
|
||||||
|
setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
|
||||||
|
|
||||||
|
// Log the rounded distance
|
||||||
|
console.log(`Rounded Distance: ${roundedDistance} km`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to calculate distance:', error);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
} else {
|
||||||
|
// If the checkbox is unchecked, remove the customer from dropCust
|
||||||
|
setDropCust((prevDropCust) => {
|
||||||
|
return prevDropCust.filter((cust) => cust.customerid !== customer.customerid);
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========================================================= || calculateDistance || =========================================================
|
||||||
|
const calculateDistance = async (customer) => {
|
||||||
|
console.log('Distance calculation starts');
|
||||||
|
const service = new google.maps.DistanceMatrixService();
|
||||||
|
// Helper function to get the distance matrix
|
||||||
|
const getDistanceMatrix = async (origins, destinations) => {
|
||||||
|
console.log('origins', origins);
|
||||||
|
console.log('destinations', destinations);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log('calculation starts');
|
||||||
|
service.getDistanceMatrix(
|
||||||
|
{
|
||||||
|
origins: [new google.maps.LatLng(origins.latitude, origins.longitude)],
|
||||||
|
destinations: [new google.maps.LatLng(destinations.latitude, destinations.longitude)],
|
||||||
|
travelMode: 'DRIVING',
|
||||||
|
unitSystem: google.maps.UnitSystem.METRIC // Distances in metric units (km)
|
||||||
|
},
|
||||||
|
(response, status) => {
|
||||||
|
console.log('cal response', response);
|
||||||
|
console.log('cal status', status);
|
||||||
|
if (status === 'OK') {
|
||||||
|
console.log('calcualtion resolved');
|
||||||
|
resolve(response);
|
||||||
|
} else {
|
||||||
|
console.log('calcualtion rejected');
|
||||||
|
|
||||||
|
reject(new Error(`Error calculating distance: ${status}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Call getDistanceMatrix and wait for the response
|
||||||
|
const response = await getDistanceMatrix(pickCust, customer);
|
||||||
|
|
||||||
|
// Extract distance from the first result
|
||||||
|
const distanceInMeters = response.rows[0].elements[0].distance.value;
|
||||||
|
|
||||||
|
// Convert distance from meters to kilometers
|
||||||
|
const distanceInKilometers = distanceInMeters / 1000;
|
||||||
|
|
||||||
|
// Round the distance to the nearest integer
|
||||||
|
const roundedDistance = Math.round(distanceInKilometers);
|
||||||
|
let totalcharge;
|
||||||
|
if (roundedDistance < minKm) {
|
||||||
|
console.log('minKm', minKm);
|
||||||
|
console.log('pricePerKm', pricePerKm);
|
||||||
|
console.log('basePrice', basePrice);
|
||||||
|
totalcharge = basePrice;
|
||||||
|
} else {
|
||||||
|
console.log('minKm', minKm);
|
||||||
|
console.log('pricePerKm', pricePerKm);
|
||||||
|
console.log('basePrice', basePrice);
|
||||||
|
totalcharge = (roundedDistance - minKm) * pricePerKm + basePrice;
|
||||||
|
console.log('totalcharge', totalcharge);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the rounded distance
|
||||||
|
return { roundedDistance, totalcharge };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error calculating distance:', error);
|
||||||
|
throw error; // Rethrow the error to be handled by the caller
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================================================== || fetchTiming || ====================================================
|
||||||
|
const fetchTiming = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await axios
|
||||||
|
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
|
||||||
|
.then((res) => {
|
||||||
|
console.log('fetchTiming', res);
|
||||||
|
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
|
||||||
|
if (res.data.status) {
|
||||||
|
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||||
|
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
|
||||||
|
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
|
||||||
|
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
|
||||||
|
let arr = [];
|
||||||
|
for (
|
||||||
|
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
|
||||||
|
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
|
||||||
|
j++, i = dayjs(i).add(30, 'm')
|
||||||
|
) {
|
||||||
|
arr.push(i);
|
||||||
|
}
|
||||||
|
console.log('setTimeslotarr', arr);
|
||||||
|
setTimeslotarr(arr);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (appId) {
|
||||||
|
fetchTiming();
|
||||||
|
}
|
||||||
|
}, [starttime, endtime, appId]);
|
||||||
|
|
||||||
|
const fetchAppAdminTokens = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await axios
|
||||||
|
.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
|
||||||
|
.then((res) => {
|
||||||
|
const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging
|
||||||
|
console.log('fetchAppAdminTokens', res);
|
||||||
|
console.log('userfcmtokemArray', userfcmtokemArray);
|
||||||
|
if (res.data.status) {
|
||||||
|
setAdmintoken(userfcmtokemArray);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (starttime && endtime) {
|
||||||
|
fetchAppAdminTokens();
|
||||||
|
}
|
||||||
|
}, [starttime, endtime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('pickCust', pickCust);
|
||||||
|
}, [pickCust]);
|
||||||
|
|
||||||
|
// ==================================================== || fetchtenantinfo || ====================================================
|
||||||
|
const fetchtenantinfo = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
console.log('tid', tid);
|
||||||
|
|
||||||
|
await axios
|
||||||
|
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
|
||||||
|
.then((res) => {
|
||||||
|
console.log('fetchtenantinfo', res);
|
||||||
|
if (res.data.status) {
|
||||||
|
fetchAppAdminTokens();
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (tid) {
|
||||||
|
fetchtenantinfo();
|
||||||
|
}
|
||||||
|
}, [tid]);
|
||||||
|
// ================================================== || sendnotifications || ==================================================
|
||||||
|
const sendnotifications = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
await axios
|
||||||
|
.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
|
||||||
|
priority: 'high',
|
||||||
|
registration_ids: admintoken,
|
||||||
|
data: {
|
||||||
|
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
|
||||||
|
},
|
||||||
|
notification: {
|
||||||
|
title: 'Nearle Merchant',
|
||||||
|
body: 'An Order has been placed successfully,kindly process the same',
|
||||||
|
sound: 'ring'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
if (res.data.message == 'Success') {
|
||||||
|
enqueueSnackbar('Notification sent Successfully', {
|
||||||
|
variant: 'success',
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
enqueueSnackbar(err.message, {
|
||||||
|
variant: 'error',
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: 1000
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// =============================================== || creategrouporders || ===============================================
|
||||||
|
const creategrouporders = async () => {
|
||||||
|
const arr = dropCust?.map((customer) => ({
|
||||||
|
applocationid: pickCust.applocationid,
|
||||||
|
cancellled: '',
|
||||||
|
// categoryid: +tenant.categoryid,
|
||||||
|
configid: 9,
|
||||||
|
customerid: customer.customerid,
|
||||||
|
deliveryaddress: customer.address || '',
|
||||||
|
deliverycharge: +customer.totalcharge || 0,
|
||||||
|
deliverycity: customer.city || '',
|
||||||
|
deliverycontactno: customer.contactno || '',
|
||||||
|
deliverycustomer: customer.firstname || '',
|
||||||
|
deliveryid: +customer.customerid,
|
||||||
|
deliverylandmark: customer.landmark || '',
|
||||||
|
deliverylat: customer.latitude,
|
||||||
|
deliverylocation: customer.suburb || '',
|
||||||
|
deliverylocationid: customer.deliverylocationid || 0,
|
||||||
|
deliverylong: customer.longitude,
|
||||||
|
// deliverytime: `${dayjs(startdate).format('YYYY-MM-DD HH:mm:ss')} `,
|
||||||
|
deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
deliverytype: 'B',
|
||||||
|
delivered: '',
|
||||||
|
itemcount: 1,
|
||||||
|
kms: customer.distance.toString() || 0,
|
||||||
|
locationid: +pickCust.locationid,
|
||||||
|
moduleid: +pickCust.moduleid,
|
||||||
|
orderamount: +customer.totalcharge || 0,
|
||||||
|
ordercharges: 0.0,
|
||||||
|
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||||
|
orderheaderid: 0,
|
||||||
|
orderid: '', //
|
||||||
|
ordernotes: otherinstructions,
|
||||||
|
orderstatus: 'created',
|
||||||
|
ordervalue: +customer.totalcharge || 0,
|
||||||
|
partnerid: pickCust.partnerid,
|
||||||
|
partneruserid: +userid,
|
||||||
|
paymentstatus: 1,
|
||||||
|
paymenttype: 42,
|
||||||
|
pending: '',
|
||||||
|
pickupaddress: pickCust.address || '',
|
||||||
|
pickupcity: pickCust.locationcity || '',
|
||||||
|
pickupcontactno: pickCust.contactno || '',
|
||||||
|
pickupcustomer: pickCust.locationname || '',
|
||||||
|
pickuplandmark: pickCust.landmark || '',
|
||||||
|
pickuplat: pickCust.latitude,
|
||||||
|
pickuplocation: pickCust.suburb || '',
|
||||||
|
pickuplocationid: pickCust.locationid || 0,
|
||||||
|
pickuplong: pickCust.longitude,
|
||||||
|
processing: '',
|
||||||
|
ready: '',
|
||||||
|
remarks: '',
|
||||||
|
taxamount: 0.0,
|
||||||
|
tenantid: pickCust.tenantid,
|
||||||
|
tenantuserid: 0
|
||||||
|
}));
|
||||||
|
console.log('arr', arr);
|
||||||
|
|
||||||
|
if (!tenId) {
|
||||||
|
opentoast('Choose Client ', 'warning');
|
||||||
|
} else {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
await axios
|
||||||
|
.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.data.status) {
|
||||||
|
enqueueSnackbar('Order Created Successfully', {
|
||||||
|
variant: 'success',
|
||||||
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
||||||
|
autoHideDuration: 1000
|
||||||
|
});
|
||||||
|
if (admintoken) {
|
||||||
|
// notifyadmin(admintoken);
|
||||||
|
sendnotifications();
|
||||||
|
}
|
||||||
|
navigate('/nearle/orders');
|
||||||
|
} else {
|
||||||
|
opentoast(res.data.message, 'warning');
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
console.log(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
// opentoast(err.data.message, 'warning');
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(arr);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{loading && <Loader />}
|
||||||
|
{/* <RidersPinPointOSM /> */}
|
||||||
|
<Grid container sx={{ mb: 2 }}>
|
||||||
|
<Grid item xs={12} sm={3} md={6}>
|
||||||
|
<Stack>
|
||||||
|
<Typography variant="h3" whiteSpace="nowrap">
|
||||||
|
Multiple Orders
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={9} md={6}>
|
||||||
|
<Stack
|
||||||
|
sx={{}}
|
||||||
|
width={'100%'}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={2}
|
||||||
|
justifyContent={'flex-end'}
|
||||||
|
flexWrap={{ xs: 'wrap', custom550: 'nowrap' }}
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
|
{/* Business Location */}
|
||||||
|
<Stack sx={{ width: '100%' }}>
|
||||||
|
{tenantLocations?.length === 1 ? (
|
||||||
|
<TextField
|
||||||
|
label="Business Location"
|
||||||
|
fullWidth
|
||||||
|
focused
|
||||||
|
value={tenantLocations[0]?.locationname}
|
||||||
|
InputProps={{
|
||||||
|
style: { color: theme.palette.primary.main },
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<MyLocationIcon color="primary" />
|
||||||
|
</InputAdornment>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Autocomplete
|
||||||
|
fullWidth
|
||||||
|
options={tenantLocations || []}
|
||||||
|
getOptionLabel={(option) => `${option.locationname} (${option.suburb})`}
|
||||||
|
onChange={(event, value, reason) => {
|
||||||
|
if (value) {
|
||||||
|
setTid(value.tenantid);
|
||||||
|
setIsLocation(true);
|
||||||
|
setPickCust(value);
|
||||||
|
}
|
||||||
|
if (reason === 'clear') setIsLocation(false);
|
||||||
|
}}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Select Business Location" color="primary" fullWidth />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Date Picker */}
|
||||||
|
<Stack sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||||
|
<DatePicker
|
||||||
|
format="DD-MM-YYYY"
|
||||||
|
disablePast
|
||||||
|
value={dayjs(startdate)}
|
||||||
|
sx={{ width: 150 }}
|
||||||
|
onChange={(e) => {
|
||||||
|
let diff = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd');
|
||||||
|
|
||||||
|
if (diff <= 0) {
|
||||||
|
setStartdate(e);
|
||||||
|
|
||||||
|
let arr = [];
|
||||||
|
timeslotarr.forEach((val) => {
|
||||||
|
if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) {
|
||||||
|
arr.push(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (arr[0]) {
|
||||||
|
setOrderarr([
|
||||||
|
{
|
||||||
|
sno: 1,
|
||||||
|
address: '',
|
||||||
|
customerid: '',
|
||||||
|
deliverytime: dayjs(arr[0]),
|
||||||
|
deliverylocationid: '',
|
||||||
|
clientname: '',
|
||||||
|
contactno: '',
|
||||||
|
latitude: '',
|
||||||
|
longitude: ''
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
setOrderarr([]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
opentoast('choose Upcoming Date', 'warning');
|
||||||
|
setStartdate(NaN);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LocalizationProvider>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ===================================================== || Pickup || ===================================================== */}
|
||||||
|
{pickCust && (
|
||||||
|
<TableContainer component={Paper} sx={{ mb: 2 }}>
|
||||||
|
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Pickup Location</TableCell>
|
||||||
|
<TableCell>Address</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{pickCust?.locationname}</TableCell>
|
||||||
|
<TableCell>{pickCust?.address}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===================================================== || Drop || ===================================================== */}
|
||||||
|
|
||||||
|
<MainCard
|
||||||
|
sx={{ height: '100%' }}
|
||||||
|
title={`Drop (${dropCust?.length || 0})`}
|
||||||
|
secondary={
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: theme.palette.primary.main,
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (!isLocation) {
|
||||||
|
opentoast('Select Business Location', 'warning');
|
||||||
|
} else {
|
||||||
|
setIsCustomerOpen(true);
|
||||||
|
setSearchCustList('');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select Customers
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>S.No</TableCell>
|
||||||
|
<TableCell>Customer</TableCell>
|
||||||
|
<TableCell>Address</TableCell>
|
||||||
|
<TableCell>Kms</TableCell>
|
||||||
|
<TableCell align="right">Charge</TableCell>
|
||||||
|
<TableCell>Action</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{!dropCust && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={6}>
|
||||||
|
<Empty description={' Drop Customers Not Selected'} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{dropCust?.map((customer, index) => (
|
||||||
|
<TableRow key={index}>
|
||||||
|
<TableCell>{index + 1}</TableCell>
|
||||||
|
<TableCell>{customer.firstname}</TableCell>
|
||||||
|
<TableCell>{customer.address}</TableCell>
|
||||||
|
<TableCell>{customer.distance}</TableCell>
|
||||||
|
<TableCell align="right">{`₹${customer.totalcharge}.00`}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
{
|
||||||
|
<CloseOutlined
|
||||||
|
style={{ cursor: 'pointer', color: 'red' }}
|
||||||
|
onClick={(event) => handleCheckboxChange(event, customer)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{dropCust?.length != 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="h5">Total</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell></TableCell>
|
||||||
|
<TableCell></TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="h5">{`${totaldist} `}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Typography variant="h5"> {`₹${totalAmt}.00`}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell></TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</MainCard>
|
||||||
|
|
||||||
|
{/* ================================================= || Riders Map || ================================================= */}
|
||||||
|
|
||||||
|
{/* {showMap && dropCust.length >= 1 && <RidersPinPoint pickCust={pickCust} dropCust={dropCust} />} */}
|
||||||
|
|
||||||
|
{/* ================================================= || Notes || ================================================= */}
|
||||||
|
{dropCust && (
|
||||||
|
<MainCard sx={{ mt: 2 }} title={'Notes'}>
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TextField
|
||||||
|
focused
|
||||||
|
id="outlined-multiline-static"
|
||||||
|
sx={{ width: '100%', height: '100%', mb: 2 }}
|
||||||
|
multiline
|
||||||
|
rows={1}
|
||||||
|
placeholder="Notes"
|
||||||
|
value={otherinstructions}
|
||||||
|
onChange={(e) => setOtherinstructions(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Stack direction="row" justifyContent={'end'} sx={{ mt: 2, width: '100%' }}>
|
||||||
|
<Button
|
||||||
|
disabled={dropCust?.length == 0}
|
||||||
|
size="medium"
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => {
|
||||||
|
setLoading(true);
|
||||||
|
setBtnLoading(true);
|
||||||
|
creategrouporders();
|
||||||
|
setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
setBtnLoading(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
transition: 'transform 0.3s ease'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{btnLoading ? <CircularProgress color="primary" size={20} thickness={10} /> : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</MainCard>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ============================================= || saved address Dialog || ============================================= */}
|
||||||
|
<Dialog
|
||||||
|
open={isCustomerOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsCustomerOpen(false);
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
sx={{ minWidth: 'lg' }}
|
||||||
|
>
|
||||||
|
{isLoading && <CircularLoader />}
|
||||||
|
<DialogTitle sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
|
||||||
|
<Stack>
|
||||||
|
<Typography variant="h4"> {`Select Drop Customers (${dropCust?.length || 0})`}</Typography>
|
||||||
|
<FormControl
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
mt: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={2} sx={{ py: 0.2 }}>
|
||||||
|
<OutlinedInput
|
||||||
|
fullWidth
|
||||||
|
id="input-search-header"
|
||||||
|
placeholder="Search"
|
||||||
|
value={searchCustList}
|
||||||
|
onChange={(e) => setSearchCustList(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
'& .MuiOutlinedInput-input': {
|
||||||
|
p: '10.5px 0px 12px'
|
||||||
|
},
|
||||||
|
bgcolor: 'white'
|
||||||
|
}}
|
||||||
|
startAdornment={
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchOutlined style={{ fontSize: 'small' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
endAdornment={
|
||||||
|
<IconButton
|
||||||
|
sx={{ visibility: searchCustList ? 'visible' : 'hidden' }}
|
||||||
|
onClick={() => {
|
||||||
|
setSearchCustList('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ClearIcon />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</DialogTitle>
|
||||||
|
<Divider />
|
||||||
|
<DialogContent sx={{ p: 2.5 }}>
|
||||||
|
{customerlist.length == 0 ? (
|
||||||
|
<Stack spacing={2} direction={'row'} alignItems={'center'} justifyContent={'center'} sx={{ minHeight: 600, maxHeight: 600 }}>
|
||||||
|
<Empty />
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={2} sx={{ minHeight: 600, maxHeight: 600 }}>
|
||||||
|
{customerlist &&
|
||||||
|
customerlist.map((customer, index) => (
|
||||||
|
<FormGroup key={index}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={dropCust?.some((cust) => cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust`
|
||||||
|
onChange={(event) => handleCheckboxChange(event, customer)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<div style={{ width: '100%' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ textAlign: 'left' }}>
|
||||||
|
{`${customer.firstname} (${customer.contactno})`}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="secondary" sx={{ textAlign: 'left' }}>
|
||||||
|
{customer.address}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<Divider />
|
||||||
|
<DialogActions sx={{ p: 2.5 }}>
|
||||||
|
<Button
|
||||||
|
color={dropCust?.length !== 0 ? 'primary' : 'error'}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: dropCust?.length !== 0 ? theme.palette.primary.main : theme.palette.error.main,
|
||||||
|
color: 'white'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setIsCustomerOpen(false);
|
||||||
|
{
|
||||||
|
dropCust?.length !== 0 && setShowMap(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{dropCust?.length !== 0 ? 'Continue' : 'Close'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MultipleOrders;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
76
src/pages/nearle/reports/RiderLocationMap.js
Normal file
76
src/pages/nearle/reports/RiderLocationMap.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { Button } from '@mui/material';
|
||||||
|
import { LoadScriptNext, GoogleMap, Marker, OverlayView } from '@react-google-maps/api';
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'calc(100vh - 150px)'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RiderLocationMap({ riderLocations }) {
|
||||||
|
console.log('riderLocations', riderLocations);
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
lat: Number(riderLocations?.[0]?.latitude || 11.0056),
|
||||||
|
lng: Number(riderLocations?.[0]?.longitude || 76.9661)
|
||||||
|
};
|
||||||
|
const GreenIcon = {
|
||||||
|
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
||||||
|
scaledSize: new window.google.maps.Size(25, 41),
|
||||||
|
anchor: new window.google.maps.Point(12, 41)
|
||||||
|
};
|
||||||
|
|
||||||
|
const RedIcon = {
|
||||||
|
url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
||||||
|
scaledSize: new window.google.maps.Size(25, 41),
|
||||||
|
anchor: new window.google.maps.Point(12, 41)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LoadScriptNext googleMapsApiKey={process.env.REACT_APP_GOOGLE_MAPS_API_KEY}>
|
||||||
|
<GoogleMap mapContainerStyle={containerStyle} zoom={12} center={center}>
|
||||||
|
{riderLocations &&
|
||||||
|
riderLocations?.map((r, index) => {
|
||||||
|
const lat = Number(r.latitude);
|
||||||
|
const lng = Number(r.longitude);
|
||||||
|
return (
|
||||||
|
<div key={index}>
|
||||||
|
{/* Marker */}
|
||||||
|
<Marker
|
||||||
|
position={{ lat, lng }}
|
||||||
|
icon={r.status == 'active' ? GreenIcon : RedIcon}
|
||||||
|
label={{
|
||||||
|
fontSize: '14px',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<OverlayView position={{ lat, lng }} mapPaneName={OverlayView.OVERLAY_LAYER}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
color: 'green',
|
||||||
|
padding: '2px 8px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 600,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
transform: 'translate(-50%, -140%)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
ml: 20
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button variant="contained" color="primary" size="small">
|
||||||
|
{` ${r.username} `}
|
||||||
|
{/* <br /> */}
|
||||||
|
{/* {`${r.contactno || '##### ##### '} `} */}
|
||||||
|
<br />
|
||||||
|
{`(${r.orderid || ''}) `}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</OverlayView>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</GoogleMap>
|
||||||
|
</LoadScriptNext>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,209 +1,703 @@
|
|||||||
import * as React from 'react';
|
import React, { useState, useEffect, useRef, Fragment } from 'react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|
||||||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
|
||||||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
// material-ui
|
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
|
AppBar,
|
||||||
|
Backdrop,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
Divider,
|
Divider,
|
||||||
Table,
|
Drawer,
|
||||||
TableBody,
|
IconButton,
|
||||||
TableCell,
|
InputBase,
|
||||||
TableContainer,
|
List,
|
||||||
TableHead,
|
ListItem,
|
||||||
TableRow,
|
Paper,
|
||||||
FormControl,
|
Skeleton,
|
||||||
OutlinedInput,
|
Stack,
|
||||||
InputAdornment,
|
Toolbar,
|
||||||
Chip,
|
Typography,
|
||||||
Stack
|
useMediaQuery,
|
||||||
|
useTheme
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
var utc = require('dayjs/plugin/utc');
|
|
||||||
dayjs.extend(utc);
|
|
||||||
import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
|
|
||||||
import Loader from 'components/Loader';
|
|
||||||
// project imports
|
|
||||||
import MainCard from 'components/MainCard';
|
|
||||||
import { Empty } from 'antd';
|
|
||||||
import TitleCard from '../titleCard';
|
|
||||||
import { fetchAppLocations, fetchRidersLogs } from '../api/api';
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
import {
|
||||||
const date = dayjs(dateString);
|
MdMenu,
|
||||||
const formattedDate = date.format('DD-MM-YYYY ');
|
MdSearch,
|
||||||
return formattedDate;
|
MdClear,
|
||||||
}
|
MdRefresh,
|
||||||
|
MdLocalShipping,
|
||||||
|
MdCheckCircle,
|
||||||
|
MdHighlightOff,
|
||||||
|
MdGroups,
|
||||||
|
MdAccessTime,
|
||||||
|
MdLocationOn,
|
||||||
|
MdMyLocation
|
||||||
|
} from 'react-icons/md';
|
||||||
|
|
||||||
const formatTime = (timeString) =>
|
import RiderLocationMap from './RiderLocationMap';
|
||||||
new Date('2024-01-01T' + timeString + 'Z').toLocaleTimeString('en-US', {
|
import { fetchRidersLogs } from '../api/api';
|
||||||
timeZone: 'UTC',
|
import CircularLoader from 'components/nearle_components/CircularLoader';
|
||||||
hour12: true,
|
import error500 from 'assets/images/maintenance/Error500.png';
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==============================|| RidersLogs ||============================== //
|
// ============================================================================
|
||||||
|
// Design tokens — shared with the rest of the redesigned operator pages.
|
||||||
|
// ============================================================================
|
||||||
|
const DT = {
|
||||||
|
radiusPill: 999,
|
||||||
|
radiusCard: 16,
|
||||||
|
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||||
|
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||||
|
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||||
|
textPrimary: '#0f172a',
|
||||||
|
textSecondary: '#64748b',
|
||||||
|
textMuted: '#94a3b8',
|
||||||
|
borderSubtle: '#e2e8f0',
|
||||||
|
divider: '#f1f5f9',
|
||||||
|
surface: '#ffffff',
|
||||||
|
surfaceAlt: '#f8fafc'
|
||||||
|
};
|
||||||
|
const dtA = (c, suffix) => `${c}${suffix}`;
|
||||||
|
const tint = (c) => dtA(c, '08');
|
||||||
|
const soft = (c) => dtA(c, '18');
|
||||||
|
const ring = (c) => dtA(c, '26');
|
||||||
|
const edge = (c) => dtA(c, '55');
|
||||||
|
|
||||||
export default function RidersLogs() {
|
const BRAND = '#662582';
|
||||||
const tenantid = localStorage.getItem('tenantid');
|
const BRAND_LIGHT = '#9255AB';
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
const C_ACTIVE = '#10b981';
|
||||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
const C_INACTIVE = '#ef4444';
|
||||||
const [searchword, setSearchword] = useState('');
|
|
||||||
const [showClose, SetShowClose] = useState(false);
|
const drawerWidth = 360;
|
||||||
|
|
||||||
|
// Soft pill used for status / count chips throughout the page.
|
||||||
|
const SoftPill = ({ color, icon, children, sx = {} }) => (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
px: 0.875,
|
||||||
|
py: 0.25,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: tint(color),
|
||||||
|
border: `1px solid ${edge(color)}`,
|
||||||
|
color,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 800,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
...sx
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const RidersLogs = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isDesktop = useMediaQuery('(min-width:900px)');
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [selectedRiders, setSelectedRiders] = useState([]);
|
||||||
|
const [riderSearch, setRiderSearch] = useState('');
|
||||||
|
const searchRef = useRef(null);
|
||||||
|
const appId = 1;
|
||||||
|
|
||||||
/* ============================================= || fetchRidersLogs| ============================================= */
|
|
||||||
const {
|
const {
|
||||||
data: rows = [], // Default to empty array
|
data: riders,
|
||||||
isLoading: IsRiderLogsLoading,
|
isLoading: ridersIsLoading,
|
||||||
isError: IsRiderLogsError,
|
isFetching: riderIsFetching,
|
||||||
error: RiderLogsError
|
refetch: riderLogsRefetch,
|
||||||
|
error: riderLogsError
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: [tenantid, startdate], // Meaningful query key
|
queryKey: [appId, dayjs().format('YYYY-MM-DD'), riderSearch],
|
||||||
queryFn: fetchRidersLogs,
|
queryFn: fetchRidersLogs,
|
||||||
enabled: !!tenantid && !!startdate, // Fetch only if appId & startdate exist
|
refetchInterval: 5 * 60 * 1000
|
||||||
refetchInterval: 300000 // Auto-fetch every 5 minutes
|
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
// Only active riders are surfaced on this page — inactive riders are filtered
|
||||||
setRowsPerPage(rows?.length + 1);
|
// out entirely before they reach the list, selection, counts, or map.
|
||||||
}, [rows]);
|
const activeRiders = React.useMemo(
|
||||||
{
|
() => (riders || []).filter((r) => r.status === 'active'),
|
||||||
IsRiderLogsError && console.log('RiderLogsError', RiderLogsError);
|
[riders]
|
||||||
}
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedRiders(activeRiders);
|
||||||
|
}, [activeRiders]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(isDesktop);
|
||||||
|
}, [isDesktop]);
|
||||||
|
|
||||||
|
// Ctrl/Cmd+K focuses the rider search.
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (event) => {
|
||||||
|
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||||
|
event.preventDefault();
|
||||||
|
searchRef.current && searchRef.current.focus();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape' && document.activeElement === searchRef.current) {
|
||||||
|
searchRef.current.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Counts shown in the drawer header (active riders only).
|
||||||
|
const totalRiders = activeRiders.length;
|
||||||
|
const activeCount = totalRiders;
|
||||||
|
const isAllSelected = totalRiders > 0 && selectedRiders?.length === totalRiders;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Fragment>
|
||||||
{IsRiderLogsLoading && <Loader />}
|
<Backdrop
|
||||||
<TitleCard title="Riders Logs" />
|
sx={{
|
||||||
<MainCard
|
color: '#fff',
|
||||||
content={false}
|
zIndex: (t) => t.zIndex.drawer + 1
|
||||||
title={
|
}}
|
||||||
<Stack display={'flex'} flexDirection={'row'} alignItems={'center'} justifyContent={'space-between'} flexWrap={'wrap'} gap={1}>
|
open={ridersIsLoading || riderIsFetching}
|
||||||
<Stack>
|
>
|
||||||
<FormControl sx={{ width: 250 }}>
|
<CircularLoader color="inherit" />
|
||||||
<OutlinedInput
|
</Backdrop>
|
||||||
sx={{ background: 'white' }}
|
|
||||||
size="medium"
|
<Paper
|
||||||
id="header-search"
|
elevation={0}
|
||||||
startAdornment={
|
sx={{
|
||||||
<InputAdornment position="start" sx={{ mr: -0.5 }}>
|
borderRadius: DT.radiusCard / 8,
|
||||||
<SearchOutlined />
|
border: '1px solid',
|
||||||
</InputAdornment>
|
borderColor: DT.borderSubtle,
|
||||||
|
overflow: 'hidden',
|
||||||
|
background: '#fff'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
|
||||||
|
{/* ============================================= || Drawer || ============================================= */}
|
||||||
|
<Drawer
|
||||||
|
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||||
|
open={open}
|
||||||
|
onClose={() => !isDesktop && setOpen(false)}
|
||||||
|
ModalProps={{ keepMounted: true }}
|
||||||
|
sx={{
|
||||||
|
'& .MuiDrawer-paper': {
|
||||||
|
width: drawerWidth,
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
height: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
transition: 'transform 0.35s ease-in-out',
|
||||||
|
zIndex: 13,
|
||||||
|
borderRight: `1px solid ${DT.borderSubtle}`,
|
||||||
|
backgroundColor: '#fff'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* ===== Drawer header — gradient strip with title + count pills ===== */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 5,
|
||||||
|
// Solid white base UNDER the translucent brand gradient so the
|
||||||
|
// scrolling rider list never shows through the sticky header.
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
backgroundImage: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
p: 1.75
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.25} sx={{ mb: 1.25 }}>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
bgcolor: BRAND,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdGroups size={20} />
|
||||||
|
</Avatar>
|
||||||
|
<Stack sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 800,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
fontSize: { xs: '1rem', sm: '1.1rem' },
|
||||||
|
lineHeight: 1.1
|
||||||
|
}}
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
Riders
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.25 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: C_ACTIVE,
|
||||||
|
boxShadow: `0 0 0 3px ${ring(C_ACTIVE)}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
|
Updated · {dayjs().format('hh:mm A')}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Count pills */}
|
||||||
|
<Stack direction="row" spacing={0.75} flexWrap="wrap" useFlexGap sx={{ mb: 1.25 }}>
|
||||||
|
<SoftPill color={BRAND} icon={<MdGroups size={11} />}>
|
||||||
|
Total · {totalRiders}
|
||||||
|
</SoftPill>
|
||||||
|
<SoftPill color={C_ACTIVE} icon={<MdCheckCircle size={11} />}>
|
||||||
|
Active · {activeCount}
|
||||||
|
</SoftPill>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* Pill search */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.75,
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.5,
|
||||||
|
borderRadius: 999,
|
||||||
|
bgcolor: '#fff',
|
||||||
|
border: `1.5px solid ${edge(BRAND)}`,
|
||||||
|
transition: 'all 0.18s',
|
||||||
|
'&:focus-within': {
|
||||||
|
borderColor: BRAND,
|
||||||
|
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||||||
}
|
}
|
||||||
endAdornment={
|
}}
|
||||||
showClose && (
|
>
|
||||||
<InputAdornment position="end" sx={{ mr: -0.5 }}>
|
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
||||||
<CloseOutlined
|
<InputBase
|
||||||
onClick={() => {
|
inputRef={searchRef}
|
||||||
setSearchword('');
|
placeholder="Search rider (ctrl+k)"
|
||||||
SetShowClose(false);
|
value={riderSearch}
|
||||||
|
onChange={(e) => setRiderSearch(e.target.value)}
|
||||||
|
autoComplete="off"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{riderSearch && (
|
||||||
|
<IconButton size="small" onClick={() => setRiderSearch('')} sx={{ p: 0.25, color: BRAND }}>
|
||||||
|
<MdClear size={14} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ===== "All" selection pill ===== */}
|
||||||
|
<Box sx={{ p: 1.25, pb: 0.5 }}>
|
||||||
|
<Box
|
||||||
|
onClick={() => {
|
||||||
|
if (!isAllSelected) {
|
||||||
|
setSelectedRiders(activeRiders);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
px: 1,
|
||||||
|
py: 0.75,
|
||||||
|
borderRadius: 999,
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: `1.5px solid ${isAllSelected ? BRAND : edge(BRAND)}`,
|
||||||
|
bgcolor: isAllSelected ? BRAND : tint(BRAND),
|
||||||
|
color: isAllSelected ? '#fff' : BRAND,
|
||||||
|
transition: 'all 0.18s',
|
||||||
|
boxShadow: isAllSelected ? `0 6px 18px ${ring(BRAND)}` : 'none',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: BRAND,
|
||||||
|
boxShadow: `0 0 0 3px ${ring(BRAND)}`
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={isAllSelected}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedRiders(activeRiders);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
color: isAllSelected ? '#fff' : BRAND,
|
||||||
|
'&.Mui-checked': { color: '#fff' }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: 12.5 }}>
|
||||||
|
Show All Riders
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
ml: 'auto',
|
||||||
|
minWidth: 24,
|
||||||
|
height: 20,
|
||||||
|
px: 0.625,
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 999,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 800,
|
||||||
|
bgcolor: isAllSelected ? 'rgba(255,255,255,0.22)' : '#fff',
|
||||||
|
color: isAllSelected ? '#fff' : BRAND,
|
||||||
|
border: isAllSelected ? 'none' : `1px solid ${edge(BRAND)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{totalRiders}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* ===== Rider list ===== */}
|
||||||
|
<List sx={{ px: 1, py: 0.5 }}>
|
||||||
|
{ridersIsLoading || riderIsFetching
|
||||||
|
? Array.from({ length: 8 }).map((_, index) => (
|
||||||
|
<ListItem key={index} sx={{ px: 1, py: 1 }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ width: '100%' }}>
|
||||||
|
<Skeleton variant="circular" width={32} height={32} />
|
||||||
|
<Stack sx={{ flex: 1 }}>
|
||||||
|
<Skeleton variant="text" width="60%" height={18} />
|
||||||
|
<Skeleton variant="text" width="40%" height={14} />
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={0.5}>
|
||||||
|
<Skeleton variant="text" width={60} height={16} />
|
||||||
|
<Skeleton variant="text" width={80} height={12} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
: activeRiders.map((row) => {
|
||||||
|
const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
|
||||||
|
const statusColor = row.status === 'active' ? C_ACTIVE : C_INACTIVE;
|
||||||
|
const initial = (row.firstname || row.username || '?').charAt(0).toUpperCase();
|
||||||
|
const name =
|
||||||
|
row.username && row.username.length > 0
|
||||||
|
? `${row.username.slice(0, 25)}${row.username.length > 25 ? '…' : ''}`
|
||||||
|
: `${row.firstname || ''}${row.lastname ? ` ${row.lastname}` : ''}`.trim() || 'Rider';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={row.userid}
|
||||||
|
onClick={() => {
|
||||||
|
// Tapping the card selects this rider in isolation.
|
||||||
|
setSelectedRiders([row]);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 1,
|
||||||
|
p: 1,
|
||||||
|
mb: 0.625,
|
||||||
|
borderRadius: 2,
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: `1px solid ${isSelected ? edge(BRAND) : DT.divider}`,
|
||||||
|
bgcolor: isSelected ? tint(BRAND) : '#fff',
|
||||||
|
transition: 'all 0.15s',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: edge(BRAND),
|
||||||
|
bgcolor: isSelected ? soft(BRAND) : DT.surfaceAlt,
|
||||||
|
boxShadow: DT.shadowSoft
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={isSelected}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.checked) {
|
||||||
|
setSelectedRiders([row]);
|
||||||
|
} else {
|
||||||
|
setSelectedRiders(activeRiders);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
p: 0.5,
|
||||||
|
color: edge(BRAND),
|
||||||
|
'&.Mui-checked': { color: BRAND }
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
aria-describedby="header-search-text"
|
|
||||||
inputProps={{
|
|
||||||
'aria-label': 'weight'
|
|
||||||
}}
|
|
||||||
placeholder="Search"
|
|
||||||
value={searchword}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchword(e.target.value);
|
|
||||||
if (e.target.value == '') {
|
|
||||||
SetShowClose(false);
|
|
||||||
} else {
|
|
||||||
SetShowClose(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</FormControl>{' '}
|
|
||||||
</Stack>
|
|
||||||
<Stack flexDirection="row" alignItems="center" gap={2}>
|
|
||||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
|
||||||
<DatePicker
|
|
||||||
label="Choose Date"
|
|
||||||
value={dayjs(startdate)}
|
|
||||||
format="DD-MM-YYYY"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (e) {
|
|
||||||
setStartdate(dayjs(e.$d).format('YYYY-MM-DD'));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</LocalizationProvider>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell>S.No</TableCell>
|
|
||||||
<TableCell>ID</TableCell>
|
|
||||||
<TableCell>Rider</TableCell>
|
|
||||||
<TableCell>LogDate</TableCell>
|
|
||||||
<TableCell>Shift(HRS)</TableCell>
|
|
||||||
<TableCell>Login</TableCell>
|
|
||||||
<TableCell>Logout</TableCell>
|
|
||||||
<TableCell>WRK(HRS)</TableCell>
|
|
||||||
<TableCell>Shift(HRS)</TableCell>
|
|
||||||
<TableCell>BRK(HRS)</TableCell>
|
|
||||||
<TableCell>Status</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{rows.length == 0 ? (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={11}>
|
|
||||||
<Empty />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
) : (
|
|
||||||
rows.map((row, index) => (
|
|
||||||
<TableRow key={index + 1}>
|
|
||||||
<TableCell align="left">{index + 1}</TableCell>
|
|
||||||
<TableCell align="left">{row.userid}</TableCell>
|
|
||||||
<TableCell align="left">{row.username}</TableCell>
|
|
||||||
<TableCell align="left">
|
|
||||||
{' '}
|
|
||||||
<Chip label={formatDate(row.logdate)} color="warning" variant="outlined" size="small" sx={{ bgcolor: '#fffde7' }} />
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell align="left">{row.shifthours}</TableCell>
|
<Avatar
|
||||||
<TableCell align="left">
|
sx={{
|
||||||
{row.login != '' && (
|
width: 36,
|
||||||
<Chip label={formatTime(row.login)} color="info" variant="outlined" size="small" sx={{ bgcolor: '#e0f7fa' }} />
|
height: 36,
|
||||||
)}
|
bgcolor: soft(statusColor),
|
||||||
</TableCell>
|
color: statusColor,
|
||||||
<TableCell align="left">
|
fontWeight: 800,
|
||||||
{row.logout != '' && (
|
fontSize: 14,
|
||||||
<Chip label={formatTime(row.logout)} color="info" variant="outlined" size="small" sx={{ bgcolor: '#e0f7fa' }} />
|
position: 'relative'
|
||||||
)}
|
}}
|
||||||
</TableCell>
|
>
|
||||||
|
{initial}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: -1,
|
||||||
|
right: -1,
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: statusColor,
|
||||||
|
border: '2px solid #fff'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
<TableCell align="left">{row.workhours}</TableCell>
|
<Stack sx={{ flex: 1, minWidth: 0 }}>
|
||||||
<TableCell align="left">{row.shorthours}</TableCell>
|
<Typography
|
||||||
<TableCell align="left">{row.breakhours}</TableCell>
|
sx={{
|
||||||
<TableCell align="left">
|
fontWeight: 700,
|
||||||
{row.logstatus == 0 ? (
|
color: DT.textPrimary,
|
||||||
<Chip label="Active" color="success" variant="outlined" size="small" sx={{ bgcolor: '#e8f5e9' }} />
|
fontSize: 13,
|
||||||
) : (
|
lineHeight: 1.15
|
||||||
<Chip label="Inactive" color="error" variant="outlined" size="small" sx={{ bgcolor: '#fce4ec' }} />
|
}}
|
||||||
)}
|
noWrap
|
||||||
</TableCell>
|
>
|
||||||
</TableRow>
|
{name}
|
||||||
))
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.25 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontSize: 11 }} noWrap>
|
||||||
|
{row.contactno || '##########'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack alignItems="flex-end" spacing={0.5} sx={{ minWidth: 0 }}>
|
||||||
|
<SoftPill
|
||||||
|
color={statusColor}
|
||||||
|
icon={row.status === 'active' ? <MdCheckCircle size={10} /> : <MdHighlightOff size={10} />}
|
||||||
|
sx={{ fontSize: 10 }}
|
||||||
|
>
|
||||||
|
{row.status === 'active' ? 'Active' : 'Inactive'}
|
||||||
|
</SoftPill>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.375}>
|
||||||
|
<MdAccessTime size={10} style={{ color: DT.textMuted }} />
|
||||||
|
<Typography sx={{ fontSize: 10, color: DT.textMuted, fontWeight: 700 }} noWrap>
|
||||||
|
{row.logdate ? dayjs(row.logdate).format('DD/MM · hh:mm A') : '—'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{!ridersIsLoading && !riderIsFetching && totalRiders === 0 && (
|
||||||
|
<Stack alignItems="center" spacing={1.25} sx={{ py: 6, px: 2 }}>
|
||||||
|
<Avatar sx={{ width: 56, height: 56, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||||
|
<MdGroups size={24} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||||
|
No riders to show
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, textAlign: 'center' }}>
|
||||||
|
{riderSearch ? 'Try a different rider name.' : 'Pull-to-refresh once you have rider activity.'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</List>
|
||||||
</Table>
|
</Drawer>
|
||||||
</TableContainer>
|
|
||||||
</MainCard>
|
{/* ============================================= || AppBar || ============================================= */}
|
||||||
</>
|
<AppBar
|
||||||
|
elevation={0}
|
||||||
|
position="absolute"
|
||||||
|
sx={{
|
||||||
|
top: 0,
|
||||||
|
left: open && isDesktop ? `${drawerWidth}px` : 0,
|
||||||
|
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
|
||||||
|
transition: 'left 0.3s ease, width 0.3s ease',
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
color: DT.textPrimary
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar sx={{ minHeight: { xs: 60, md: 68 } }}>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
sx={{
|
||||||
|
bgcolor: '#fff',
|
||||||
|
border: `1px solid ${DT.borderSubtle}`,
|
||||||
|
borderRadius: 999,
|
||||||
|
color: BRAND,
|
||||||
|
'&:hover': { bgcolor: tint(BRAND), borderColor: edge(BRAND) }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdMenu size={18} />
|
||||||
|
</IconButton>
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
bgcolor: BRAND,
|
||||||
|
color: '#fff',
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdLocationOn size={18} />
|
||||||
|
</Avatar>
|
||||||
|
<Stack>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: 800,
|
||||||
|
color: DT.textPrimary,
|
||||||
|
lineHeight: 1.1,
|
||||||
|
fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.25rem' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Riders Locations
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.25 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 7,
|
||||||
|
height: 7,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: C_ACTIVE,
|
||||||
|
boxShadow: `0 0 0 3px ${ring(C_ACTIVE)}`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
|
Live · {selectedRiders?.length || 0} of {totalRiders} on map
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<SoftPill color={BRAND} icon={<MdMyLocation size={11} />} sx={{ display: { xs: 'none', sm: 'inline-flex' } }}>
|
||||||
|
{dayjs().format('DD MMM YYYY')}
|
||||||
|
</SoftPill>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => riderLogsRefetch()}
|
||||||
|
startIcon={<MdRefresh size={16} />}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
px: 2,
|
||||||
|
bgcolor: BRAND,
|
||||||
|
fontWeight: 700,
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`,
|
||||||
|
textTransform: 'none',
|
||||||
|
'&:hover': { bgcolor: '#4D1C61' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
{/* ============================================= || Map area || ============================================= */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
pt: { xs: '60px', md: '68px' },
|
||||||
|
pl: open && isDesktop ? `${drawerWidth}px` : 0,
|
||||||
|
transition: 'padding-left 0.3s ease',
|
||||||
|
minHeight: '80vh',
|
||||||
|
bgcolor: DT.surfaceAlt
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(ridersIsLoading || riderIsFetching) && (
|
||||||
|
<Box position="relative" width="100%" height="80vh" display="grid" placeItems="center">
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
animation="wave"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
borderRadius: 0,
|
||||||
|
zIndex: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedRiders?.length > 0 && !riderLogsError && <RiderLocationMap riderLocations={selectedRiders} />}
|
||||||
|
|
||||||
|
{riderLogsError && (
|
||||||
|
<Stack alignItems="center" justifyContent="center" sx={{ p: 4, height: '70vh' }}>
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: 3,
|
||||||
|
borderRadius: DT.radiusCard / 8,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: edge(C_INACTIVE),
|
||||||
|
bgcolor: tint(C_INACTIVE),
|
||||||
|
maxWidth: 520,
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar sx={{ bgcolor: C_INACTIVE, color: '#fff', width: 56, height: 56, mx: 'auto', mb: 1.5 }}>
|
||||||
|
<MdHighlightOff size={26} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 800, color: DT.textPrimary, mb: 0.5 }}>
|
||||||
|
Couldn’t load rider logs
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
||||||
|
The map is unavailable right now. Try refreshing in a moment.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => riderLogsRefetch()}
|
||||||
|
startIcon={<MdRefresh size={16} />}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 999,
|
||||||
|
px: 3,
|
||||||
|
bgcolor: BRAND,
|
||||||
|
fontWeight: 700,
|
||||||
|
boxShadow: `0 6px 18px ${ring(BRAND)}`,
|
||||||
|
textTransform: 'none',
|
||||||
|
'&:hover': { bgcolor: '#4D1C61' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={error500}
|
||||||
|
alt="error"
|
||||||
|
sx={{ mt: 2, maxWidth: 240, opacity: 0.6, filter: 'grayscale(0.2)' }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default RidersLogs;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,9 @@ const RiderLogs = Loadable(lazy(() => import('pages/nearle/reports/riderLogs')))
|
|||||||
const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice')));
|
const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice')));
|
||||||
const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview')));
|
const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview')));
|
||||||
|
|
||||||
|
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
|
||||||
|
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
||||||
|
|
||||||
// ==============================|| MAIN ROUTING ||============================== //
|
// ==============================|| MAIN ROUTING ||============================== //
|
||||||
|
|
||||||
const MainRoutes = {
|
const MainRoutes = {
|
||||||
@@ -123,6 +126,19 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: 'locations',
|
path: 'locations',
|
||||||
element: <Locations />
|
element: <Locations />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dispatch',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
element: <Dispatch />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'preview',
|
||||||
|
element: <DispatchPreview />
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const initialState = {
|
|||||||
openItem: ['dashboard'],
|
openItem: ['dashboard'],
|
||||||
openComponent: 'buttons',
|
openComponent: 'buttons',
|
||||||
selectedID: null,
|
selectedID: null,
|
||||||
|
selectedMenu: null,
|
||||||
drawerOpen: false,
|
drawerOpen: false,
|
||||||
componentDrawerOpen: true,
|
componentDrawerOpen: true,
|
||||||
menu: {},
|
menu: {},
|
||||||
@@ -48,6 +49,10 @@ const menu = createSlice({
|
|||||||
|
|
||||||
hasError(state, action) {
|
hasError(state, action) {
|
||||||
state.error = action.payload;
|
state.error = action.payload;
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedMenu(state, action) {
|
||||||
|
state.selectedMenu = action.payload;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,4 +65,4 @@ const menu = createSlice({
|
|||||||
|
|
||||||
export default menu.reducer;
|
export default menu.reducer;
|
||||||
|
|
||||||
export const { activeItem, activeComponent, openDrawer, openComponentDrawer, activeID } = menu.actions;
|
export const { activeItem, activeComponent, openDrawer, openComponentDrawer, activeID, setSelectedMenu } = menu.actions;
|
||||||
|
|||||||
154
src/utils/leafletPolylineOffset.js
Normal file
154
src/utils/leafletPolylineOffset.js
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// Vendored from leaflet-polylineoffset@1.1.1 (MIT).
|
||||||
|
//
|
||||||
|
// Why this lives in-tree instead of being an npm dep:
|
||||||
|
// • The published package would require --legacy-peer-deps because of an
|
||||||
|
// unrelated React-17 peer-dep conflict elsewhere in the project, and we
|
||||||
|
// don't want a renderer plugin to force a global resolver flag.
|
||||||
|
// • It's frozen upstream (no meaningful updates since 2020), tiny, and
|
||||||
|
// has zero runtime deps besides leaflet (already in package.json).
|
||||||
|
//
|
||||||
|
// What it does:
|
||||||
|
// Monkey-patches L.Polyline so that any path passed with a numeric
|
||||||
|
// `offset` in pathOptions is rendered shifted perpendicular to its
|
||||||
|
// direction of travel by that many pixels (positive = right of travel,
|
||||||
|
// negative = left). Used by Dispatch.js's Compare → Combined view to
|
||||||
|
// render planned + actual as parallel rails when they share the same
|
||||||
|
// road geometry; without this they overlap and read as one polyline.
|
||||||
|
//
|
||||||
|
// Import once for the side effect:
|
||||||
|
// import '../../../utils/leafletPolylineOffset';
|
||||||
|
//
|
||||||
|
// Then add to any pathOptions:
|
||||||
|
// pathOptions={{ ..., offset: 5 }}
|
||||||
|
//
|
||||||
|
// Plays nicely with both SVG and Canvas renderers.
|
||||||
|
|
||||||
|
import L from 'leaflet';
|
||||||
|
|
||||||
|
L.PolylineOffset = {
|
||||||
|
translatePoint(pt, dist, radians) {
|
||||||
|
return L.point(pt.x + dist * Math.cos(radians), pt.y + dist * Math.sin(radians));
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetPointLine(points, distance) {
|
||||||
|
const l = points.length;
|
||||||
|
if (l < 2) {
|
||||||
|
throw new Error('Line should be defined by at least 2 points');
|
||||||
|
}
|
||||||
|
let a = points[0];
|
||||||
|
let b;
|
||||||
|
const offsetAngle = Math.PI / 2;
|
||||||
|
const offsetSegments = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < l; i++) {
|
||||||
|
b = points[i];
|
||||||
|
// Each segment's offset angle is perpendicular to its direction.
|
||||||
|
const segAngle = Math.atan2(b.y - a.y, b.x - a.x);
|
||||||
|
offsetSegments.push({
|
||||||
|
offsetAngle: segAngle - offsetAngle,
|
||||||
|
original: [a, b],
|
||||||
|
offset: [
|
||||||
|
this.translatePoint(a, distance, segAngle - offsetAngle),
|
||||||
|
this.translatePoint(b, distance, segAngle - offsetAngle)
|
||||||
|
]
|
||||||
|
});
|
||||||
|
a = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
return offsetSegments;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Find the intersection of two segments by extending them to infinity
|
||||||
|
// along their direction, then walking along segment 1 by parameter t.
|
||||||
|
// Returns null when the segments are parallel (no intersection).
|
||||||
|
intersection(l1a, l1b, l2a, l2b) {
|
||||||
|
const line1 = this.segmentAsVector(l1a, l1b);
|
||||||
|
const line2 = this.segmentAsVector(l2a, l2b);
|
||||||
|
|
||||||
|
const denom = -line2.x * line1.y + line1.x * line2.y;
|
||||||
|
if (denom === 0) return null;
|
||||||
|
|
||||||
|
const s = (-line1.y * (l1a.x - l2a.x) + line1.x * (l1a.y - l2a.y)) / denom;
|
||||||
|
const t = (line2.x * (l1a.y - l2a.y) - line2.y * (l1a.x - l2a.x)) / denom;
|
||||||
|
|
||||||
|
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
|
||||||
|
return L.point(l1a.x + t * line1.x, l1a.y + t * line1.y);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
segmentAsVector(a, b) {
|
||||||
|
return L.point(b.x - a.x, b.y - a.y);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Walk the offset segments and join adjacent ones at their intersection
|
||||||
|
// points (mitered corners). When two consecutive segments don't intersect
|
||||||
|
// within their bounds (sharp turn, or co-linear), fall back to the offset
|
||||||
|
// endpoint so the polyline doesn't gap.
|
||||||
|
joinLineSegments(segments) {
|
||||||
|
const joined = [];
|
||||||
|
let last = segments[0].offset;
|
||||||
|
joined.push(last[0]);
|
||||||
|
|
||||||
|
for (let i = 1; i < segments.length; i++) {
|
||||||
|
const next = segments[i].offset;
|
||||||
|
const inter = this.intersection(last[0], last[1], next[0], next[1]);
|
||||||
|
if (inter) {
|
||||||
|
joined.push(inter);
|
||||||
|
} else {
|
||||||
|
joined.push(last[1]);
|
||||||
|
}
|
||||||
|
last = next;
|
||||||
|
}
|
||||||
|
joined.push(last[1]);
|
||||||
|
return joined;
|
||||||
|
},
|
||||||
|
|
||||||
|
offsetPoints(points, offset) {
|
||||||
|
if (!points || points.length < 2) return points;
|
||||||
|
const offsets = this.offsetPointLine(points, offset);
|
||||||
|
return this.joinLineSegments(offsets);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Operates on a ring of LatLngs by projecting → offsetting → unprojecting,
|
||||||
|
// since leaflet polyline math is in screen pixels but our points are LatLng.
|
||||||
|
offsetLatLngs(map, latlngs, offset) {
|
||||||
|
const points = latlngs.map((ll) => map.latLngToLayerPoint(ll));
|
||||||
|
const offsetPts = this.offsetPoints(points, offset);
|
||||||
|
return offsetPts.map((p) => map.layerPointToLatLng(p));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Patch Polyline._projectLatlngs (used by both SVG and Canvas renderers) so
|
||||||
|
// that when an offset is set, the projected ring is offset before clipping.
|
||||||
|
// We keep the original on _projectLatlngsOriginal so we can call through.
|
||||||
|
const originalProject = L.Polyline.prototype._projectLatlngs;
|
||||||
|
|
||||||
|
L.Polyline.prototype._projectLatlngs = function patchedProject(latlngs, result, projectedBounds) {
|
||||||
|
const offset = this.options.offset;
|
||||||
|
if (!offset || typeof offset !== 'number') {
|
||||||
|
return originalProject.call(this, latlngs, result, projectedBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse for multi-ring polylines (shouldn't happen for simple lines,
|
||||||
|
// but the leaflet API allows it).
|
||||||
|
const flat = latlngs[0] instanceof L.LatLng;
|
||||||
|
if (!flat) {
|
||||||
|
for (let i = 0; i < latlngs.length; i++) {
|
||||||
|
this._projectLatlngs(latlngs[i], result, projectedBounds);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projected = latlngs.map((ll) => this._map.latLngToLayerPoint(ll));
|
||||||
|
const offsetted = L.PolylineOffset.offsetPoints(projected, offset);
|
||||||
|
// Update projectedBounds with each offset point so the renderer's
|
||||||
|
// viewport-clipping check still works.
|
||||||
|
for (let i = 0; i < offsetted.length; i++) {
|
||||||
|
projectedBounds.extend(offsetted[i]);
|
||||||
|
}
|
||||||
|
result.push(offsetted);
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default L;
|
||||||
66
src/utils/logger.js
Normal file
66
src/utils/logger.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const LOG_LEVELS = {
|
||||||
|
DEBUG: 0,
|
||||||
|
INFO: 1,
|
||||||
|
WARN: 2,
|
||||||
|
ERROR: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default log level based on environment
|
||||||
|
const currentEnv = process.env.NODE_ENV || 'development';
|
||||||
|
const isDev = currentEnv === 'development';
|
||||||
|
const GLOBAL_LOG_LEVEL = isDev ? LOG_LEVELS.DEBUG : LOG_LEVELS.WARN;
|
||||||
|
|
||||||
|
const style = (bg, color) => `background: ${bg}; color: ${color}; padding: 2px 5px; border-radius: 4px; font-weight: bold;`;
|
||||||
|
|
||||||
|
const PREFIX = '%c[NearlExpress]';
|
||||||
|
const PREFIX_STYLE = style('#2563eb', '#ffffff');
|
||||||
|
|
||||||
|
// Capture original console methods before any global overrides occur
|
||||||
|
const originalLog = console.log;
|
||||||
|
const originalWarn = console.warn || console.log;
|
||||||
|
const originalError = console.error || console.log;
|
||||||
|
|
||||||
|
const print = (levelName, args, labelStyle) => {
|
||||||
|
const levelValue = LOG_LEVELS[levelName];
|
||||||
|
if (levelValue < GLOBAL_LOG_LEVEL) return;
|
||||||
|
|
||||||
|
const [message, ...extra] = args;
|
||||||
|
const isMessageString = typeof message === 'string';
|
||||||
|
|
||||||
|
const formatPrefix = `${PREFIX}%c ${levelName}`;
|
||||||
|
const styles = [PREFIX_STYLE, labelStyle];
|
||||||
|
|
||||||
|
const consoleMethod =
|
||||||
|
levelName === 'ERROR' ? originalError :
|
||||||
|
levelName === 'WARN' ? originalWarn :
|
||||||
|
originalLog;
|
||||||
|
|
||||||
|
if (isMessageString) {
|
||||||
|
consoleMethod(
|
||||||
|
`${formatPrefix}%c ${message}`,
|
||||||
|
...styles,
|
||||||
|
'color: inherit;',
|
||||||
|
...extra
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If first argument is an object/array, preserve raw interactive log
|
||||||
|
consoleMethod(
|
||||||
|
`${formatPrefix}`,
|
||||||
|
...styles,
|
||||||
|
message,
|
||||||
|
...extra
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = {
|
||||||
|
debug: (...args) => print('DEBUG', args, style('#64748b', '#ffffff')),
|
||||||
|
info: (...args) => print('INFO', args, style('#10b981', '#ffffff')),
|
||||||
|
warn: (...args) => print('WARN', args, style('#f59e0b', '#ffffff')),
|
||||||
|
error: (...args) => {
|
||||||
|
print('ERROR', args, style('#ef4444', '#ffffff'));
|
||||||
|
// Future expansion hook: e.g., Sentry.captureException(args[0]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default logger;
|
||||||
68
yarn.lock
68
yarn.lock
@@ -3054,6 +3054,11 @@ adjust-sourcemap-loader@^4.0.0:
|
|||||||
loader-utils "^2.0.0"
|
loader-utils "^2.0.0"
|
||||||
regex-parser "^2.2.11"
|
regex-parser "^2.2.11"
|
||||||
|
|
||||||
|
adler-32@~1.3.0:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz"
|
||||||
|
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
|
||||||
|
|
||||||
agent-base@6:
|
agent-base@6:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
|
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
|
||||||
@@ -3858,6 +3863,14 @@ case-sensitive-paths-webpack-plugin@^2.4.0:
|
|||||||
resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz"
|
resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz"
|
||||||
integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==
|
integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==
|
||||||
|
|
||||||
|
cfb@~1.2.1:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz"
|
||||||
|
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
|
||||||
|
dependencies:
|
||||||
|
adler-32 "~1.3.0"
|
||||||
|
crc-32 "~1.2.0"
|
||||||
|
|
||||||
chalk@^2.0.0, chalk@^2.4.1:
|
chalk@^2.0.0, chalk@^2.4.1:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
|
resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz"
|
||||||
@@ -4007,6 +4020,11 @@ coa@^2.0.2:
|
|||||||
chalk "^2.4.1"
|
chalk "^2.4.1"
|
||||||
q "^1.1.2"
|
q "^1.1.2"
|
||||||
|
|
||||||
|
codepage@~1.15.0:
|
||||||
|
version "1.15.0"
|
||||||
|
resolved "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz"
|
||||||
|
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
|
||||||
|
|
||||||
collect-v8-coverage@^1.0.0:
|
collect-v8-coverage@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz"
|
||||||
@@ -4228,6 +4246,11 @@ country-flag-icons@^1.5.4:
|
|||||||
resolved "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz"
|
resolved "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.5.10.tgz"
|
||||||
integrity sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g==
|
integrity sha512-x3elaK+ZY23W1YtFsNQknRdURzkV7g3Z93AoA7SHZJUEXbVjRsNh4h9Uf09+OjWF/4u8tXeAt37gezGRdwR/2g==
|
||||||
|
|
||||||
|
crc-32@~1.2.0, crc-32@~1.2.1:
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz"
|
||||||
|
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
|
||||||
|
|
||||||
create-ecdh@^4.0.0:
|
create-ecdh@^4.0.0:
|
||||||
version "4.0.4"
|
version "4.0.4"
|
||||||
resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz"
|
resolved "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz"
|
||||||
@@ -5709,6 +5732,11 @@ forwarded@0.2.0:
|
|||||||
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
|
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
|
||||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||||
|
|
||||||
|
frac@~1.1.2:
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz"
|
||||||
|
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
|
||||||
|
|
||||||
fraction.js@^4.2.0:
|
fraction.js@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz"
|
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz"
|
||||||
@@ -5767,11 +5795,6 @@ fs.realpath@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
|
||||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||||
|
|
||||||
fsevents@^2.3.2, fsevents@~2.3.2:
|
|
||||||
version "2.3.2"
|
|
||||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
|
|
||||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
|
||||||
|
|
||||||
function-bind@^1.1.1:
|
function-bind@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
|
||||||
@@ -7922,6 +7945,11 @@ p-try@^2.0.0:
|
|||||||
resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
|
resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
|
||||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||||
|
|
||||||
|
papaparse@^5.5.3:
|
||||||
|
version "5.5.3"
|
||||||
|
resolved "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz"
|
||||||
|
integrity sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==
|
||||||
|
|
||||||
param-case@^3.0.4:
|
param-case@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz"
|
resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz"
|
||||||
@@ -10161,6 +10189,13 @@ sprintf-js@~1.0.2:
|
|||||||
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
|
||||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
|
ssf@~0.11.2:
|
||||||
|
version "0.11.2"
|
||||||
|
resolved "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz"
|
||||||
|
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
|
||||||
|
dependencies:
|
||||||
|
frac "~1.1.2"
|
||||||
|
|
||||||
stable@^0.1.8:
|
stable@^0.1.8:
|
||||||
version "0.1.8"
|
version "0.1.8"
|
||||||
resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz"
|
resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz"
|
||||||
@@ -11144,11 +11179,21 @@ which@^2.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
isexe "^2.0.0"
|
||||||
|
|
||||||
|
wmf@~1.0.1:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz"
|
||||||
|
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
|
||||||
|
|
||||||
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
word-wrap@^1.2.3, word-wrap@~1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
|
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz"
|
||||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||||
|
|
||||||
|
word@~0.3.0:
|
||||||
|
version "0.3.0"
|
||||||
|
resolved "https://registry.npmjs.org/word/-/word-0.3.0.tgz"
|
||||||
|
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
|
||||||
|
|
||||||
workbox-background-sync@6.5.4:
|
workbox-background-sync@6.5.4:
|
||||||
version "6.5.4"
|
version "6.5.4"
|
||||||
resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz"
|
resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.5.4.tgz"
|
||||||
@@ -11352,6 +11397,19 @@ ws@^8.4.2:
|
|||||||
resolved "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz"
|
resolved "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz"
|
||||||
integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
|
integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
|
||||||
|
|
||||||
|
xlsx@^0.18.5:
|
||||||
|
version "0.18.5"
|
||||||
|
resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz"
|
||||||
|
integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
|
||||||
|
dependencies:
|
||||||
|
adler-32 "~1.3.0"
|
||||||
|
cfb "~1.2.1"
|
||||||
|
codepage "~1.15.0"
|
||||||
|
crc-32 "~1.2.1"
|
||||||
|
ssf "~0.11.2"
|
||||||
|
wmf "~1.0.1"
|
||||||
|
word "~0.3.0"
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz"
|
resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz"
|
||||||
|
|||||||
Reference in New Issue
Block a user