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
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
|
|
@ -12,16 +12,25 @@
|
|||
"@emotion/styled": "^11.14.0",
|
||||
"@fontsource/public-sans": "^5.1.2",
|
||||
"@mui/icons-material": "^6.4.3",
|
||||
"@mui/lab": "^6.0.0-beta.27",
|
||||
"@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",
|
||||
"immer": "^10.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.1.5",
|
||||
"react-svg": "^16.3.0"
|
||||
"react-svg": "^16.3.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
|
|
@ -993,6 +1002,40 @@
|
|||
"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": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.1.2.tgz",
|
||||
|
|
@ -1102,10 +1145,42 @@
|
|||
"@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": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.3.tgz",
|
||||
"integrity": "sha512-hlyOzo2ObarllAOeT1ZSAusADE5NZNencUeIvXrdQ1Na+FL1lcznhbxfV5He1KqGiuR8Az3xtCUcYKwMVGFdzg==",
|
||||
"version": "6.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.4.tgz",
|
||||
"integrity": "sha512-r+J0EditrekkTtO2CnCBCOGpNaDYwJqz8lH4rj6o/anDcskZFJodBlG8aCJkS8DL/CF/9EHS+Gz53EbmYEnQbw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
|
|
@ -1136,13 +1211,57 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.3.tgz",
|
||||
"integrity": "sha512-ubtQjplbWneIEU8Y+4b2VA0CDBlyH5I3AmVFGmsLyDe/bf0ubxav5t11c8Afem6rkSFWPlZA2DilxmGka1xiKQ==",
|
||||
"node_modules/@mui/lab": {
|
||||
"version": "6.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.27.tgz",
|
||||
"integrity": "sha512-weLxPsCs2wJKgWKf46shXHE+x7qlf5VxMK3P+4HsWasMakV/uTmxsoT7PG3QCvakGQ2TdpZtQLE2umJKC0mvKQ==",
|
||||
"dependencies": {
|
||||
"@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/types": "^7.2.21",
|
||||
"@mui/utils": "^6.4.3",
|
||||
|
|
@ -1625,6 +1744,37 @@
|
|||
"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": {
|
||||
"version": "7.20.5",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
|
|
@ -1689,6 +1845,12 @@
|
|||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -2218,11 +2406,21 @@
|
|||
"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": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"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": {
|
||||
"version": "4.4.0",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
|
|
@ -2614,6 +2820,38 @@
|
|||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
||||
"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": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
|
|
@ -2728,6 +2966,15 @@
|
|||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
|
|
@ -2965,6 +3212,25 @@
|
|||
"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": {
|
||||
"version": "3.1.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
|
@ -3756,6 +4027,34 @@
|
|||
"funding": {
|
||||
"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",
|
||||
"@fontsource/public-sans": "^5.1.2",
|
||||
"@mui/icons-material": "^6.4.3",
|
||||
"@mui/lab": "^6.0.0-beta.27",
|
||||
"@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",
|
||||
"immer": "^10.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.1.5",
|
||||
"react-svg": "^16.3.0"
|
||||
"react-svg": "^16.3.0",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"@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 Box from "@mui/material/Box";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useAuthStore } from "../stores/authStore";
|
||||
|
||||
export default function AuthLayout() {
|
||||
const theme = useTheme();
|
||||
const { token } = useAuthStore();
|
||||
|
||||
if (token) {
|
||||
window.location.href = "/dashboard";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
|
|
|||
|
|
@ -5,19 +5,17 @@ import Menu from "@mui/material/Menu";
|
|||
import MenuItem from "@mui/material/MenuItem";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
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 { useNavigate } from "react-router";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
|
||||
export default function Header({
|
||||
handleDrawerToggle,
|
||||
}: {
|
||||
handleDrawerToggle: () => void;
|
||||
}) {
|
||||
const { user, logout } = useAuthStore();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
|
|
@ -27,10 +25,10 @@ export default function Header({
|
|||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const onLogout = () => {
|
||||
const onLogout = async () => {
|
||||
setAnchorEl(null);
|
||||
|
||||
navigate("/login")
|
||||
await logout();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -63,7 +61,7 @@ export default function Header({
|
|||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" noWrap component="div">
|
||||
Hi, Admins
|
||||
Hi, {user?.name || ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Menu
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import Drawer from "@mui/material/Drawer";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Divider from "@mui/material/Divider";
|
||||
import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListSubheader from "@mui/material/ListSubheader";
|
||||
|
|
@ -13,6 +12,11 @@ import { Link, Typography } from "@mui/material";
|
|||
import { useLocation } from "react-router";
|
||||
import { ReactSVG } from "react-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 {
|
||||
/**
|
||||
|
|
@ -38,17 +42,22 @@ const MENU = [
|
|||
{
|
||||
title: "Backbone",
|
||||
link: "/backbone",
|
||||
icon: Speedometer,
|
||||
icon: Backbone,
|
||||
},
|
||||
{
|
||||
title: "Fishbone",
|
||||
link: "/fishbone",
|
||||
icon: Speedometer,
|
||||
icon: Fishbone,
|
||||
},
|
||||
{
|
||||
title: "Devices",
|
||||
link: "/devices",
|
||||
icon: Speedometer,
|
||||
icon: Device,
|
||||
},
|
||||
{
|
||||
title: "Tower",
|
||||
link: "/towers",
|
||||
icon: Tower,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -57,8 +66,8 @@ const MENU = [
|
|||
menus: [
|
||||
{
|
||||
title: "User",
|
||||
link: "/user",
|
||||
icon: Speedometer,
|
||||
link: "/users",
|
||||
icon: Users,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
@ -108,7 +117,7 @@ export default function Sidebar({
|
|||
<ListItemButton
|
||||
component={Link}
|
||||
href={menu.link}
|
||||
selected={menu.link === location.pathname}
|
||||
selected={location.pathname.startsWith(menu.link)}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ReactSVG src={menu.icon} />
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ export default function DashboardLayout(props: Props) {
|
|||
<Box sx={{ display: "flex" }}>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
// color="default"
|
||||
position="fixed"
|
||||
enableColorOnDark
|
||||
sx={{
|
||||
width: { sm: `calc(100% - ${drawerWidth}px)` },
|
||||
ml: { sm: `${drawerWidth}px` },
|
||||
|
|
|
|||
|
|
@ -3,13 +3,79 @@ 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 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() {
|
||||
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 (
|
||||
<Box component="main">
|
||||
<Grid container spacing={2} alignItems={"center"} mb={4}>
|
||||
|
|
@ -17,7 +83,7 @@ export default function Backbone() {
|
|||
<Typography variant="h3">Backbone</Typography>
|
||||
</Grid>
|
||||
<Grid size="auto">
|
||||
<Button variant="contained" startIcon={<AddIcon color="inherit" />}>
|
||||
<Button variant="contained" startIcon={<AddIcon color="inherit" />} href="/backbone/create">
|
||||
Add New
|
||||
</Button>
|
||||
</Grid>
|
||||
|
|
@ -31,18 +97,15 @@ export default function Backbone() {
|
|||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Card sx={{ display: "flex" }}>
|
||||
<CardContent sx={{ flex: "1 0 auto" }}>
|
||||
<Typography component="div" variant="h5">
|
||||
Live From Space
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
sx={{ color: "text.secondary" }}
|
||||
>
|
||||
Mac Miller
|
||||
</Typography>
|
||||
{/* <FlashMessage store={useBackboneStore} /> */}
|
||||
|
||||
<Card>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography variant="h5">List Backbone</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 { 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() {
|
||||
useTitle("Dashboard");
|
||||
|
||||
return (
|
||||
<div>
|
||||
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 Checkbox from "@mui/material/Checkbox";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Link from "@mui/material/Link";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuthStore } from "../../stores/authStore";
|
||||
import useTitle from "../../hooks/useTitle";
|
||||
|
||||
export default function Login() {
|
||||
useTitle("Login");
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
const onSubmit = (e: any) => {
|
||||
const { login, token, isLoading, error } = useAuthStore();
|
||||
|
||||
const onSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
|
||||
navigate("/dashboard");
|
||||
await login({
|
||||
username: e.target.username.value,
|
||||
password: e.target.password.value,
|
||||
});
|
||||
|
||||
if (token) {
|
||||
navigate("/dashboard");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -34,6 +46,8 @@ export default function Login() {
|
|||
name="username"
|
||||
autoComplete="username"
|
||||
autoFocus
|
||||
error={!!error}
|
||||
helperText={error?.toString()}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
|
|
@ -58,7 +72,7 @@ export default function Login() {
|
|||
</Link>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
<Button type="submit" fullWidth variant="contained" sx={{ my: 2 }}>
|
||||
<Button type="submit" fullWidth variant="contained" sx={{ my: 2 }} disabled={isLoading}>
|
||||
Sign In
|
||||
</Button>
|
||||
</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 App from "../App";
|
||||
import { Navigate, Routes as RRRoutes, Route } from "react-router";
|
||||
import AuthLayout from "../layouts/AuthLayout";
|
||||
import Login from "../pages/auth/Login";
|
||||
import Dashboard from "../pages/Dashboard";
|
||||
import DashboardLayout from "../layouts/DashboardLayout";
|
||||
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() {
|
||||
return (
|
||||
<RRRoutes>
|
||||
<Route path="/" element={<App />} />
|
||||
|
||||
<Route element={<DashboardLayout />}>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/backbone" element={<Backbone />} />
|
||||
</Route>
|
||||
{/* Auth routes */}
|
||||
<Route element={<AuthLayout />}>
|
||||
<Route path="login" element={<Login />} />
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: theme.palette.text.primary,
|
||||
color: theme.palette.mode === 'dark' ? theme.palette.primary.main : theme.palette.text.primary,
|
||||
fontSize: '0.875rem'
|
||||
},
|
||||
icon: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// MATERIAL - UI
|
||||
import { Theme } from '@mui/material/styles';
|
||||
import { Theme } from '@mui/material';
|
||||
|
||||
// PROJECT IMPORTS
|
||||
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);
|
||||
|
||||
return {
|
||||
|
|
@ -88,11 +88,11 @@ export default function Switch(theme: Theme) {
|
|||
'& ~ .MuiFormControlLabel-label': {
|
||||
margin: 6
|
||||
},
|
||||
...switchStyle(theme, 'medium')
|
||||
...switchStyle('medium')
|
||||
},
|
||||
sizeLarge: { ...switchStyle(theme, 'large') },
|
||||
sizeLarge: { ...switchStyle('large') },
|
||||
sizeSmall: {
|
||||
...switchStyle(theme, 'small')
|
||||
...switchStyle('small')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Theme } from '@mui/material';
|
|||
export default function TableBody(theme: Theme) {
|
||||
const hoverStyle = {
|
||||
'&: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: {
|
||||
'&.striped .MuiTableRow-root': {
|
||||
'&:nth-of-type(even)': {
|
||||
backgroundColor: theme.palette.secondary.lighter
|
||||
backgroundColor: theme.palette.secondary[theme.palette.mode === 'dark' ? '950' : 'lighter'],
|
||||
},
|
||||
...hoverStyle
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ export default function TableHead(theme: Theme) {
|
|||
MuiTableHead: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundColor: theme.palette.secondary.lighter,
|
||||
backgroundColor: theme.palette.secondary[theme.palette.mode === 'dark' ? '950' : 'lighter'],
|
||||
borderTop: `1px solid ${theme.palette.divider}`,
|
||||
borderBottom: `2px solid ${theme.palette.divider}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// ==============================|| OVERRIDES - TABLE ROW ||============================== //
|
||||
|
||||
export default function TableRow() {
|
||||
import { Theme } from "@mui/material";
|
||||
|
||||
export default function TableRow(theme: Theme) {
|
||||
return {
|
||||
MuiTableRow: {
|
||||
styleOverrides: {
|
||||
|
|
@ -11,6 +13,10 @@ export default function TableRow() {
|
|||
}
|
||||
},
|
||||
'& .MuiTableCell-root': {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
cursor: 'pointer'
|
||||
},
|
||||
'&:last-of-type': {
|
||||
paddingRight: 24
|
||||
},
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export default function ComponentsOverrides(theme: Theme) {
|
|||
TableFooter(theme),
|
||||
TableHead(theme),
|
||||
TablePagination(),
|
||||
TableRow(),
|
||||
TableRow(theme),
|
||||
Tabs(),
|
||||
ToggleButton(theme),
|
||||
Tooltip(theme),
|
||||
|
|
|
|||
|
|
@ -46,13 +46,13 @@ export default function ThemeCustomization({
|
|||
}
|
||||
}
|
||||
|
||||
const theme = Palette(ThemeMode.LIGHT);
|
||||
const theme = Palette(themeMode);
|
||||
|
||||
const themeTypography: TypographyVariantsOptions =
|
||||
useMemo<TypographyVariantsOptions>(
|
||||
() => Typography(themeMode, fontFamily, theme),
|
||||
() => Typography(fontFamily),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[themeMode, fontFamily]
|
||||
[fontFamily]
|
||||
);
|
||||
const themeCustomShadows: CustomShadowProps = useMemo<CustomShadowProps>(
|
||||
() => CustomShadows(theme),
|
||||
|
|
|
|||
|
|
@ -46,10 +46,11 @@ const Palette = (mode: ThemeMode) => {
|
|||
800: "#5c6077",
|
||||
darker: "#5c6077",
|
||||
900: "#4e5161",
|
||||
"950": "#121212",
|
||||
},
|
||||
error: {
|
||||
lighter: "#fef3f2",
|
||||
light: "#ffe3e1",
|
||||
lighter: "#ffe3e1",
|
||||
light: "#ffccc8",
|
||||
main: "#f44334",
|
||||
dark: "#e22f20",
|
||||
darker: "#be2417",
|
||||
|
|
@ -65,8 +66,8 @@ const Palette = (mode: ThemeMode) => {
|
|||
'900': '#82221a',
|
||||
},
|
||||
warning: {
|
||||
lighter: "#fffbec",
|
||||
light: "#ffd96d",
|
||||
lighter: "#fff6d3",
|
||||
light: "#ffeaa5",
|
||||
main: "#fb8c00",
|
||||
dark: "#cc6902",
|
||||
darker: "#a1500b",
|
||||
|
|
@ -82,7 +83,7 @@ const Palette = (mode: ThemeMode) => {
|
|||
'900': '#82430c'
|
||||
},
|
||||
info: {
|
||||
lighter: "#ecfdff",
|
||||
lighter: "#d0f6fd",
|
||||
light: "#a7edfa",
|
||||
main: "#16c0e8",
|
||||
dark: "#106c8e",
|
||||
|
|
@ -99,8 +100,8 @@ const Palette = (mode: ThemeMode) => {
|
|||
'900': '#174962',
|
||||
},
|
||||
success: {
|
||||
lighter: "#f3faf3",
|
||||
light: "#e3f5e3",
|
||||
lighter: "#e3f5e3",
|
||||
light: "#c8eac9",
|
||||
main: "#4caf50",
|
||||
dark: "#358438",
|
||||
darker: "#2d6830",
|
||||
|
|
@ -113,7 +114,7 @@ const Palette = (mode: ThemeMode) => {
|
|||
'600': '#358438',
|
||||
'700': '#2d6830',
|
||||
'800': '#275429',
|
||||
'900': '#224525'
|
||||
'900': '#224525',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -126,17 +127,18 @@ const Palette = (mode: ThemeMode) => {
|
|||
},
|
||||
...paletteColor,
|
||||
text: {
|
||||
primary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.87) : paletteColor.secondary[800],
|
||||
secondary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.45) : paletteColor.secondary.main,
|
||||
disabled: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.darker!, 0.1) : paletteColor.secondary[400]
|
||||
primary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.87) : paletteColor.secondary[800],
|
||||
secondary: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.45) : paletteColor.secondary.main,
|
||||
disabled: mode === ThemeMode.DARK ? alpha(paletteColor.secondary.lighter!, 0.1) : paletteColor.secondary[400]
|
||||
},
|
||||
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: {
|
||||
paper: mode === ThemeMode.DARK ? paletteColor.secondary[100] : '#fff',
|
||||
default: mode !== ThemeMode.DARK ? '#fff' : paletteColor.secondary.lighter
|
||||
paper: mode === ThemeMode.DARK ? paletteColor.secondary[950] : '#fff',
|
||||
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%)`,
|
||||
z1:
|
||||
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)}`,
|
||||
z2:
|
||||
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)}`,
|
||||
primary: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.1)}`,
|
||||
secondary: `0 0 0 2px ${alpha(theme.palette.secondary.main, 0.2)}`,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// MATERIAL - UI
|
||||
import { Theme, TypographyVariantsOptions } from '@mui/material/styles';
|
||||
import { FontFamily, ThemeMode } from '../types/config';
|
||||
import { TypographyVariantsOptions } from '@mui/material';
|
||||
import { FontFamily } from '../types/config';
|
||||
|
||||
// ==============================|| DEFAULT THEME - TYPOGRAPHY ||============================== //
|
||||
|
||||
const Typography = (mode: ThemeMode, fontFamily: FontFamily, theme: Theme): TypographyVariantsOptions => ({
|
||||
const Typography = (fontFamily: FontFamily): TypographyVariantsOptions => ({
|
||||
htmlFontSize: 16,
|
||||
fontFamily,
|
||||
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
|
||||
import * as createPalette from '@mui/material';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
declare module '@mui/material' {
|
||||
interface SimplePaletteColorOptions {
|
||||
lighter?: string;
|
||||
darker?: string;
|
||||
|
|
@ -16,6 +16,7 @@ declare module '@mui/material/styles' {
|
|||
700?: string;
|
||||
800?: string;
|
||||
900?: string;
|
||||
950?: string;
|
||||
}
|
||||
|
||||
interface PaletteColor {
|
||||
|
|
@ -32,5 +33,6 @@ declare module '@mui/material/styles' {
|
|||
700?: string;
|
||||
800?: 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,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"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