Compare commits

...

10 Commits

Author SHA1 Message Date
HasanMu ce874def91 feat: imp mock api for user management 2025-02-19 09:51:47 +07:00
HasanMu ed88b1ed27 feat: impl mock api for tower, backbone and change icon sidebar menu 2025-02-18 14:45:03 +07:00
HasanMu f8aa6ad785 feat: impl mock api for devices & fishbone 2025-02-17 18:40:40 +07:00
HasanMu 167ec6b943 fix: dark mode color 2025-02-17 18:39:54 +07:00
HasanMu 778b05fd50 feat: impl cache & crud mock in devices 2025-02-14 17:00:08 +07:00
HasanMu 5e471b7e20 wip: impl crud devices 2025-02-13 17:27:59 +07:00
HasanMu 8a2a93a288 feat: add hooks useTitle for set title 2025-02-13 09:29:01 +07:00
HasanMu fe41e1e45a fix: unused import / params for build 2025-02-13 09:10:10 +07:00
HasanMu 77c4b0cb1a feat: add zustand for state management & implement for auth 2025-02-12 18:23:50 +07:00
HasanMu e62e678fba feat: styling index page backbone, fishbone, devices & tower 2025-02-12 16:09:24 +07:00
72 changed files with 4047 additions and 84 deletions

4
.env.example Normal file
View File

@ -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"

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

317
package-lock.json generated
View File

@ -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
}
}
}
}
}

View File

@ -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",

6
src/assets/backbone.svg Normal file
View File

@ -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

1
src/assets/device.svg Normal file
View File

@ -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

1
src/assets/fishbone.svg Normal file
View File

@ -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

1
src/assets/tower.svg Normal file
View File

@ -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

1
src/assets/users.svg Normal file
View File

@ -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

View File

@ -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>
);
}

View File

@ -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);

View File

@ -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 }} />;
}

7
src/hooks/useTitle.ts Normal file
View File

@ -0,0 +1,7 @@
import React from "react";
export default function useTitle(title: string) {
React.useEffect(() => {
document.title = `NAM | ${title}`
}, [title])
}

View File

@ -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={{

View File

@ -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

View File

@ -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} />

View File

@ -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` },

View File

@ -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>

View File

@ -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>
);
}

121
src/pages/Backbone/Form.tsx Normal file
View File

@ -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>
);
}

View File

@ -1,4 +1,8 @@
import useTitle from "../hooks/useTitle";
export default function Dashboard() {
useTitle("Dashboard");
return (
<div>
Dashboard

133
src/pages/Devices.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>
);
}

139
src/pages/Devices/Form.tsx Normal file
View File

@ -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>
);
}

115
src/pages/Fishbone.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>
);
}

143
src/pages/Fishbone/Form.tsx Normal file
View File

@ -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>
);
}

97
src/pages/Towers.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>
);
}

89
src/pages/Towers/Form.tsx Normal file
View File

@ -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>
);
}

97
src/pages/Users.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>
);
}

115
src/pages/Users/Form.tsx Normal file
View File

@ -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>
);
}

View File

@ -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>

View File

@ -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');
},};

View File

@ -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",
}
]);
},
};

View File

@ -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",
}
]);
}
};

View File

@ -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",
}
]);
}
};

View File

@ -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",
}
]);
}
};

View File

@ -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",
},
]);
}
};

View File

@ -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;

View File

@ -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>
)
}

24
src/services/api.ts Normal file
View File

@ -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;
};

69
src/stores/authStore.ts Normal file
View File

@ -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),
}
)
);

129
src/stores/backboneStore.ts Normal file
View File

@ -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) }
)
);

128
src/stores/deviceStore.ts Normal file
View File

@ -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) }
)
);

146
src/stores/fishboneStore.ts Normal file
View File

@ -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) }
)
);

26
src/stores/storage.ts Normal file
View File

@ -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);
},
};

128
src/stores/towerStore.ts Normal file
View File

@ -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) }
)
);

125
src/stores/userStore.ts Normal file
View File

@ -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) }
)
);

View File

@ -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: {

View File

@ -1,5 +1,5 @@
// MATERIAL - UI
import { Theme } from '@mui/material/styles';
import { Theme } from '@mui/material';
// PROJECT IMPORTS
import getColors from '../../utils/getColors';

View File

@ -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')
}
}
}

View File

@ -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
},

View File

@ -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}`
}

View File

@ -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
},

View File

@ -96,7 +96,7 @@ export default function ComponentsOverrides(theme: Theme) {
TableFooter(theme),
TableHead(theme),
TablePagination(),
TableRow(),
TableRow(theme),
Tabs(),
ToggleButton(theme),
Tooltip(theme),

View File

@ -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),

View File

@ -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]
}
}
});

View File

@ -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)}`,

View File

@ -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,

38
src/types/auth.types.ts Normal file
View File

@ -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;
}

View File

@ -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;
}

55
src/types/device.types.ts Normal file
View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
}
}

View File

@ -0,0 +1,10 @@
interface ResponseApiStatus {
status: string | number;
description: string;
}
export interface ResponseApi<T = null> {
status: ResponseApiStatus;
message: string;
data?: T | [] | null;
}

13
src/types/store.types.ts Normal file
View File

@ -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;
}

50
src/types/tower.types.ts Normal file
View File

@ -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;
}

49
src/types/users.types.ts Normal file
View File

@ -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;
}

View File

@ -20,7 +20,10 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noUncheckedSideEffectImports": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"]
}

15
vite-env.d.ts vendored Normal file
View File

@ -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
}