Compare commits
10 Commits
d4ac8fff4e
...
ce874def91
| Author | SHA1 | Date |
|---|---|---|
|
|
ce874def91 | |
|
|
ed88b1ed27 | |
|
|
f8aa6ad785 | |
|
|
167ec6b943 | |
|
|
778b05fd50 | |
|
|
5e471b7e20 | |
|
|
8a2a93a288 | |
|
|
fe41e1e45a | |
|
|
77c4b0cb1a | |
|
|
e62e678fba |
|
|
@ -0,0 +1,4 @@
|
||||||
|
VITE_API_URL="http://localhost:8000"
|
||||||
|
VITE_APP_KEY="4ae931db63f0d18f5082781be5e2c622"
|
||||||
|
VITE_APP_CACHE_TIME_VALUE=30
|
||||||
|
VITE_APP_CACHE_TIME_UNIT="seconds"
|
||||||
|
|
@ -22,3 +22,5 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
@ -12,16 +12,25 @@
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fontsource/public-sans": "^5.1.2",
|
"@fontsource/public-sans": "^5.1.2",
|
||||||
"@mui/icons-material": "^6.4.3",
|
"@mui/icons-material": "^6.4.3",
|
||||||
|
"@mui/lab": "^6.0.0-beta.27",
|
||||||
"@mui/material": "^6.4.3",
|
"@mui/material": "^6.4.3",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"immer": "^10.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.1.5",
|
||||||
"react-svg": "^16.3.0"
|
"react-svg": "^16.3.0",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
|
@ -993,6 +1002,40 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
|
||||||
|
"integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
|
||||||
|
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
|
||||||
|
},
|
||||||
"node_modules/@fontsource/public-sans": {
|
"node_modules/@fontsource/public-sans": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.1.2.tgz",
|
||||||
|
|
@ -1102,10 +1145,42 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/base": {
|
||||||
|
"version": "5.0.0-beta.69",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.69.tgz",
|
||||||
|
"integrity": "sha512-r2YyGUXpZxj8rLAlbjp1x2BnMERTZ/dMqd9cClKj2OJ7ALAuiv/9X5E9eHfRc9o/dGRuLSMq/WTjREktJVjxVA==",
|
||||||
|
"deprecated": "This package has been replaced by @base-ui-components/react",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@floating-ui/react-dom": "^2.1.1",
|
||||||
|
"@mui/types": "^7.2.21",
|
||||||
|
"@mui/utils": "^6.4.1",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "6.4.3",
|
"version": "6.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.4.tgz",
|
||||||
"integrity": "sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==",
|
"integrity": "sha512-r+J0EditrekkTtO2CnCBCOGpNaDYwJqz8lH4rj6o/anDcskZFJodBlG8aCJkS8DL/CF/9EHS+Gz53EbmYEnQbw==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui-org"
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
|
@ -1136,13 +1211,57 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/lab": {
|
||||||
"version": "6.4.3",
|
"version": "6.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.27.tgz",
|
||||||
"integrity": "sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==",
|
"integrity": "sha512-weLxPsCs2wJKgWKf46shXHE+x7qlf5VxMK3P+4HsWasMakV/uTmxsoT7PG3QCvakGQ2TdpZtQLE2umJKC0mvKQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.26.0",
|
"@babel/runtime": "^7.26.0",
|
||||||
"@mui/core-downloads-tracker": "^6.4.3",
|
"@mui/base": "5.0.0-beta.69",
|
||||||
|
"@mui/system": "^6.4.3",
|
||||||
|
"@mui/types": "^7.2.21",
|
||||||
|
"@mui/utils": "^6.4.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/react": "^11.5.0",
|
||||||
|
"@emotion/styled": "^11.3.0",
|
||||||
|
"@mui/material": "^6.4.4",
|
||||||
|
"@mui/material-pigment-css": "^6.4.3",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@emotion/styled": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@mui/material-pigment-css": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/material": {
|
||||||
|
"version": "6.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.4.tgz",
|
||||||
|
"integrity": "sha512-ISVPrIsPQsxnwvS40C4u03AuNSPigFeS2+n1qpuEZ94hDsdMi19dQM2JcC9CHEhXecSIQjP1RTyY0mPiSpSrFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@mui/core-downloads-tracker": "^6.4.4",
|
||||||
"@mui/system": "^6.4.3",
|
"@mui/system": "^6.4.3",
|
||||||
"@mui/types": "^7.2.21",
|
"@mui/types": "^7.2.21",
|
||||||
"@mui/utils": "^6.4.3",
|
"@mui/utils": "^6.4.3",
|
||||||
|
|
@ -1625,6 +1744,37 @@
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.2.tgz",
|
||||||
|
"integrity": "sha512-11tNlEDTdIhMJba2RBH+ecJ9l1zgS2kjmexDPAraulc8jeNA4xocSNeyzextT0XJyASil4XsCYlJmf5jEWAtYg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.2.tgz",
|
||||||
|
"integrity": "sha512-uvXk/U4cBiFMxt+p9/G7yUWI/UbHYbyghLCjlpWZ3mLeIZiUBSKcUnw9UnKkdRz7Z/N4UBuFLWQdJCjUe7HjvA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
@ -1671,6 +1821,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/crypto-js": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
|
|
@ -1689,6 +1845,12 @@
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
|
||||||
|
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/parse-json": {
|
"node_modules/@types/parse-json": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||||
|
|
@ -2009,6 +2171,21 @@
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
|
||||||
|
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-macros": {
|
"node_modules/babel-plugin-macros": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
|
||||||
|
|
@ -2153,6 +2330,17 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -2218,11 +2406,21 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-js": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
|
|
@ -2245,6 +2443,14 @@
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-helpers": {
|
"node_modules/dom-helpers": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
|
@ -2614,6 +2820,38 @@
|
||||||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fsevents": {
|
"node_modules/fsevents": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
|
@ -2728,6 +2966,15 @@
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
|
|
@ -2965,6 +3212,25 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
|
@ -3195,6 +3461,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
@ -3756,6 +4027,34 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -14,16 +14,25 @@
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fontsource/public-sans": "^5.1.2",
|
"@fontsource/public-sans": "^5.1.2",
|
||||||
"@mui/icons-material": "^6.4.3",
|
"@mui/icons-material": "^6.4.3",
|
||||||
|
"@mui/lab": "^6.0.0-beta.27",
|
||||||
"@mui/material": "^6.4.3",
|
"@mui/material": "^6.4.3",
|
||||||
|
"@tanstack/react-table": "^8.21.2",
|
||||||
|
"axios": "^1.7.9",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"immer": "^10.1.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.1.5",
|
||||||
"react-svg": "^16.3.0"
|
"react-svg": "^16.3.0",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.19.0",
|
"@eslint/js": "^9.19.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/lodash": "^4.17.15",
|
||||||
"@types/react": "^19.0.8",
|
"@types/react": "^19.0.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@types/react-dom": "^19.0.3",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
|
||||||
|
<path d="m5 20l14-8L5 4z"/>
|
||||||
|
<path d="M19 20L5 12l14-8z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 255 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9 21q-1.925 0-3.463-1.1T3.35 17h17.3q-.65 1.8-2.187 2.9T15 21zm-6-6V9q0-2.5 1.75-4.25T9 3h6q2.5 0 4.25 1.75T21 9v6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 237 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 512 512"><path fill="currentColor" d="M190.406 51.97c-4.104 29.047-7.13 44.833-6.72 58.53c-8.64 1.623-17.247 4.38-25.75 8.375c-25.746-38.28-86.43-44.672-139.06-13.5c27.12 33.838 44.53 50.06 88.468 49.094c-23.64 40.44-16.737 66.468-3.094 106.405c50.213-29.74 75.816-83.433 61.906-125.22c6.763-3.156 13.588-5.373 20.47-6.75c3.344 11.76 9.795 25.62 20.75 48.032c5.394-22.616 8.44-37.168 9.03-48.906c10.954 1.452 21.88 4.78 32.688 9.657c-5.85 16.68-9.962 35.86-13.25 60.688c17.36-16.772 29.677-31.288 38.844-46.188c5.488 3.756 10.908 7.874 16.25 12.313c4.39 3.648 8.69 7.515 12.906 11.563c-17.963 19.995-33.863 41.93-46.688 65.218c24.165-10.115 46.634-25.15 67.156-43.28a318 318 0 0 1 23.344 31.5c-26.33 17.278-50.652 37.486-70.375 59.844c28.77-5.146 58.07-17.09 85.908-33.156c5.354 10.33 9.97 20.708 13.75 30.906c-30.085 15.35-57.335 37.104-81.75 65.28c47.72 50.605 103.617 99.44 170 110.25c2.28-5.74 4.45-11.502 6.5-17.312c-60.357-19.627-109.66-52.267-145.563-89.156l13.406-13.03c33.782 34.707 80.56 65.736 137.97 84.405c16.732-55.238 20.843-112.648-1.844-167.217c-29.13 2.03-56.363 8.342-81.72 18.906c-4.096-10.92-9.036-21.94-14.717-32.845c30.638-19.792 58.958-44.156 82.092-69.313c-31.87 8.903-65.846 23.524-97.718 42.47c-7.725-11.933-16.287-23.498-25.53-34.376c26.697-26.486 49.666-57.987 68.186-90.03c-31.647 16.955-62.495 40.003-89.563 67.343c-4.5-4.3-9.1-8.428-13.812-12.345a225 225 0 0 0-19.375-14.438c10.292-22.438 15.372-47.604 20.875-83.437c-22.557 26.39-37.655 45.848-48.28 68.344c-13.42-5.92-27.255-9.89-41.22-11.313c-3.22-14.055-11.237-28.646-24.47-57.31zM426.25 302.187c11.947-.273 23.58 6.173 29.375 17.53c8.063 15.802 1.77 35.125-14.03 43.188c-15.803 8.063-35.126 1.802-43.19-14c-8.062-15.8-1.8-35.156 14-43.22a32 32 0 0 1 13.845-3.498"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 2v20M2 5h20M3 3v2m4-2v2m10-2v2m4-2v2m-2 0l-7 7l-7-7"/></svg>
|
||||||
|
After Width: | Height: | Size: 254 B |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 640 512"><path fill="currentColor" d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64s-64 28.7-64 64s28.7 64 64 64m448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64s-64 28.7-64 64s28.7 64 64 64m32 32h-64c-17.6 0-33.5 7.1-45.1 18.6c40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64m-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32S208 82.1 208 144s50.1 112 112 112m76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2m-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4"/></svg>
|
||||||
|
After Width: | Height: | Size: 763 B |
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Alert from "@mui/material/Alert";
|
||||||
|
import { DeviceState } from "../types/device.types";
|
||||||
|
import { AuthState } from "../types/auth.types";
|
||||||
|
import { FishboneState } from "../types/fishbone.types";
|
||||||
|
import { TowerState } from "../types/tower.types";
|
||||||
|
import { BackboneState } from "../types/backbone.types";
|
||||||
|
import { UserState } from "../types/users.types";
|
||||||
|
|
||||||
|
interface FlashMessageProps {
|
||||||
|
store: () => DeviceState
|
||||||
|
| AuthState
|
||||||
|
| FishboneState
|
||||||
|
| TowerState
|
||||||
|
| BackboneState
|
||||||
|
| UserState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FlashMessage(props: FlashMessageProps) {
|
||||||
|
const { error, clearError } = props.store();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>("");
|
||||||
|
|
||||||
|
const onCloseError = () => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
clearError()
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
setErrorMessage(error as string);
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
clearError()
|
||||||
|
}, 500);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
if (!errorMessage) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={"flex"} mb={2}>
|
||||||
|
<Alert
|
||||||
|
severity="error"
|
||||||
|
variant="border"
|
||||||
|
onClose={onCloseError}
|
||||||
|
sx={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { forwardRef, ReactNode, ReactPortal, Ref } from 'react';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { alpha, styled, useTheme } from '@mui/material';
|
||||||
|
import MuiIconButton, { IconButtonProps } from '@mui/material/IconButton';
|
||||||
|
|
||||||
|
// project-imports
|
||||||
|
import getColors from '../utils/getColors';
|
||||||
|
import getShadow from '../utils/getShadow';
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { ButtonVariantProps, ExtendedStyleProps, IconButtonShapeProps } from '../types/extended';
|
||||||
|
|
||||||
|
// ==============================|| ICON BUTTON - COLOR STYLE ||============================== //
|
||||||
|
|
||||||
|
interface IconButtonStyleProps extends ExtendedStyleProps {
|
||||||
|
variant?: ButtonVariantProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColorStyle({ variant, theme, color }: IconButtonStyleProps) {
|
||||||
|
const colors = getColors(theme, color);
|
||||||
|
const { lighter, light, dark, main, contrastText } = colors;
|
||||||
|
|
||||||
|
const buttonShadow = `${color}Button`;
|
||||||
|
const shadows = getShadow(theme, buttonShadow);
|
||||||
|
|
||||||
|
const commonShadow = {
|
||||||
|
'&::after': {
|
||||||
|
boxShadow: `0 0 6px 6px ${alpha(main, 0.9)}`
|
||||||
|
},
|
||||||
|
'&:active::after': {
|
||||||
|
boxShadow: `0 0 0 0 ${alpha(main, 0.9)}`
|
||||||
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
outline: `2px solid ${dark}`,
|
||||||
|
outlineOffset: 2
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (variant) {
|
||||||
|
case 'contained':
|
||||||
|
return {
|
||||||
|
color: contrastText,
|
||||||
|
backgroundColor: main,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: dark
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
case 'light':
|
||||||
|
return {
|
||||||
|
color: main,
|
||||||
|
backgroundColor: lighter,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: light
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
case 'shadow':
|
||||||
|
return {
|
||||||
|
boxShadow: shadows,
|
||||||
|
color: contrastText,
|
||||||
|
backgroundColor: main,
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: 'none',
|
||||||
|
backgroundColor: dark
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
case 'outlined':
|
||||||
|
return {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: dark,
|
||||||
|
borderColor: dark
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
case 'dashed':
|
||||||
|
return {
|
||||||
|
backgroundColor: theme.palette.mode === 'dark' ? alpha(lighter, 0.85) : lighter,
|
||||||
|
'&:hover': {
|
||||||
|
color: dark,
|
||||||
|
borderColor: dark
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
'&:hover': {
|
||||||
|
color: dark,
|
||||||
|
backgroundColor: lighter
|
||||||
|
},
|
||||||
|
...commonShadow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==============================|| ICON BUTTON - STYLED ||============================== //
|
||||||
|
|
||||||
|
interface StyleProps extends IconButtonStyleProps {
|
||||||
|
shape?: IconButtonShapeProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IconButtonStyle = styled(MuiIconButton, { shouldForwardProp: (prop) => prop !== 'variant' && prop !== 'shape' })(
|
||||||
|
({ theme, variant, shape, color }: StyleProps) => ({
|
||||||
|
position: 'relative',
|
||||||
|
'::after': {
|
||||||
|
content: '""',
|
||||||
|
display: 'block',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: shape === 'rounded' ? '50%' : 8,
|
||||||
|
opacity: 0,
|
||||||
|
transition: 'all 0.5s'
|
||||||
|
},
|
||||||
|
|
||||||
|
':active::after': {
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: shape === 'rounded' ? '50%' : 8,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: '0s'
|
||||||
|
},
|
||||||
|
...(shape === 'rounded' && {
|
||||||
|
borderRadius: '50%'
|
||||||
|
}),
|
||||||
|
...(variant === 'outlined' && {
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'inherit'
|
||||||
|
}),
|
||||||
|
...(variant === 'dashed' && {
|
||||||
|
border: '1px dashed',
|
||||||
|
borderColor: 'inherit'
|
||||||
|
}),
|
||||||
|
...(variant !== 'text' && {
|
||||||
|
'&.Mui-disabled': {
|
||||||
|
backgroundColor: theme.palette.secondary[200]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...getColorStyle({ variant, theme, color })
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==============================|| ICON BUTTON - EXTENDED ||============================== //
|
||||||
|
|
||||||
|
export interface Props extends IconButtonProps {
|
||||||
|
shape?: IconButtonShapeProps;
|
||||||
|
variant?: ButtonVariantProps;
|
||||||
|
children: ReactNode;
|
||||||
|
tooltip?: boolean | ReactNode | ReactPortal;
|
||||||
|
href?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function IconButton(
|
||||||
|
{ variant = 'text', shape = 'square', children, color = 'primary', tooltip, ...others }: Props,
|
||||||
|
ref: Ref<HTMLButtonElement>
|
||||||
|
) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButtonStyle ref={ref} variant={variant} shape={shape} theme={theme} color={color} {...others}>
|
||||||
|
{children}
|
||||||
|
</IconButtonStyle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton.displayName = 'IconButton';
|
||||||
|
|
||||||
|
export default forwardRef(IconButton);
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import InputAdornment from "@mui/material/InputAdornment";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import Table from "@mui/material/Table";
|
||||||
|
import TableBody from "@mui/material/TableBody";
|
||||||
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
|
import TableCell from "@mui/material/TableCell";
|
||||||
|
import TableHead from "@mui/material/TableHead";
|
||||||
|
import TableRow from "@mui/material/TableRow";
|
||||||
|
import TablePagination from "@mui/material/TablePagination";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
useReactTable,
|
||||||
|
ColumnDef,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
SortingState,
|
||||||
|
ColumnFiltersState,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { SearchNormal } from "iconsax-react";
|
||||||
|
|
||||||
|
interface ReactTableProps<T extends object> {
|
||||||
|
columns: ColumnDef<T>[];
|
||||||
|
data: T[];
|
||||||
|
striped?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComponentReactTable<T extends object>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
striped,
|
||||||
|
}: ReactTableProps<T>) {
|
||||||
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
|
const [globalFilter, setGlobalFilter] = useState("");
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
globalFilter,
|
||||||
|
},
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
onGlobalFilterChange: setGlobalFilter,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Box justifyContent={"space-between"} display="flex" sx={{ px: 2}}>
|
||||||
|
<Box>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
value={globalFilter ?? ""}
|
||||||
|
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||||
|
placeholder="Search all columns..."
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
startAdornment: <InputAdornment position="start">
|
||||||
|
<SearchNormal size="20" variant="Linear" color="#808191" />
|
||||||
|
</InputAdornment>,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableCell
|
||||||
|
key={header.id}
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{header.isPlaceholder ? null : (
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
{flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)}
|
||||||
|
{{
|
||||||
|
asc: " 🔼",
|
||||||
|
desc: " 🔽",
|
||||||
|
}[header.column.getIsSorted() as string] ?? null}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHead>
|
||||||
|
<TableBody className={striped ? "striped" : undefined}>
|
||||||
|
{table.getRowModel().rows.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} align="center">
|
||||||
|
No data.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<TablePagination
|
||||||
|
component="div"
|
||||||
|
count={table.getFilteredRowModel().rows.length}
|
||||||
|
page={table.getState().pagination.pageIndex}
|
||||||
|
onPageChange={(_, page) => table.setPageIndex(page)}
|
||||||
|
rowsPerPage={table.getState().pagination.pageSize}
|
||||||
|
onRowsPerPageChange={(e) => table.setPageSize(Number(e.target.value))}
|
||||||
|
rowsPerPageOptions={[5, 10, 25, 50]}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReactTable<T extends object>({
|
||||||
|
striped,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
}: ReactTableProps<T>) {
|
||||||
|
return <ComponentReactTable {...{ data, columns, striped }} />;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function useTitle(title: string) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
document.title = `NAM | ${title}`
|
||||||
|
}, [title])
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import { useTheme } from "@mui/material";
|
import { useTheme } from "@mui/material";
|
||||||
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
|
||||||
export default function AuthLayout() {
|
export default function AuthLayout() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { token } = useAuthStore();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,17 @@ import Menu from "@mui/material/Menu";
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import MenuIcon from "@mui/icons-material/Menu";
|
import MenuIcon from "@mui/icons-material/Menu";
|
||||||
import Avatar from "@mui/material/Avatar";
|
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useAuthStore } from "../../stores/authStore";
|
||||||
|
|
||||||
export default function Header({
|
export default function Header({
|
||||||
handleDrawerToggle,
|
handleDrawerToggle,
|
||||||
}: {
|
}: {
|
||||||
handleDrawerToggle: () => void;
|
handleDrawerToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { user, logout } = useAuthStore();
|
||||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
let navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
|
|
@ -27,10 +25,10 @@ export default function Header({
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLogout = () => {
|
const onLogout = async () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
|
|
||||||
navigate("/login")
|
await logout();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -63,7 +61,7 @@ export default function Header({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="subtitle1" noWrap component="div">
|
<Typography variant="subtitle1" noWrap component="div">
|
||||||
Hi, Admins
|
Hi, {user?.name || ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Menu
|
<Menu
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import List from "@mui/material/List";
|
import List from "@mui/material/List";
|
||||||
import ListItem from "@mui/material/ListItem";
|
import ListItem from "@mui/material/ListItem";
|
||||||
import ListSubheader from "@mui/material/ListSubheader";
|
import ListSubheader from "@mui/material/ListSubheader";
|
||||||
|
|
@ -13,6 +12,11 @@ import { Link, Typography } from "@mui/material";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
import { ReactSVG } from "react-svg";
|
import { ReactSVG } from "react-svg";
|
||||||
import Speedometer from "../../assets/speedometer.svg";
|
import Speedometer from "../../assets/speedometer.svg";
|
||||||
|
import Backbone from "../../assets/backbone.svg";
|
||||||
|
import Fishbone from "../../assets/fishbone.svg";
|
||||||
|
import Device from "../../assets/device.svg";
|
||||||
|
import Tower from "../../assets/tower.svg";
|
||||||
|
import Users from "../../assets/users.svg";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,17 +42,22 @@ const MENU = [
|
||||||
{
|
{
|
||||||
title: "Backbone",
|
title: "Backbone",
|
||||||
link: "/backbone",
|
link: "/backbone",
|
||||||
icon: Speedometer,
|
icon: Backbone,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Fishbone",
|
title: "Fishbone",
|
||||||
link: "/fishbone",
|
link: "/fishbone",
|
||||||
icon: Speedometer,
|
icon: Fishbone,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Devices",
|
title: "Devices",
|
||||||
link: "/devices",
|
link: "/devices",
|
||||||
icon: Speedometer,
|
icon: Device,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tower",
|
||||||
|
link: "/towers",
|
||||||
|
icon: Tower,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -57,8 +66,8 @@ const MENU = [
|
||||||
menus: [
|
menus: [
|
||||||
{
|
{
|
||||||
title: "User",
|
title: "User",
|
||||||
link: "/user",
|
link: "/users",
|
||||||
icon: Speedometer,
|
icon: Users,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +117,7 @@ export default function Sidebar({
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
component={Link}
|
component={Link}
|
||||||
href={menu.link}
|
href={menu.link}
|
||||||
selected={menu.link === location.pathname}
|
selected={location.pathname.startsWith(menu.link)}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ReactSVG src={menu.icon} />
|
<ReactSVG src={menu.icon} />
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,9 @@ export default function DashboardLayout(props: Props) {
|
||||||
<Box sx={{ display: "flex" }}>
|
<Box sx={{ display: "flex" }}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<AppBar
|
<AppBar
|
||||||
|
// color="default"
|
||||||
position="fixed"
|
position="fixed"
|
||||||
|
enableColorOnDark
|
||||||
sx={{
|
sx={{
|
||||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||||
ml: { sm: `${drawerWidth}px` },
|
ml: { sm: `${drawerWidth}px` },
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,79 @@ import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import CardContent from "@mui/material/CardContent";
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Grid from "@mui/material/Grid2";
|
import Grid from "@mui/material/Grid2";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import { Home } from "@mui/icons-material";
|
import { Home } from "@mui/icons-material";
|
||||||
|
import ReactTable from "../components/ReactTable";
|
||||||
|
import IconButton from "../components/IconButton";
|
||||||
|
import { Edit } from "iconsax-react";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
import FlashMessage from "../components/FlashMessage";
|
||||||
|
import { useBackboneStore } from "../stores/backboneStore";
|
||||||
|
import { BackboneDataTable } from "../types/backbone.types";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
interface UserData {
|
||||||
|
backbone: string;
|
||||||
|
device_start: string;
|
||||||
|
device_end: string;
|
||||||
|
total_core: string;
|
||||||
|
total_fishbone: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Backbone() {
|
export default function Backbone() {
|
||||||
|
useTitle("Backbone");
|
||||||
|
|
||||||
|
const { data, cacheExpired, getAll } = useBackboneStore();
|
||||||
|
|
||||||
|
// Define columns
|
||||||
|
const columns: ColumnDef<BackboneDataTable>[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
header: "Backbone",
|
||||||
|
accessorKey: "backbone_code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device Start",
|
||||||
|
accessorKey: "device_start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device End",
|
||||||
|
accessorKey: "device_end",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Total Core",
|
||||||
|
accessorKey: "core_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Total Fishbone",
|
||||||
|
accessorKey: "total_fishbone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "",
|
||||||
|
accessorKey: "action",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<IconButton variant="text" color="secondary" href={`/backbone/${row.original.id}`}>
|
||||||
|
<Edit variant="Bulk" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cacheExpired()) {
|
||||||
|
(async () => {
|
||||||
|
await getAll();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box component="main">
|
<Box component="main">
|
||||||
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
|
@ -17,7 +83,7 @@ export default function Backbone() {
|
||||||
<Typography variant="h3">Backbone</Typography>
|
<Typography variant="h3">Backbone</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid size="auto">
|
<Grid size="auto">
|
||||||
<Button variant="contained" startIcon={<AddIcon color="inherit" />}>
|
<Button variant="contained" startIcon={<AddIcon color="inherit" />} href="/backbone/create">
|
||||||
Add New
|
Add New
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
@ -31,18 +97,15 @@ export default function Backbone() {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Card sx={{ display: "flex" }}>
|
{/* <FlashMessage store={useBackboneStore} /> */}
|
||||||
<CardContent sx={{ flex: "1 0 auto" }}>
|
|
||||||
<Typography component="div" variant="h5">
|
<Card>
|
||||||
Live From Space
|
<Box sx={{ p: 2 }}>
|
||||||
</Typography>
|
<Typography variant="h5">List Backbone</Typography>
|
||||||
<Typography
|
</Box>
|
||||||
variant="subtitle1"
|
<Divider />
|
||||||
component="div"
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
sx={{ color: "text.secondary" }}
|
<ReactTable data={data || []} columns={columns} />
|
||||||
>
|
|
||||||
Mac Miller
|
|
||||||
</Typography>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Home, Save } from "@mui/icons-material";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
import IconButton from "../../components/IconButton";
|
||||||
|
import { Back } from "iconsax-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { FormEvent, useEffect } from "react";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
import { useBackboneStore } from "../../stores/backboneStore";
|
||||||
|
import { BackboneRequest } from "../../types/backbone.types";
|
||||||
|
|
||||||
|
export default function BackboneCreateOrEdit() {
|
||||||
|
useTitle("Backbone");
|
||||||
|
const { pid } = useParams();
|
||||||
|
const { update, create, isLoading, find, clearBackbone } = useBackboneStore();
|
||||||
|
|
||||||
|
const IS_EDIT_PAGE = pid!!;
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
|
||||||
|
const data = Object.fromEntries(formData.entries()) as unknown as BackboneRequest;
|
||||||
|
|
||||||
|
if (IS_EDIT_PAGE) {
|
||||||
|
await update(pid!!, data);
|
||||||
|
} else {
|
||||||
|
await create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid!!) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid!!);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearBackbone()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Box display={"flex"} alignItems={"center"} gap={1}>
|
||||||
|
<IconButton variant="dashed" color="primary" href="/backbone">
|
||||||
|
<Back variant="Outline" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Backbone - ${pid}` : 'Create Backbone'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Link underline="hover" color="inherit" href="/backbone">
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Backbone</Typography>
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useBackboneStore} />
|
||||||
|
|
||||||
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
|
<Box
|
||||||
|
sx={{ px: 3, py: 1 }}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Fill the form</Typography>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save color="inherit" />}
|
||||||
|
>
|
||||||
|
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Form />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useBackboneStore } from "../../stores/backboneStore";
|
||||||
|
|
||||||
|
export default function Form() {
|
||||||
|
const {
|
||||||
|
backbone,
|
||||||
|
clearBackbone,
|
||||||
|
deviceSelect,
|
||||||
|
getDeviceSelect,
|
||||||
|
} = useBackboneStore();
|
||||||
|
const [deviceStart, setDeviceStart] = useState<string>("");
|
||||||
|
const [deviceEnd, setDeviceEnd] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getDeviceSelect();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (backbone) {
|
||||||
|
setDeviceStart(backbone.dev_start_id as string);
|
||||||
|
setDeviceEnd(backbone.dev_end_id as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearBackbone();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [backbone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="backbone_code"
|
||||||
|
label="Backbone Code"
|
||||||
|
variant="outlined"
|
||||||
|
value={backbone?.backbone_code}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="core_amount"
|
||||||
|
label="Core used"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={backbone?.core_amount}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-start-label">Device Start</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device-start"
|
||||||
|
labelId="device-start-label"
|
||||||
|
label="Device Start"
|
||||||
|
value={deviceStart}
|
||||||
|
onChange={(e) => setDeviceStart(e.target.value)}
|
||||||
|
>
|
||||||
|
{deviceSelect?.map((device, key) => (
|
||||||
|
<MenuItem key={key} value={device.value}>
|
||||||
|
{device.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-end-label">Device End</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device-end"
|
||||||
|
labelId="device-end-label"
|
||||||
|
label="Device End"
|
||||||
|
value={deviceEnd}
|
||||||
|
onChange={(e) => setDeviceEnd(e.target.value)}
|
||||||
|
>
|
||||||
|
{deviceSelect
|
||||||
|
?.filter((b) => b.value != deviceStart)
|
||||||
|
.map((device, key) => (
|
||||||
|
<MenuItem key={key} value={device.value}>
|
||||||
|
{device.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="longitude"
|
||||||
|
label="Longitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={backbone?.longitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="latitude"
|
||||||
|
label="Latitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={backbone?.latitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
useTitle("Dashboard");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
Dashboard
|
Dashboard
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Home } from "@mui/icons-material";
|
||||||
|
import ReactTable from "../components/ReactTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import IconButton from "../components/IconButton";
|
||||||
|
import { Edit } from "iconsax-react";
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
import { useDeviceStore } from "../stores/deviceStore";
|
||||||
|
import { DeviceDataTable } from "../types/device.types";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import FlashMessage from "../components/FlashMessage";
|
||||||
|
|
||||||
|
export default function Devices() {
|
||||||
|
useTitle("Devices");
|
||||||
|
|
||||||
|
const { data, cacheExpired, getAll } = useDeviceStore();
|
||||||
|
|
||||||
|
// Define columns
|
||||||
|
const columns: ColumnDef<DeviceDataTable>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
header: "Device Code",
|
||||||
|
accessorKey: "device_code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device Type",
|
||||||
|
accessorKey: "device_type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Address",
|
||||||
|
accessorKey: "address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Total Port",
|
||||||
|
accessorKey: "port_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Total Used Port",
|
||||||
|
accessorKey: "total_used_port",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Status",
|
||||||
|
accessorKey: "status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Typography
|
||||||
|
color={
|
||||||
|
row.original.status.toLowerCase() === "active"
|
||||||
|
? "success.main"
|
||||||
|
: row.original.status.toLowerCase() === "inactive"
|
||||||
|
? "error.main"
|
||||||
|
: "warning.main"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{row.original.status.toUpperCase()}
|
||||||
|
</Typography>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "",
|
||||||
|
accessorKey: "action",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
href={`/devices/${row.original.id}`}
|
||||||
|
>
|
||||||
|
<Edit variant="Bulk" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cacheExpired()) {
|
||||||
|
(async () => {
|
||||||
|
await getAll();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="grow">
|
||||||
|
<Typography variant="h3">Devices</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Button
|
||||||
|
href="/devices/create"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon color="inherit" />}
|
||||||
|
>
|
||||||
|
Add New
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Devices</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useDeviceStore} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h5">List Devices</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
|
<ReactTable data={data || []} columns={columns} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Home, Save } from "@mui/icons-material";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
import IconButton from "../../components/IconButton";
|
||||||
|
import { Back } from "iconsax-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { FormEvent, useEffect } from "react";
|
||||||
|
import { useDeviceStore } from "../../stores/deviceStore";
|
||||||
|
import { DeviceRequest } from "../../types/device.types";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
|
||||||
|
export default function DeviceCreateOrEdit() {
|
||||||
|
useTitle("Devices");
|
||||||
|
const { pid } = useParams();
|
||||||
|
const { update, create, isLoading, find, clearDevice, device } = useDeviceStore();
|
||||||
|
|
||||||
|
const IS_EDIT_PAGE = pid!!;
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
|
||||||
|
const data = Object.fromEntries(formData.entries()) as unknown as DeviceRequest;
|
||||||
|
|
||||||
|
if (IS_EDIT_PAGE) {
|
||||||
|
await update(pid!!, data);
|
||||||
|
} else {
|
||||||
|
await create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid!!) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid!!);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearDevice()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Box display={"flex"} alignItems={"center"} gap={1}>
|
||||||
|
<IconButton variant="dashed" color="primary" href="/devices">
|
||||||
|
<Back variant="Outline" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Device - ${pid}` : 'Create Device'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Link underline="hover" color="inherit" href="/devices">
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Devices</Typography>
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useDeviceStore} />
|
||||||
|
|
||||||
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
|
<Box
|
||||||
|
sx={{ px: 3, py: 1 }}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Fill the form {device?.device_code}</Typography>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save color="inherit" />}
|
||||||
|
>
|
||||||
|
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Form />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { DeviceStatus } from "../../types/device.types";
|
||||||
|
import { useDeviceStore } from "../../stores/deviceStore";
|
||||||
|
|
||||||
|
export default function Form() {
|
||||||
|
const { device, clearDevice, towerSelect, getTowerSelect } = useDeviceStore();
|
||||||
|
const [select, setSelect] = useState<DeviceStatus>("active");
|
||||||
|
const [tower, setTower] = useState<string>("");
|
||||||
|
|
||||||
|
const STATUS = ["active", "inactive", "maintenance"];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getTowerSelect();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (device) {
|
||||||
|
setSelect(device.status);
|
||||||
|
setTower(device.tower_id as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearDevice();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [device]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="device_code"
|
||||||
|
label="Device Code"
|
||||||
|
variant="outlined"
|
||||||
|
value={device?.device_code}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="port_count"
|
||||||
|
label="Total Port"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={device?.port_amount}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="tower-label">Tower</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="tower"
|
||||||
|
labelId="tower-label"
|
||||||
|
label="Tower"
|
||||||
|
value={tower}
|
||||||
|
onChange={(e) => setTower(e.target.value)}
|
||||||
|
>
|
||||||
|
{towerSelect?.map((tower, key) => (
|
||||||
|
<MenuItem key={key} value={tower.value}>
|
||||||
|
{tower.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="status-label">Status</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="status"
|
||||||
|
labelId="status-label"
|
||||||
|
label="Status"
|
||||||
|
value={select}
|
||||||
|
onChange={(e) => setSelect(e.target.value as DeviceStatus)}
|
||||||
|
renderValue={(selected: string) => (
|
||||||
|
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||||
|
<Chip
|
||||||
|
label={selected.toUpperCase()}
|
||||||
|
size="medium"
|
||||||
|
variant="filled"
|
||||||
|
color={
|
||||||
|
selected === "active"
|
||||||
|
? "success"
|
||||||
|
: selected === "inactive"
|
||||||
|
? "error"
|
||||||
|
: "warning"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{STATUS.map((status) => (
|
||||||
|
<MenuItem key={status} value={status}>
|
||||||
|
{status.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="longitude"
|
||||||
|
label="Longitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={device?.longitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="latitude"
|
||||||
|
label="Latitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={device?.latitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Home } from "@mui/icons-material";
|
||||||
|
import ReactTable from "../components/ReactTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import IconButton from "../components/IconButton";
|
||||||
|
import { Edit } from "iconsax-react";
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
import { useFishboneStore } from "../stores/fishboneStore";
|
||||||
|
import { FishboneDataTable } from "../types/fishbone.types";
|
||||||
|
import FlashMessage from "../components/FlashMessage";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
export default function Fishbone() {
|
||||||
|
useTitle("Fishbone");
|
||||||
|
|
||||||
|
const { data, cacheExpired, getAll } = useFishboneStore();
|
||||||
|
|
||||||
|
const columns: ColumnDef<FishboneDataTable>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
header: "Backbone",
|
||||||
|
accessorKey: "backbone",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device Start",
|
||||||
|
accessorKey: "device_start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device End",
|
||||||
|
accessorKey: "device_end",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Address",
|
||||||
|
accessorKey: "address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Core used",
|
||||||
|
accessorKey: "core_amount",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "",
|
||||||
|
accessorKey: "action",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
href={`/fishbone/${row.original.id}`}
|
||||||
|
>
|
||||||
|
<Edit variant="Bulk" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cacheExpired()) {
|
||||||
|
(async () => {
|
||||||
|
await getAll();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="grow">
|
||||||
|
<Typography variant="h3">Fishbone</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Button
|
||||||
|
href="/fishbone/create"
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<AddIcon color="inherit" />}
|
||||||
|
>
|
||||||
|
Add New
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Fishbone</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useFishboneStore} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h5">List Devices</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
|
<ReactTable data={data || []} columns={columns} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Home, Save } from "@mui/icons-material";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
import IconButton from "../../components/IconButton";
|
||||||
|
import { Back } from "iconsax-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { FormEvent, useEffect } from "react";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
import { useFishboneStore } from "../../stores/fishboneStore";
|
||||||
|
import { FishboneRequest } from "../../types/fishbone.types";
|
||||||
|
|
||||||
|
export default function FishboneCreateOrEdit() {
|
||||||
|
useTitle("Fishbone");
|
||||||
|
const { pid } = useParams();
|
||||||
|
const { update, create, isLoading, find, clearFishbone } = useFishboneStore();
|
||||||
|
|
||||||
|
const IS_EDIT_PAGE = pid!!;
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
|
||||||
|
const data = Object.fromEntries(formData.entries()) as unknown as FishboneRequest;
|
||||||
|
|
||||||
|
if (IS_EDIT_PAGE) {
|
||||||
|
await update(pid!!, data);
|
||||||
|
} else {
|
||||||
|
await create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid!!) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid!!);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearFishbone()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Box display={"flex"} alignItems={"center"} gap={1}>
|
||||||
|
<IconButton variant="dashed" color="primary" href="/fishbone">
|
||||||
|
<Back variant="Outline" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Fishbone - ${pid}` : 'Create Fishbone'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Link underline="hover" color="inherit" href="/fishbone">
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Fishbone</Typography>
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useFishboneStore} />
|
||||||
|
|
||||||
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
|
<Box
|
||||||
|
sx={{ px: 3, py: 1 }}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Fill the form</Typography>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save color="inherit" />}
|
||||||
|
>
|
||||||
|
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Form />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFishboneStore } from "../../stores/fishboneStore";
|
||||||
|
|
||||||
|
export default function Form() {
|
||||||
|
const {
|
||||||
|
fishbone,
|
||||||
|
clearFishbone,
|
||||||
|
backboneSelect,
|
||||||
|
deviceSelect,
|
||||||
|
getDeviceSelect,
|
||||||
|
getBackboneSelect,
|
||||||
|
} = useFishboneStore();
|
||||||
|
const [backbone, setBackbone] = useState<string>("");
|
||||||
|
const [deviceStart, setDeviceStart] = useState<string>("");
|
||||||
|
const [deviceEnd, setDeviceEnd] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getBackboneSelect();
|
||||||
|
await getDeviceSelect();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (fishbone) {
|
||||||
|
setBackbone(fishbone.backbone_id);
|
||||||
|
setDeviceStart(fishbone.dev_start_id);
|
||||||
|
setDeviceEnd(fishbone.dev_end_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearFishbone();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [fishbone]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ md: 4, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="backbone-label">Backbone</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="backbone"
|
||||||
|
labelId="backbone-label"
|
||||||
|
label="Backbone"
|
||||||
|
value={backbone}
|
||||||
|
onChange={(e) => setBackbone(e.target.value)}
|
||||||
|
>
|
||||||
|
{backboneSelect?.map((backbone, key) => (
|
||||||
|
<MenuItem key={key} value={backbone.value}>
|
||||||
|
{backbone.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 4, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-start-label">Device Start</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device-start"
|
||||||
|
labelId="device-start-label"
|
||||||
|
label="Device Start"
|
||||||
|
value={deviceStart}
|
||||||
|
onChange={(e) => setDeviceStart(e.target.value)}
|
||||||
|
>
|
||||||
|
{deviceSelect?.map((device, key) => (
|
||||||
|
<MenuItem key={key} value={device.value}>
|
||||||
|
{device.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 4, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-end-label">Device End</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device-end"
|
||||||
|
labelId="device-end-label"
|
||||||
|
label="Device End"
|
||||||
|
value={deviceEnd}
|
||||||
|
onChange={(e) => setDeviceEnd(e.target.value)}
|
||||||
|
>
|
||||||
|
{deviceSelect
|
||||||
|
?.filter((b) => b.value != deviceStart)
|
||||||
|
.map((device, key) => (
|
||||||
|
<MenuItem key={key} value={device.value}>
|
||||||
|
{device.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 12, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="core_amount"
|
||||||
|
label="Core used"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
slotProps={{
|
||||||
|
htmlInput: Object.assign(
|
||||||
|
{ min: 1 },
|
||||||
|
fishbone?.backbone_core && { max: fishbone.backbone_core }
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
value={fishbone?.core_amount}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="longitude"
|
||||||
|
label="Longitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={fishbone?.longitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="latitude"
|
||||||
|
label="Latitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={fishbone?.latitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Home } from "@mui/icons-material";
|
||||||
|
import ReactTable from "../components/ReactTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import IconButton from "../components/IconButton";
|
||||||
|
import { Edit } from "iconsax-react";
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
import { useTowerStore } from "../stores/towerStore";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import FlashMessage from "../components/FlashMessage";
|
||||||
|
import { TowerDataTable } from "../types/tower.types";
|
||||||
|
|
||||||
|
export default function Towers() {
|
||||||
|
useTitle("Towers");
|
||||||
|
|
||||||
|
const { data, cacheExpired, getAll } = useTowerStore();
|
||||||
|
|
||||||
|
// Define columns
|
||||||
|
const columns: ColumnDef<TowerDataTable>[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
header: "Tower Code",
|
||||||
|
accessorKey: "tower_code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Device",
|
||||||
|
accessorKey: "device_code",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Address",
|
||||||
|
accessorKey: "address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
accessorKey: "action",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<IconButton variant="text" color="secondary" href={`/towers/${row.original.id}`}>
|
||||||
|
<Edit variant="Bulk" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cacheExpired()) {
|
||||||
|
(async () => {
|
||||||
|
await getAll();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="grow">
|
||||||
|
<Typography variant="h3">Towers</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Button variant="contained" href="/towers/create" startIcon={<AddIcon color="inherit" />}>
|
||||||
|
Add New
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Towers</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useTowerStore} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h5">List Towers</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
|
<ReactTable data={data || []} columns={columns} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Home, Save } from "@mui/icons-material";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
import IconButton from "../../components/IconButton";
|
||||||
|
import { Back } from "iconsax-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { FormEvent, useEffect } from "react";
|
||||||
|
import { useTowerStore } from "../../stores/towerStore";
|
||||||
|
import { TowerRequest } from "../../types/tower.types";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
|
||||||
|
export default function TowerCreateOrEdit() {
|
||||||
|
useTitle("Towers");
|
||||||
|
const { pid } = useParams();
|
||||||
|
const { update, create, isLoading, find, clearTower, tower } = useTowerStore();
|
||||||
|
|
||||||
|
const IS_EDIT_PAGE = pid!!;
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
|
||||||
|
const data = Object.fromEntries(formData.entries()) as unknown as TowerRequest;
|
||||||
|
|
||||||
|
if (IS_EDIT_PAGE) {
|
||||||
|
await update(pid!!, data);
|
||||||
|
} else {
|
||||||
|
await create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid!!) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid!!);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearTower()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Box display={"flex"} alignItems={"center"} gap={1}>
|
||||||
|
<IconButton variant="dashed" color="primary" href="/towers">
|
||||||
|
<Back variant="Outline" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit Tower - ${pid}` : 'Create Tower'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Link underline="hover" color="inherit" href="/towers">
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Towers</Typography>
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useTowerStore} />
|
||||||
|
|
||||||
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
|
<Box
|
||||||
|
sx={{ px: 3, py: 1 }}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Fill the form {tower?.tower_code}</Typography>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save color="inherit" />}
|
||||||
|
>
|
||||||
|
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Form />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Chip from "@mui/material/Chip";
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { DeviceStatus } from "../../types/device.types";
|
||||||
|
import { useTowerStore } from "../../stores/towerStore";
|
||||||
|
|
||||||
|
export default function Form() {
|
||||||
|
const { tower, clearTower, deviceSelect, getDeviceSelect } = useTowerStore();
|
||||||
|
const [device, setDevice] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getDeviceSelect();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (tower) {
|
||||||
|
setDevice(tower.device_id as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearTower();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [tower]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="tower_code"
|
||||||
|
label="Tower Code"
|
||||||
|
variant="outlined"
|
||||||
|
value={tower?.tower_code}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-label">Device</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device"
|
||||||
|
labelId="device-label"
|
||||||
|
label="Device"
|
||||||
|
value={device}
|
||||||
|
onChange={(e) => setDevice(e.target.value)}
|
||||||
|
>
|
||||||
|
{deviceSelect?.map((device, key) => (
|
||||||
|
<MenuItem key={key} value={device.value}>
|
||||||
|
{device.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="longitude"
|
||||||
|
label="Longitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={tower?.longitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: 6, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="latitude"
|
||||||
|
label="Latitude"
|
||||||
|
variant="outlined"
|
||||||
|
type="number"
|
||||||
|
value={tower?.latitude}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Home } from "@mui/icons-material";
|
||||||
|
import ReactTable from "../components/ReactTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import IconButton from "../components/IconButton";
|
||||||
|
import { Edit } from "iconsax-react";
|
||||||
|
import useTitle from "../hooks/useTitle";
|
||||||
|
import { useUserStore } from "../stores/userStore";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import FlashMessage from "../components/FlashMessage";
|
||||||
|
import { UserDataTable } from "../types/users.types";
|
||||||
|
|
||||||
|
export default function Users() {
|
||||||
|
useTitle("Users");
|
||||||
|
|
||||||
|
const { data, cacheExpired, getAll } = useUserStore();
|
||||||
|
|
||||||
|
// Define columns
|
||||||
|
const columns: ColumnDef<UserDataTable>[] = useMemo(() => [
|
||||||
|
{
|
||||||
|
header: "Name",
|
||||||
|
accessorKey: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Username",
|
||||||
|
accessorKey: "username",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Role",
|
||||||
|
accessorKey: "role",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
accessorKey: "action",
|
||||||
|
enableSorting: false,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<>
|
||||||
|
<IconButton variant="text" color="secondary" href={`/users/${row.original.id}`}>
|
||||||
|
<Edit variant="Bulk" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cacheExpired()) {
|
||||||
|
(async () => {
|
||||||
|
await getAll();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="grow">
|
||||||
|
<Typography variant="h3">Users</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Button variant="contained" href="/users/create" startIcon={<AddIcon color="inherit" />}>
|
||||||
|
Add New
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Users</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useUserStore} />
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<Typography variant="h5">List Users</Typography>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent sx={{ flex: "1 0 auto", pt: 2, px: 0 }}>
|
||||||
|
<ReactTable data={data || []} columns={columns} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Breadcrumbs from "@mui/material/Breadcrumbs";
|
||||||
|
import Button from "@mui/material/Button";
|
||||||
|
import Card from "@mui/material/Card";
|
||||||
|
import Divider from "@mui/material/Divider";
|
||||||
|
import CardContent from "@mui/material/CardContent";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import Link from "@mui/material/Link";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Home, Save } from "@mui/icons-material";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
import IconButton from "../../components/IconButton";
|
||||||
|
import { Back } from "iconsax-react";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import Form from "./Form";
|
||||||
|
import { FormEvent, useEffect } from "react";
|
||||||
|
import { useUserStore } from "../../stores/userStore";
|
||||||
|
import { UserRequest } from "../../types/users.types";
|
||||||
|
import FlashMessage from "../../components/FlashMessage";
|
||||||
|
|
||||||
|
export default function UserCreateOrEdit() {
|
||||||
|
useTitle("Users");
|
||||||
|
const { pid } = useParams();
|
||||||
|
const { update, create, isLoading, find, clearUser } = useUserStore();
|
||||||
|
|
||||||
|
const IS_EDIT_PAGE: boolean = !!pid;
|
||||||
|
|
||||||
|
const onSubmit = async (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement);
|
||||||
|
|
||||||
|
const data = Object.fromEntries(formData.entries()) as unknown as UserRequest;
|
||||||
|
|
||||||
|
if (IS_EDIT_PAGE) {
|
||||||
|
await update(pid!, data);
|
||||||
|
} else {
|
||||||
|
await create(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pid) {
|
||||||
|
(async () => {
|
||||||
|
await find(pid);
|
||||||
|
})()
|
||||||
|
} else {
|
||||||
|
(async () => {
|
||||||
|
await clearUser()
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box component="main">
|
||||||
|
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||||
|
<Grid size="auto">
|
||||||
|
<Box display={"flex"} alignItems={"center"} gap={1}>
|
||||||
|
<IconButton variant="dashed" color="primary" href="/users">
|
||||||
|
<Back variant="Outline" color="currentColor" />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h3">{IS_EDIT_PAGE ? `Edit User - ${pid}` : 'Create User'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Breadcrumbs aria-label="breadcrumb">
|
||||||
|
<Link underline="hover" color="inherit" href="/dashboard">
|
||||||
|
<Home />
|
||||||
|
</Link>
|
||||||
|
<Link underline="hover" color="inherit" href="/users">
|
||||||
|
<Typography sx={{ color: "text.primary" }}>Users</Typography>
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{ color: "text.primary" }}>{IS_EDIT_PAGE ? pid : 'Create'}</Typography>
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<FlashMessage store={useUserStore} />
|
||||||
|
|
||||||
|
<Card component="form" onSubmit={onSubmit}>
|
||||||
|
<Box
|
||||||
|
sx={{ px: 3, py: 1 }}
|
||||||
|
display={"flex"}
|
||||||
|
justifyContent={"space-between"}
|
||||||
|
alignItems={"center"}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Fill the form</Typography>
|
||||||
|
<Button
|
||||||
|
disabled={isLoading}
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
startIcon={<Save color="inherit" />}
|
||||||
|
>
|
||||||
|
{IS_EDIT_PAGE ? "Save Changes" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
|
<CardContent>
|
||||||
|
<Form isEdit={IS_EDIT_PAGE} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
import FormControl from "@mui/material/FormControl";
|
||||||
|
import Grid from "@mui/material/Grid2";
|
||||||
|
import IconButton from "@mui/material/IconButton";
|
||||||
|
import InputAdornment from "@mui/material/InputAdornment";
|
||||||
|
import InputLabel from "@mui/material/InputLabel";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
|
import Select from "@mui/material/Select";
|
||||||
|
import TextField from "@mui/material/TextField";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useUserStore } from "../../stores/userStore";
|
||||||
|
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||||
|
|
||||||
|
export default function Form({ isEdit }: { isEdit: boolean }) {
|
||||||
|
const { user, clearUser, getRoleSelect, roleSelect } = useUserStore();
|
||||||
|
const [role, setRole] = useState<string>("");
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const handleClickShowPassword = () => setShowPassword((show) => !show);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await getRoleSelect();
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
setRole(user.role_id as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = async () => {
|
||||||
|
await clearUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clear();
|
||||||
|
};
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ md: isEdit ? 6 : 4, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
variant="outlined"
|
||||||
|
value={user?.name}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: isEdit ? 6 : 4, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
variant="outlined"
|
||||||
|
type="text"
|
||||||
|
value={user?.username}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid size={{ md: isEdit ? 12 : 4, xs: 12 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="device-label">Role</InputLabel>
|
||||||
|
<Select
|
||||||
|
name="device"
|
||||||
|
labelId="device-label"
|
||||||
|
label="Device"
|
||||||
|
value={role}
|
||||||
|
onChange={(e) => setRole(e.target.value)}
|
||||||
|
>
|
||||||
|
{roleSelect?.map((role, key) => (
|
||||||
|
<MenuItem key={key} value={role.value}>
|
||||||
|
{role.label.toUpperCase()}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{!isEdit ? (
|
||||||
|
<Grid size={{ md: 12, xs: 12 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
variant="outlined"
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={user?.password}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
endAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<IconButton
|
||||||
|
aria-label={
|
||||||
|
showPassword
|
||||||
|
? "hide the password"
|
||||||
|
: "display the password"
|
||||||
|
}
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
edge="end"
|
||||||
|
>
|
||||||
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,29 @@ import Card from "@mui/material/Card";
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||||
import Checkbox from "@mui/material/Checkbox";
|
import Checkbox from "@mui/material/Checkbox";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import Link from "@mui/material/Link";
|
|
||||||
import Grid from "@mui/material/Grid2";
|
import Grid from "@mui/material/Grid2";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import { useAuthStore } from "../../stores/authStore";
|
||||||
|
import useTitle from "../../hooks/useTitle";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
|
useTitle("Login");
|
||||||
|
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const onSubmit = (e: any) => {
|
const { login, token, isLoading, error } = useAuthStore();
|
||||||
|
|
||||||
|
const onSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
await login({
|
||||||
|
username: e.target.username.value,
|
||||||
|
password: e.target.password.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (token) {
|
||||||
navigate("/dashboard");
|
navigate("/dashboard");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -34,6 +46,8 @@ export default function Login() {
|
||||||
name="username"
|
name="username"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
error={!!error}
|
||||||
|
helperText={error?.toString()}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
margin="normal"
|
margin="normal"
|
||||||
|
|
@ -58,7 +72,7 @@ export default function Login() {
|
||||||
</Link>
|
</Link>
|
||||||
</Grid> */}
|
</Grid> */}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button type="submit" fullWidth variant="contained" sx={{ my: 2 }}>
|
<Button type="submit" fullWidth variant="contained" sx={{ my: 2 }} disabled={isLoading}>
|
||||||
Sign In
|
Sign In
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { createAPI } from '../services/api';
|
||||||
|
import { LoginPayload, LoginResponse } from '../types/auth.types';
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const authRepository = {
|
||||||
|
async login(payload: LoginPayload): Promise<LoginResponse> {
|
||||||
|
// const response = await api.post<LoginResponse>('/auth/login', payload);
|
||||||
|
|
||||||
|
// return response.data;
|
||||||
|
return Promise.resolve({
|
||||||
|
status: {
|
||||||
|
code: 200,
|
||||||
|
description: 'OK',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
token: 'sample-token',
|
||||||
|
user: {
|
||||||
|
name: 'John Doe',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async logout(): Promise<void> {
|
||||||
|
await api.post('/auth/logout');
|
||||||
|
},};
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { createAPI } from "../services/api";
|
||||||
|
import { BackboneRepositoryProps, Backbone, IGetDeviceSelect } from "../types/backbone.types";
|
||||||
|
import { ResponseApi } from "../types/response.types";
|
||||||
|
import { SelectState } from "../types/store.types";
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const backboneRepository: BackboneRepositoryProps = {
|
||||||
|
async getAll() {
|
||||||
|
// const response = await api.get<ResponseApi<Backbone[]>>("/api/v1/backbones");
|
||||||
|
|
||||||
|
// return response.data.data as Backbone[];
|
||||||
|
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
backbone_code: "BB-01",
|
||||||
|
device_start: "DEV-S1",
|
||||||
|
device_end: "DEV-E1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
core_amount: 10,
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
total_fishbone: 2,
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async getBackboneById(id: string) {
|
||||||
|
// const response = await api.get<ResponseApi<Backbone>>(`/api/v1/backbones/${id}`);
|
||||||
|
|
||||||
|
// return response.data.data as Backbone;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
backbone_code: "BB-01",
|
||||||
|
dev_start_id: "1",
|
||||||
|
dev_end_id: "2",
|
||||||
|
core_amount: 10,
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createBackbone(data: Backbone) {
|
||||||
|
const response = await api.post<ResponseApi<null>>("/api/v1/backbones", data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateBackbone(id: string, data: Backbone) {
|
||||||
|
const response = await api.put<ResponseApi<null>>(`/api/v1/backbones/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDeviceSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/backbones/backbone");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetBackboneSelect[])?.map((i: IGetBackboneSelect) => ({label: i.backbone_code, value: i.id})) as SelectState[];
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "DV-EX-001",
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "DV-EX-002",
|
||||||
|
value: "2",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { createAPI } from "../services/api";
|
||||||
|
import { DeviceRepositoryProps, Device, IGetTowerSelect } from "../types/device.types";
|
||||||
|
import { ResponseApi } from "../types/response.types";
|
||||||
|
import { SelectState } from "../types/store.types";
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const deviceRepository: DeviceRepositoryProps = {
|
||||||
|
async getAll() {
|
||||||
|
// const response = await api.get<ResponseApi<Device[]>>("/api/v1/devices");
|
||||||
|
|
||||||
|
// return response.data.data as Device[];
|
||||||
|
|
||||||
|
return Promise.resolve( [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
device_code: "DV-EX-001",
|
||||||
|
device_type: "OTB",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
port_amount: 10,
|
||||||
|
total_used_port: 5,
|
||||||
|
status: "active",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
device_code: "DV-EX-002",
|
||||||
|
device_type: "OTB",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
port_amount: 10,
|
||||||
|
total_used_port: 5,
|
||||||
|
status: "active",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDeviceById(id: string) {
|
||||||
|
// const response = await api.get<ResponseApi<Device>>(`/api/v1/devices/${id}`);
|
||||||
|
|
||||||
|
// return response.data.data as Device;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
device_code: "DV-EX-001",
|
||||||
|
device_type: "OTB",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
port_amount: 10,
|
||||||
|
status: "active",
|
||||||
|
tower_id: null,
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createDevice(data: Device) {
|
||||||
|
const response = await api.post<ResponseApi<null>>("/api/v1/devices", data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateDevice(id: string, data: Device) {
|
||||||
|
const response = await api.put<ResponseApi<null>>(`/api/v1/devices/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTowerSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/devices/tower-select");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetTowerSelect[])?.map((i: IGetTowerSelect) => ({label: i.tower_code, value: i.id})) as SelectState[];
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "TWR-EX-001",
|
||||||
|
value: "1",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { createAPI } from "../services/api";
|
||||||
|
import { FishboneRepositoryProps, Fishbone, IGetBackboneSelect } from "../types/fishbone.types";
|
||||||
|
import { ResponseApi } from "../types/response.types";
|
||||||
|
import { SelectState } from "../types/store.types";
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const fishboneRepository: FishboneRepositoryProps = {
|
||||||
|
async getAll() {
|
||||||
|
// const response = await api.get<ResponseApi<Fishbone[]>>("/api/v1/fishbones");
|
||||||
|
|
||||||
|
// return response.data.data as Fishbone[];
|
||||||
|
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
backbone: "abced-efghi",
|
||||||
|
device_start: "DEV-S1",
|
||||||
|
device_end: "DEV-E1",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
core_amount: 10,
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFishboneById(id: string) {
|
||||||
|
// const response = await api.get<ResponseApi<Fishbone>>(`/api/v1/fishbones/${id}`);
|
||||||
|
|
||||||
|
// return response.data.data as Fishbone;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
// fishbone_code: "DV-EX-001",
|
||||||
|
backbone_id: "abced-efghi",
|
||||||
|
backbone_core: "16",
|
||||||
|
dev_start_id: "1",
|
||||||
|
dev_end_id: "2",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
core_amount: 10,
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createFishbone(data: Fishbone) {
|
||||||
|
const response = await api.post<ResponseApi<null>>("/api/v1/fishbones", data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateFishbone(id: string, data: Fishbone) {
|
||||||
|
const response = await api.put<ResponseApi<null>>(`/api/v1/fishbones/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getBackboneSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/fishbones/backbone");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetBackboneSelect[])?.map((i: IGetBackboneSelect) => ({label: i.backbone_code, value: i.id})) as SelectState[];
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "BB-001",
|
||||||
|
value: "abced-efghi",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "BB-002",
|
||||||
|
value: "jklmn-opqrs",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDeviceSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/fishbones/device");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetBackboneSelect[])?.map((i: IGetBackboneSelect) => ({label: i.backbone_code, value: i.id})) as SelectState[];
|
||||||
|
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "DEV-S1",
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "DEV-S2",
|
||||||
|
value: "2",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { createAPI } from "../services/api";
|
||||||
|
import { TowerRepositoryProps, Tower, IGetDeviceSelect } from "../types/tower.types";
|
||||||
|
import { ResponseApi } from "../types/response.types";
|
||||||
|
import { SelectState } from "../types/store.types";
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const towerRepository: TowerRepositoryProps = {
|
||||||
|
async getAll() {
|
||||||
|
// const response = await api.get<ResponseApi<Tower[]>>("/api/v1/towers");
|
||||||
|
|
||||||
|
// return response.data.data as Tower[];
|
||||||
|
|
||||||
|
return Promise.resolve( [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
tower_code: "TW-001",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
device_code: "DV-EX-001",
|
||||||
|
address: "Jl. Jend Sudirman No. XX",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
}
|
||||||
|
])
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTowerById(id: string) {
|
||||||
|
// const response = await api.get<ResponseApi<Tower>>(`/api/v1/towers/${id}`);
|
||||||
|
|
||||||
|
// return response.data.data as Tower;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
tower_code: "TW-001",
|
||||||
|
longitude: "1",
|
||||||
|
latitude: "1",
|
||||||
|
device_id: "1",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTower(data: Tower) {
|
||||||
|
const response = await api.post<ResponseApi<null>>("/api/v1/towers", data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTower(id: string, data: Tower) {
|
||||||
|
const response = await api.put<ResponseApi<null>>(`/api/v1/towers/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDeviceSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/towers/tower-select");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetDeviceSelect[])?.map((i: IGetDeviceSelect) => ({label: i.tower_code, value: i.id})) as SelectState[];
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "DV-EX-001",
|
||||||
|
value: "1",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { createAPI } from "../services/api";
|
||||||
|
import { UserRepositoryProps, User, IGetRoleSelect } from "../types/users.types";
|
||||||
|
import { ResponseApi } from "../types/response.types";
|
||||||
|
import { SelectState } from "../types/store.types";
|
||||||
|
|
||||||
|
const api = createAPI();
|
||||||
|
|
||||||
|
export const userRepository: UserRepositoryProps = {
|
||||||
|
async getAll() {
|
||||||
|
// const response = await api.get<ResponseApi<User[]>>("/api/v1/users");
|
||||||
|
|
||||||
|
// return response.data.data as User[];
|
||||||
|
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "John Doe",
|
||||||
|
username: "johndoe",
|
||||||
|
role: "Admin",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Jane Doe",
|
||||||
|
username: "janedoe",
|
||||||
|
role: "Admin",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getUserById(id: string) {
|
||||||
|
// const response = await api.get<ResponseApi<User>>(`/api/v1/users/${id}`);
|
||||||
|
|
||||||
|
// return response.data.data as User;
|
||||||
|
return Promise.resolve({
|
||||||
|
id: "1",
|
||||||
|
name: "John Doe",
|
||||||
|
username: "johndoe",
|
||||||
|
role_id: "1",
|
||||||
|
created_at: "2024-01-01",
|
||||||
|
updated_at: "2024-01-01",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async createUser(user) {
|
||||||
|
const response = await api.post<ResponseApi>("/api/v1/users", user);
|
||||||
|
|
||||||
|
return response.data.status as unknown as Pick<ResponseApi, "status">;
|
||||||
|
},
|
||||||
|
async updateUser(id, user) {
|
||||||
|
const response = await api.put<ResponseApi>(`/api/v1/users/${id}`, user);
|
||||||
|
|
||||||
|
return response.data.status as unknown as Pick<ResponseApi, "status">;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getRoleSelect() {
|
||||||
|
// const response = await api.get<ResponseApi<SelectState[]>>("/api/v1/users/role-select");
|
||||||
|
|
||||||
|
// return (response.data.data as IGetRoleSelect[])?.map((i: IGetRoleSelect) => ({label: i.role_name, value: i.id})) as SelectState[];
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
label: "Admin",
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Teknisi",
|
||||||
|
value: "2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Head",
|
||||||
|
value: "3",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Navigate, Outlet } from "react-router";
|
||||||
|
import { useAuthStore } from "../stores/authStore";
|
||||||
|
|
||||||
|
const RequiredAuth = () => {
|
||||||
|
const { token } = useAuthStore();
|
||||||
|
|
||||||
|
return token ? <Outlet /> : <Navigate to="/login" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequiredAuth;
|
||||||
|
|
@ -1,24 +1,58 @@
|
||||||
import { Routes as RRRoutes, Route } from "react-router";
|
import { Navigate, Routes as RRRoutes, Route } from "react-router";
|
||||||
import App from "../App";
|
|
||||||
import AuthLayout from "../layouts/AuthLayout";
|
import AuthLayout from "../layouts/AuthLayout";
|
||||||
import Login from "../pages/auth/Login";
|
import Login from "../pages/auth/Login";
|
||||||
import Dashboard from "../pages/Dashboard";
|
import Dashboard from "../pages/Dashboard";
|
||||||
import DashboardLayout from "../layouts/DashboardLayout";
|
import DashboardLayout from "../layouts/DashboardLayout";
|
||||||
import Backbone from "../pages/Backbone";
|
import Backbone from "../pages/Backbone";
|
||||||
|
import Devices from "../pages/Devices";
|
||||||
|
import Fishbone from "../pages/Fishbone";
|
||||||
|
import Towers from "../pages/Towers";
|
||||||
|
import RequiredAuth from "./RequiredAuth";
|
||||||
|
import DeviceCreateOrEdit from "../pages/Devices/CreateOrEdit";
|
||||||
|
import FishboneCreateOrEdit from "../pages/Fishbone/CreateOrEdit";
|
||||||
|
import TowerCreateOrEdit from "../pages/Towers/CreateOrEdit";
|
||||||
|
import BackboneCreateOrEdit from "../pages/Backbone/CreateOrEdit";
|
||||||
|
import Users from "../pages/Users";
|
||||||
|
import UserCreateOrEdit from "../pages/Users/CreateOrEdit";
|
||||||
|
|
||||||
export default function Routes() {
|
export default function Routes() {
|
||||||
return (
|
return (
|
||||||
<RRRoutes>
|
<RRRoutes>
|
||||||
<Route path="/" element={<App />} />
|
|
||||||
|
|
||||||
<Route element={<DashboardLayout />}>
|
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
|
||||||
<Route path="/backbone" element={<Backbone />} />
|
|
||||||
</Route>
|
|
||||||
{/* Auth routes */}
|
{/* Auth routes */}
|
||||||
<Route element={<AuthLayout />}>
|
<Route element={<AuthLayout />}>
|
||||||
<Route path="login" element={<Login />} />
|
<Route path="login" element={<Login />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route element={<RequiredAuth />}>
|
||||||
|
<Route path="/" element={<Navigate to={"/dashboard"} />} />
|
||||||
|
|
||||||
|
<Route element={<DashboardLayout />}>
|
||||||
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
|
<Route path="/backbone" element={<Backbone />} />
|
||||||
|
<Route path="/fishbone" element={<Fishbone />} />
|
||||||
|
<Route path="/devices" element={<Devices />} />
|
||||||
|
<Route path="/towers" element={<Towers />} />
|
||||||
|
<Route path="/users" element={<Users />} />
|
||||||
|
|
||||||
|
{/* Create Page */}
|
||||||
|
<Route path="/devices/:pid" element={<DeviceCreateOrEdit />} />
|
||||||
|
<Route path="/devices/create" element={<DeviceCreateOrEdit />} />
|
||||||
|
|
||||||
|
<Route path="/fishbone/:pid" element={<FishboneCreateOrEdit />} />
|
||||||
|
<Route path="/fishbone/create" element={<FishboneCreateOrEdit />} />
|
||||||
|
|
||||||
|
<Route path="/towers/:pid" element={<TowerCreateOrEdit />} />
|
||||||
|
<Route path="/towers/create" element={<TowerCreateOrEdit />} />
|
||||||
|
|
||||||
|
<Route path="/backbone/:pid" element={<BackboneCreateOrEdit />} />
|
||||||
|
<Route path="/backbone/create" element={<BackboneCreateOrEdit />} />
|
||||||
|
|
||||||
|
<Route path="/users/:pid" element={<UserCreateOrEdit />} />
|
||||||
|
<Route path="/users/create" element={<UserCreateOrEdit />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
</Route>
|
||||||
</RRRoutes>
|
</RRRoutes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
|
||||||
|
export const createAPI = (): AxiosInstance => {
|
||||||
|
const { VITE_API_URL } = import.meta.env;
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${VITE_API_URL}/api/v1`,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const token = useAuthStore.getState().token;
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { authRepository } from '../repositories/authRepository';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import { createJSONStorage } from 'zustand/middleware';
|
||||||
|
import { secureStorage } from './storage';
|
||||||
|
import { AuthState } from '../types/auth.types';
|
||||||
|
|
||||||
|
type PersistedAuthState = Pick<AuthState, 'token' | 'user'>;
|
||||||
|
|
||||||
|
export const useAuthStore = create<AuthState>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
token: null,
|
||||||
|
user: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
|
||||||
|
login: async (payload) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const response = await authRepository.login(payload);
|
||||||
|
if (response.data?.token && response.data) {
|
||||||
|
set({ token: response.data.token, user: response.data.user });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
set({ error: response.status.description || 'Login failed' });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({ error: error.message });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: async () => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
await authRepository.logout();
|
||||||
|
// Clear persisted state
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
set({ token: null, user: null, error: null, isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'session',
|
||||||
|
storage: createJSONStorage(() => secureStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
token: state.token,
|
||||||
|
user: state.user,
|
||||||
|
} as PersistedAuthState),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { backboneRepository } from "../repositories/backboneRepository";
|
||||||
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from 'zustand/middleware/immer'
|
||||||
|
import { secureStorage } from "./storage";
|
||||||
|
import { Backbone, BackboneState } from "../types/backbone.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const useBackboneStore = create<BackboneState>()(
|
||||||
|
persist(
|
||||||
|
immer((set, get) => ({
|
||||||
|
data: [],
|
||||||
|
backbone: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
backboneSelect: [],
|
||||||
|
deviceSelect: [],
|
||||||
|
getAll: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const backbones = await backboneRepository.getAll();
|
||||||
|
set({ data: backbones, lastUpdate: dayjs().valueOf() });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find: async (id: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const backbone = await backboneRepository.getBackboneById(id);
|
||||||
|
|
||||||
|
set({ backbone: backbone });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (
|
||||||
|
backbone: Omit<Backbone, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await backboneRepository.createBackbone(backbone);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
backbone: Omit<Backbone, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await backboneRepository.updateBackbone(id, backbone);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDeviceSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const deviceSelect = await backboneRepository.getDeviceSelect();
|
||||||
|
set({ deviceSelect: deviceSelect });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearBackbone: () => {
|
||||||
|
set({ backbone: null })
|
||||||
|
},
|
||||||
|
cacheExpired: () => {
|
||||||
|
return (
|
||||||
|
get().lastUpdate === null ||
|
||||||
|
dayjs(get().lastUpdate).isBefore(
|
||||||
|
dayjs().subtract(
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_VALUE,
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_UNIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ name: "backbones", storage: createJSONStorage(() => secureStorage) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { deviceRepository } from "../repositories/deviceRepository";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from 'zustand/middleware/immer'
|
||||||
|
import { secureStorage } from "./storage";
|
||||||
|
import { Device, DeviceState } from "../types/device.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const useDeviceStore = create<DeviceState>()(
|
||||||
|
persist(
|
||||||
|
immer((set, get) => ({
|
||||||
|
data: [],
|
||||||
|
device: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
towerSelect: [],
|
||||||
|
getAll: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const devices = await deviceRepository.getAll();
|
||||||
|
set({ data: devices, lastUpdate: dayjs().valueOf() });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find: async (id: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const device = await deviceRepository.getDeviceById(id);
|
||||||
|
|
||||||
|
set({ device: device });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (
|
||||||
|
device: Omit<Device, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await deviceRepository.createDevice(device);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
device: Omit<Device, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await deviceRepository.updateDevice(id, device);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getTowerSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const towers = await deviceRepository.getTowerSelect();
|
||||||
|
set({ towerSelect: towers });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearDevice: () => {
|
||||||
|
set({ device: null })
|
||||||
|
},
|
||||||
|
cacheExpired: () => {
|
||||||
|
return (
|
||||||
|
get().lastUpdate === null ||
|
||||||
|
dayjs(get().lastUpdate).isBefore(
|
||||||
|
dayjs().subtract(
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_VALUE,
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_UNIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ name: "devices", storage: createJSONStorage(() => secureStorage) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { fishboneRepository } from "../repositories/fishboneRepository";
|
||||||
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from 'zustand/middleware/immer'
|
||||||
|
import { secureStorage } from "./storage";
|
||||||
|
import { Fishbone, FishboneState } from "../types/fishbone.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const useFishboneStore = create<FishboneState>()(
|
||||||
|
persist(
|
||||||
|
immer((set, get) => ({
|
||||||
|
data: [],
|
||||||
|
fishbone: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
backboneSelect: [],
|
||||||
|
deviceSelect: [],
|
||||||
|
getAll: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const fishbones = await fishboneRepository.getAll();
|
||||||
|
set({ data: fishbones, lastUpdate: dayjs().valueOf() });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find: async (id: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const fishbone = await fishboneRepository.getFishboneById(id);
|
||||||
|
|
||||||
|
set({ fishbone: fishbone });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (
|
||||||
|
fishbone: Omit<Fishbone, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await fishboneRepository.createFishbone(fishbone);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
fishbone: Omit<Fishbone, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await fishboneRepository.updateFishbone(id, fishbone);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getBackboneSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const backboneSelect = await fishboneRepository.getBackboneSelect();
|
||||||
|
set({ backboneSelect: backboneSelect });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDeviceSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const deviceSelect = await fishboneRepository.getDeviceSelect();
|
||||||
|
set({ deviceSelect: deviceSelect });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearFishbone: () => {
|
||||||
|
set({ fishbone: null })
|
||||||
|
},
|
||||||
|
cacheExpired: () => {
|
||||||
|
return (
|
||||||
|
get().lastUpdate === null ||
|
||||||
|
dayjs(get().lastUpdate).isBefore(
|
||||||
|
dayjs().subtract(
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_VALUE,
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_UNIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ name: "fishbones", storage: createJSONStorage(() => secureStorage) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import CryptoJS from "crypto-js";
|
||||||
|
import { StateStorage } from "zustand/middleware";
|
||||||
|
|
||||||
|
const KEY = import.meta.env.VITE_APP_KEY;
|
||||||
|
|
||||||
|
export const secureStorage: StateStorage = {
|
||||||
|
getItem: (name: string): string | null => {
|
||||||
|
const data = localStorage.getItem(name);
|
||||||
|
if (!data) return null;
|
||||||
|
try {
|
||||||
|
const decrypted = CryptoJS.AES.decrypt(data, KEY).toString(
|
||||||
|
CryptoJS.enc.Utf8
|
||||||
|
);
|
||||||
|
return decrypted || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setItem: (name: string, value: string): void => {
|
||||||
|
const encrypted = CryptoJS.AES.encrypt(value, KEY).toString();
|
||||||
|
localStorage.setItem(name, encrypted);
|
||||||
|
},
|
||||||
|
removeItem: (name: string): void => {
|
||||||
|
localStorage.removeItem(name);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { towerRepository } from "../repositories/towerRepository";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
import { secureStorage } from "./storage";
|
||||||
|
import { Tower, TowerState } from "../types/tower.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const useTowerStore = create<TowerState>()(
|
||||||
|
persist(
|
||||||
|
immer((set, get) => ({
|
||||||
|
data: [],
|
||||||
|
tower: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
deviceSelect: [],
|
||||||
|
getAll: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const towers = await towerRepository.getAll();
|
||||||
|
set({ data: towers, lastUpdate: dayjs().valueOf() });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find: async (id: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const tower = await towerRepository.getTowerById(id);
|
||||||
|
|
||||||
|
set({ tower: tower });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (
|
||||||
|
tower: Omit<Tower, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await towerRepository.createTower(tower);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (
|
||||||
|
id: string,
|
||||||
|
tower: Omit<Tower, "id" & "created_at" & "updated_at">
|
||||||
|
) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await towerRepository.updateTower(id, tower);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDeviceSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const towers = await towerRepository.getDeviceSelect();
|
||||||
|
set({ deviceSelect: towers });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearTower: () => {
|
||||||
|
set({ tower: null });
|
||||||
|
},
|
||||||
|
cacheExpired: () => {
|
||||||
|
return (
|
||||||
|
get().lastUpdate === null ||
|
||||||
|
dayjs(get().lastUpdate).isBefore(
|
||||||
|
dayjs().subtract(
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_VALUE,
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_UNIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ name: "towers", storage: createJSONStorage(() => secureStorage) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { userRepository } from "../repositories/userRepository";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
import { createJSONStorage } from "zustand/middleware";
|
||||||
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
import { secureStorage } from "./storage";
|
||||||
|
import { UserRequest, UserState } from "../types/users.types";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
export const useUserStore = create<UserState>()(
|
||||||
|
persist(
|
||||||
|
immer<UserState>((set, get) => ({
|
||||||
|
data: [],
|
||||||
|
user: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdate: null,
|
||||||
|
roleSelect: [],
|
||||||
|
getAll: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const users = await userRepository.getAll();
|
||||||
|
set({ data: users, lastUpdate: dayjs().valueOf() });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false, lastUpdate: dayjs().valueOf() });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
find: async (id: string) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const user = await userRepository.getUserById(id);
|
||||||
|
|
||||||
|
set({ user: user });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: async (user: UserRequest) => {
|
||||||
|
console.log(user);
|
||||||
|
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await userRepository.createUser(user);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: async (id: string, user: UserRequest) => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
await userRepository.updateUser(id, user);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response.status.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getRoleSelect: async () => {
|
||||||
|
set({ isLoading: true });
|
||||||
|
try {
|
||||||
|
const roles = await userRepository.getRoleSelect();
|
||||||
|
set({ roleSelect: roles });
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.status === 401) {
|
||||||
|
set({ error: error.response?.status?.description });
|
||||||
|
} else {
|
||||||
|
set({
|
||||||
|
error: error.response?.status?.description || error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
set({ isLoading: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearError: () => set({ error: null }),
|
||||||
|
clearUser: () => {
|
||||||
|
set({ user: null });
|
||||||
|
},
|
||||||
|
cacheExpired: () => {
|
||||||
|
return (
|
||||||
|
get().lastUpdate === null ||
|
||||||
|
dayjs(get().lastUpdate).isBefore(
|
||||||
|
dayjs().subtract(
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_VALUE,
|
||||||
|
import.meta.env.VITE_APP_CACHE_TIME_UNIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{ name: "users", storage: createJSONStorage(() => secureStorage) }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -32,7 +32,7 @@ export default function Alert(theme: Theme) {
|
||||||
MuiAlert: {
|
MuiAlert: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.mode === 'dark' ? theme.palette.primary.main : theme.palette.text.primary,
|
||||||
fontSize: '0.875rem'
|
fontSize: '0.875rem'
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// MATERIAL - UI
|
// MATERIAL - UI
|
||||||
import { Theme } from '@mui/material/styles';
|
import { Theme } from '@mui/material';
|
||||||
|
|
||||||
// PROJECT IMPORTS
|
// PROJECT IMPORTS
|
||||||
import getColors from '../../utils/getColors';
|
import getColors from '../../utils/getColors';
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ function getSizeStyle(size?: SwitchProps['size']): SwitchSizeProps {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchStyle(theme: Theme, size?: SwitchProps['size']) {
|
function switchStyle(size?: SwitchProps['size']) {
|
||||||
const sizes: SwitchSizeProps = getSizeStyle(size);
|
const sizes: SwitchSizeProps = getSizeStyle(size);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -88,11 +88,11 @@ export default function Switch(theme: Theme) {
|
||||||
'& ~ .MuiFormControlLabel-label': {
|
'& ~ .MuiFormControlLabel-label': {
|
||||||
margin: 6
|
margin: 6
|
||||||
},
|
},
|
||||||
...switchStyle(theme, 'medium')
|
...switchStyle('medium')
|
||||||
},
|
},
|
||||||
sizeLarge: { ...switchStyle(theme, 'large') },
|
sizeLarge: { ...switchStyle('large') },
|
||||||
sizeSmall: {
|
sizeSmall: {
|
||||||
...switchStyle(theme, 'small')
|
...switchStyle('small')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Theme } from '@mui/material';
|
||||||
export default function TableBody(theme: Theme) {
|
export default function TableBody(theme: Theme) {
|
||||||
const hoverStyle = {
|
const hoverStyle = {
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
backgroundColor: theme.palette.secondary.lighter
|
backgroundColor: theme.palette.secondary[theme.palette.mode === 'dark' ? '950' : 'lighter']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -16,7 +16,7 @@ export default function TableBody(theme: Theme) {
|
||||||
root: {
|
root: {
|
||||||
'&.striped .MuiTableRow-root': {
|
'&.striped .MuiTableRow-root': {
|
||||||
'&:nth-of-type(even)': {
|
'&:nth-of-type(even)': {
|
||||||
backgroundColor: theme.palette.secondary.lighter
|
backgroundColor: theme.palette.secondary[theme.palette.mode === 'dark' ? '950' : 'lighter'],
|
||||||
},
|
},
|
||||||
...hoverStyle
|
...hoverStyle
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export default function TableHead(theme: Theme) {
|
||||||
MuiTableHead: {
|
MuiTableHead: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
backgroundColor: theme.palette.secondary.lighter,
|
backgroundColor: theme.palette.secondary[theme.palette.mode === 'dark' ? '950' : 'lighter'],
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderTop: `1px solid ${theme.palette.divider}`,
|
||||||
borderBottom: `2px solid ${theme.palette.divider}`
|
borderBottom: `2px solid ${theme.palette.divider}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// ==============================|| OVERRIDES - TABLE ROW ||============================== //
|
// ==============================|| OVERRIDES - TABLE ROW ||============================== //
|
||||||
|
|
||||||
export default function TableRow() {
|
import { Theme } from "@mui/material";
|
||||||
|
|
||||||
|
export default function TableRow(theme: Theme) {
|
||||||
return {
|
return {
|
||||||
MuiTableRow: {
|
MuiTableRow: {
|
||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
|
|
@ -11,6 +13,10 @@ export default function TableRow() {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'& .MuiTableCell-root': {
|
'& .MuiTableCell-root': {
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: theme.palette.action.hover,
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
'&:last-of-type': {
|
'&:last-of-type': {
|
||||||
paddingRight: 24
|
paddingRight: 24
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ export default function ComponentsOverrides(theme: Theme) {
|
||||||
TableFooter(theme),
|
TableFooter(theme),
|
||||||
TableHead(theme),
|
TableHead(theme),
|
||||||
TablePagination(),
|
TablePagination(),
|
||||||
TableRow(),
|
TableRow(theme),
|
||||||
Tabs(),
|
Tabs(),
|
||||||
ToggleButton(theme),
|
ToggleButton(theme),
|
||||||
Tooltip(theme),
|
Tooltip(theme),
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,13 @@ export default function ThemeCustomization({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const theme = Palette(ThemeMode.LIGHT);
|
const theme = Palette(themeMode);
|
||||||
|
|
||||||
const themeTypography: TypographyVariantsOptions =
|
const themeTypography: TypographyVariantsOptions =
|
||||||
useMemo<TypographyVariantsOptions>(
|
useMemo<TypographyVariantsOptions>(
|
||||||
() => Typography(themeMode, fontFamily, theme),
|
() => Typography(fontFamily),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[themeMode, fontFamily]
|
[fontFamily]
|
||||||
);
|
);
|
||||||
const themeCustomShadows: CustomShadowProps = useMemo<CustomShadowProps>(
|
const themeCustomShadows: CustomShadowProps = useMemo<CustomShadowProps>(
|
||||||
() => CustomShadows(theme),
|
() => CustomShadows(theme),
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,11 @@ const Palette = (mode: ThemeMode) => {
|
||||||
800: "#5c6077",
|
800: "#5c6077",
|
||||||
darker: "#5c6077",
|
darker: "#5c6077",
|
||||||
900: "#4e5161",
|
900: "#4e5161",
|
||||||
|
"950": "#121212",
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
lighter: "#fef3f2",
|
lighter: "#ffe3e1",
|
||||||
light: "#ffe3e1",
|
light: "#ffccc8",
|
||||||
main: "#f44334",
|
main: "#f44334",
|
||||||
dark: "#e22f20",
|
dark: "#e22f20",
|
||||||
darker: "#be2417",
|
darker: "#be2417",
|
||||||
|
|
@ -65,8 +66,8 @@ const Palette = (mode: ThemeMode) => {
|
||||||
'900': '#82221a',
|
'900': '#82221a',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
lighter: "#fffbec",
|
lighter: "#fff6d3",
|
||||||
light: "#ffd96d",
|
light: "#ffeaa5",
|
||||||
main: "#fb8c00",
|
main: "#fb8c00",
|
||||||
dark: "#cc6902",
|
dark: "#cc6902",
|
||||||
darker: "#a1500b",
|
darker: "#a1500b",
|
||||||
|
|
@ -82,7 +83,7 @@ const Palette = (mode: ThemeMode) => {
|
||||||
'900': '#82430c'
|
'900': '#82430c'
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
lighter: "#ecfdff",
|
lighter: "#d0f6fd",
|
||||||
light: "#a7edfa",
|
light: "#a7edfa",
|
||||||
main: "#16c0e8",
|
main: "#16c0e8",
|
||||||
dark: "#106c8e",
|
dark: "#106c8e",
|
||||||
|
|
@ -99,8 +100,8 @@ const Palette = (mode: ThemeMode) => {
|
||||||
'900': '#174962',
|
'900': '#174962',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
lighter: "#f3faf3",
|
lighter: "#e3f5e3",
|
||||||
light: "#e3f5e3",
|
light: "#c8eac9",
|
||||||
main: "#4caf50",
|
main: "#4caf50",
|
||||||
dark: "#358438",
|
dark: "#358438",
|
||||||
darker: "#2d6830",
|
darker: "#2d6830",
|
||||||
|
|
@ -113,7 +114,7 @@ const Palette = (mode: ThemeMode) => {
|
||||||
'600': '#358438',
|
'600': '#358438',
|
||||||
'700': '#2d6830',
|
'700': '#2d6830',
|
||||||
'800': '#275429',
|
'800': '#275429',
|
||||||
'900': '#224525'
|
'900': '#224525',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -126,17 +127,18 @@ const Palette = (mode: ThemeMode) => {
|
||||||
},
|
},
|
||||||
...paletteColor,
|
...paletteColor,
|
||||||
text: {
|
text: {
|
||||||
primary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.87) : paletteColor.secondary[800],
|
primary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.87) : paletteColor.secondary[800],
|
||||||
secondary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.45) : paletteColor.secondary.main,
|
secondary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.45) : paletteColor.secondary.main,
|
||||||
disabled: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.1) : paletteColor.secondary[400]
|
disabled: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.1) : paletteColor.secondary[400]
|
||||||
},
|
},
|
||||||
action: {
|
action: {
|
||||||
disabled: paletteColor.secondary.light
|
disabled: paletteColor.secondary.light,
|
||||||
|
hover: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 1) : alpha(paletteColor.secondary.light!, 0.48),
|
||||||
},
|
},
|
||||||
divider: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.05) : alpha(paletteColor.secondary.light!, 0.65),
|
divider: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.09) : alpha(paletteColor.secondary.light!, 0.65),
|
||||||
background: {
|
background: {
|
||||||
paper: mode === ThemeMode.DARK ? paletteColor.secondary[100] : '#fff',
|
paper: mode === ThemeMode.DARK ? paletteColor.secondary[950] : '#fff',
|
||||||
default: mode !== ThemeMode.DARK ? '#fff' : paletteColor.secondary.lighter
|
default: mode !== ThemeMode.DARK ? '#fff' : paletteColor.secondary[950]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,11 @@ const CustomShadows = (theme: Theme): CustomShadowProps => ({
|
||||||
text: `0 -1px 0 rgb(0 0 0 / 12%)`,
|
text: `0 -1px 0 rgb(0 0 0 / 12%)`,
|
||||||
z1:
|
z1:
|
||||||
theme.palette.mode === ThemeMode.DARK
|
theme.palette.mode === ThemeMode.DARK
|
||||||
? `0px 8px 24px ${alpha(theme.palette.secondary[200]!, 0.3)}`
|
? `0px 4px 20px ${alpha(theme.palette.secondary[800]!, 0.3)}`
|
||||||
: `0px 8px 24px ${alpha(theme.palette.secondary.dark, 0.15)}`,
|
: `0px 8px 24px ${alpha(theme.palette.secondary.dark, 0.15)}`,
|
||||||
z2:
|
z2:
|
||||||
theme.palette.mode === ThemeMode.DARK
|
theme.palette.mode === ThemeMode.DARK
|
||||||
? `0px 2px 8px ${alpha(theme.palette.secondary[200]!, 0.3)}`
|
? `0px 2px 8px ${alpha(theme.palette.secondary[800]!, 0.3)}`
|
||||||
: `0px 2px 8px ${alpha(theme.palette.secondary.dark, 0.08)}`,
|
: `0px 2px 8px ${alpha(theme.palette.secondary.dark, 0.08)}`,
|
||||||
primary: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.1)}`,
|
primary: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||||
secondary: `0 0 0 2px ${alpha(theme.palette.secondary.main, 0.2)}`,
|
secondary: `0 0 0 2px ${alpha(theme.palette.secondary.main, 0.2)}`,
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// MATERIAL - UI
|
// MATERIAL - UI
|
||||||
import { Theme, TypographyVariantsOptions } from '@mui/material/styles';
|
import { TypographyVariantsOptions } from '@mui/material';
|
||||||
import { FontFamily, ThemeMode } from '../types/config';
|
import { FontFamily } from '../types/config';
|
||||||
|
|
||||||
// ==============================|| DEFAULT THEME - TYPOGRAPHY ||============================== //
|
// ==============================|| DEFAULT THEME - TYPOGRAPHY ||============================== //
|
||||||
|
|
||||||
const Typography = (mode: ThemeMode, fontFamily: FontFamily, theme: Theme): TypographyVariantsOptions => ({
|
const Typography = (fontFamily: FontFamily): TypographyVariantsOptions => ({
|
||||||
htmlFontSize: 16,
|
htmlFontSize: 16,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontWeightLight: 300,
|
fontWeightLight: 300,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { ErrorState } from "./store.types";
|
||||||
|
|
||||||
|
export interface LoginPayload {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
status: {
|
||||||
|
code: number;
|
||||||
|
description: string;
|
||||||
|
},
|
||||||
|
data?: {
|
||||||
|
token: string;
|
||||||
|
user: UserProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth state
|
||||||
|
* ===========
|
||||||
|
*/
|
||||||
|
export interface AuthState extends ErrorState<AuthErrorInputState> {
|
||||||
|
token: string | null;
|
||||||
|
user: UserProfile | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
login: (payload: LoginPayload) => Promise<boolean>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthErrorInputState {
|
||||||
|
username?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState, SelectState } from "./store.types";
|
||||||
|
|
||||||
|
export interface BackboneRepositoryProps {
|
||||||
|
getAll: () => Promise<BackboneDataTable[] | []>;
|
||||||
|
getBackboneById: (id: string) => Promise<Backbone>;
|
||||||
|
createBackbone: (backbone: BackboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
updateBackbone: (id: string, backbone: BackboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
getDeviceSelect: () => Promise<SelectState[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Backbone {
|
||||||
|
id: string;
|
||||||
|
backbone_code: string;
|
||||||
|
longitude: string | number;
|
||||||
|
latitude: string | number;
|
||||||
|
dev_start_id?: string | number | null;
|
||||||
|
dev_end_id?: string | number | null;
|
||||||
|
core_amount: string | number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackboneRequest extends Omit<Backbone, "id" & "created_at" & "updated_at"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backbone State
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
export interface BackboneState extends ErrorState, CacheState {
|
||||||
|
data?: BackboneDataTable[];
|
||||||
|
backbone?: Backbone | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastUpdate: number | null;
|
||||||
|
deviceSelect: SelectState[] | null;
|
||||||
|
getDeviceSelect: () => Promise<void>;
|
||||||
|
getAll: () => Promise<void>;
|
||||||
|
find: (id: string) => Promise<void>;
|
||||||
|
create: (backbone: BackboneRequest) => Promise<void>;
|
||||||
|
update: (id: string, backbone: BackboneRequest) => Promise<void>;
|
||||||
|
clearBackbone: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackboneDataTable extends Pick<Backbone, "id" | "backbone_code" | "longitude" | "latitude" | "core_amount"> {
|
||||||
|
device_start: string;
|
||||||
|
device_end: string;
|
||||||
|
total_fishbone: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetDeviceSelect {
|
||||||
|
id: string;
|
||||||
|
device_code: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState, SelectState } from "./store.types";
|
||||||
|
|
||||||
|
export interface DeviceRepositoryProps {
|
||||||
|
getAll: () => Promise<DeviceDataTable[] | []>;
|
||||||
|
getDeviceById: (id: string) => Promise<Device>;
|
||||||
|
createDevice: (device: DeviceRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
updateDevice: (id: string, device: DeviceRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
getTowerSelect: () => Promise<SelectState[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Device {
|
||||||
|
id: string;
|
||||||
|
device_code: string;
|
||||||
|
device_type: string;
|
||||||
|
longitude: string | number;
|
||||||
|
latitude: string | number;
|
||||||
|
port_amount: number;
|
||||||
|
status: DeviceStatus;
|
||||||
|
tower_id: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DeviceStatus = "active" | "inactive" | "maintenance";
|
||||||
|
|
||||||
|
export interface DeviceRequest extends Omit<Device, "id" & "created_at" & "updated_at"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Device State
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
export interface DeviceState extends ErrorState, CacheState {
|
||||||
|
data?: DeviceDataTable[];
|
||||||
|
device?: Device | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastUpdate: number | null;
|
||||||
|
towerSelect: SelectState[] | null;
|
||||||
|
getTowerSelect: () => Promise<void>;
|
||||||
|
getAll: () => Promise<void>;
|
||||||
|
find: (id: string) => Promise<void>;
|
||||||
|
create: (device: DeviceRequest) => Promise<void>;
|
||||||
|
update: (id: string, device: DeviceRequest) => Promise<void>;
|
||||||
|
clearDevice: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceDataTable extends Pick<Device, "id" | "device_code" | "device_type" | "port_amount" | "status"> {
|
||||||
|
address: string;
|
||||||
|
total_used_port: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetTowerSelect {
|
||||||
|
id: string;
|
||||||
|
tower_code: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState, SelectState } from "./store.types";
|
||||||
|
|
||||||
|
export interface FishboneRepositoryProps {
|
||||||
|
getAll: () => Promise<FishboneDataTable[] | []>;
|
||||||
|
getFishboneById: (id: string) => Promise<Fishbone>;
|
||||||
|
createFishbone: (fishbone: FishboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
updateFishbone: (id: string, fishbone: FishboneRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
getBackboneSelect: () => Promise<SelectState[] | null>;
|
||||||
|
getDeviceSelect: () => Promise<SelectState[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Fishbone {
|
||||||
|
id: string;
|
||||||
|
backbone_id: string;
|
||||||
|
backbone_core: string;
|
||||||
|
dev_start_id: string;
|
||||||
|
dev_end_id: string;
|
||||||
|
longitude: string | number;
|
||||||
|
latitude: string | number;
|
||||||
|
core_amount: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FishboneRequest extends Omit<Fishbone, "id" & "created_at" & "updated_at"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fishbone State
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
export interface FishboneState extends ErrorState, CacheState {
|
||||||
|
data?: FishboneDataTable[];
|
||||||
|
fishbone?: Fishbone | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastUpdate: number | null;
|
||||||
|
backboneSelect: SelectState[] | null;
|
||||||
|
deviceSelect: SelectState[] | null;
|
||||||
|
getBackboneSelect: () => Promise<void>;
|
||||||
|
getDeviceSelect: () => Promise<void>;
|
||||||
|
getAll: () => Promise<void>;
|
||||||
|
find: (id: string) => Promise<void>;
|
||||||
|
create: (fishbone: FishboneRequest) => Promise<void>;
|
||||||
|
update: (id: string, fishbone: FishboneRequest) => Promise<void>;
|
||||||
|
clearFishbone: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FishboneDataTable extends Pick<Fishbone, "id" | "core_amount" | "longitude" | "latitude"> {
|
||||||
|
backbone: string;
|
||||||
|
address: string;
|
||||||
|
device_start: string;
|
||||||
|
device_end: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetBackboneSelect {
|
||||||
|
id: string;
|
||||||
|
backbone_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetDeviceSelect {
|
||||||
|
id: string;
|
||||||
|
device_code: string;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
import * as createPalette from '@mui/material';
|
import * as createPalette from '@mui/material';
|
||||||
|
|
||||||
declare module '@mui/material/styles' {
|
declare module '@mui/material' {
|
||||||
interface SimplePaletteColorOptions {
|
interface SimplePaletteColorOptions {
|
||||||
lighter?: string;
|
lighter?: string;
|
||||||
darker?: string;
|
darker?: string;
|
||||||
|
|
@ -16,6 +16,7 @@ declare module '@mui/material/styles' {
|
||||||
700?: string;
|
700?: string;
|
||||||
800?: string;
|
800?: string;
|
||||||
900?: string;
|
900?: string;
|
||||||
|
950?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaletteColor {
|
interface PaletteColor {
|
||||||
|
|
@ -32,5 +33,6 @@ declare module '@mui/material/styles' {
|
||||||
700?: string;
|
700?: string;
|
||||||
800?: string;
|
800?: string;
|
||||||
900?: string;
|
900?: string;
|
||||||
|
950?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
interface ResponseApiStatus {
|
||||||
|
status: string | number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseApi<T = null> {
|
||||||
|
status: ResponseApiStatus;
|
||||||
|
message: string;
|
||||||
|
data?: T | [] | null;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
export interface ErrorState<T = undefined> {
|
||||||
|
error: string | null | T;
|
||||||
|
clearError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CacheState {
|
||||||
|
cacheExpired: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectState {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState, SelectState } from "./store.types";
|
||||||
|
|
||||||
|
export interface TowerRepositoryProps {
|
||||||
|
getAll: () => Promise<TowerDataTable[] | []>;
|
||||||
|
getTowerById: (id: string) => Promise<Tower>;
|
||||||
|
createTower: (tower: TowerRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
updateTower: (id: string, tower: TowerRequest) => Promise<Pick<ResponseApi<any>, "status">>;
|
||||||
|
getDeviceSelect: () => Promise<SelectState[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tower {
|
||||||
|
id: string;
|
||||||
|
tower_code: string;
|
||||||
|
longitude: string | number;
|
||||||
|
latitude: string | number;
|
||||||
|
device_id?: string | number | null
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TowerRequest extends Omit<Tower, "id" & "created_at" & "updated_at"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tower State
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
export interface TowerState extends ErrorState, CacheState {
|
||||||
|
data?: TowerDataTable[];
|
||||||
|
tower?: Tower | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastUpdate: number | null;
|
||||||
|
deviceSelect: SelectState[] | null;
|
||||||
|
getDeviceSelect: () => Promise<void>;
|
||||||
|
getAll: () => Promise<void>;
|
||||||
|
find: (id: string) => Promise<void>;
|
||||||
|
create: (tower: TowerRequest) => Promise<void>;
|
||||||
|
update: (id: string, tower: TowerRequest) => Promise<void>;
|
||||||
|
clearTower: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TowerDataTable extends Pick<Tower, "id" | "tower_code" | "longitude" | "latitude"> {
|
||||||
|
address: string;
|
||||||
|
device_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetDeviceSelect {
|
||||||
|
id: string;
|
||||||
|
device_code: string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { ResponseApi } from "./response.types";
|
||||||
|
import { CacheState, ErrorState, SelectState } from "./store.types";
|
||||||
|
|
||||||
|
export interface UserRepositoryProps {
|
||||||
|
getAll: () => Promise<UserDataTable[] | []>;
|
||||||
|
getUserById: (id: string) => Promise<User>;
|
||||||
|
createUser: (user: UserRequest) => Promise<Pick<ResponseApi, "status">>;
|
||||||
|
updateUser: (id: string, user: UserRequest) => Promise<Pick<ResponseApi, "status">>;
|
||||||
|
getRoleSelect: () => Promise<SelectState[] | null>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
role_id: string | number;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
password?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserRequest extends Pick<User, "name" & "username" & "password"> {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User State
|
||||||
|
* =============
|
||||||
|
*/
|
||||||
|
export interface UserState extends ErrorState, CacheState {
|
||||||
|
data?: UserDataTable[];
|
||||||
|
user?: User | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
lastUpdate: number | null;
|
||||||
|
roleSelect: SelectState[] | null;
|
||||||
|
getRoleSelect: () => Promise<void>;
|
||||||
|
getAll: () => Promise<void>;
|
||||||
|
find: (id: string) => Promise<void>;
|
||||||
|
create: (user: UserRequest) => Promise<void>;
|
||||||
|
update: (id: string, user: UserRequest) => Promise<void>;
|
||||||
|
clearUser: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserDataTable extends Pick<User, "id" | "name" | "username"> {
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetRoleSelect {
|
||||||
|
id: string;
|
||||||
|
role_name: string;
|
||||||
|
}
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
import { ManipulateType } from "dayjs"
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_KEY: string
|
||||||
|
readonly VITE_API_URL: string
|
||||||
|
readonly VITE_APP_CACHE_TIME_VALUE: number
|
||||||
|
readonly VITE_APP_CACHE_TIME_UNIT: ManipulateType
|
||||||
|
// more env variables...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue