Compare commits
13 Commits
4effb25e9d
...
d8edfaf05f
| Author | SHA1 | Date | |
|---|---|---|---|
| d8edfaf05f | |||
| 92d251c122 | |||
| b4a4dfcb75 | |||
| b630bae50d | |||
| 4371fcc028 | |||
| 9deedd86e5 | |||
| 5a48c91801 | |||
| 51d65cc0b1 | |||
| 6c94005971 | |||
| 8afdc71bbf | |||
| 358e5524c7 | |||
| ef61dad210 | |||
| 7d03d0c42a |
341
frontend/package-lock.json
generated
341
frontend/package-lock.json
generated
@@ -45,6 +45,7 @@
|
|||||||
"react-grid-layout": "^1.5.1",
|
"react-grid-layout": "^1.5.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"recharts": "^2.15.2",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.15",
|
"tailwindcss": "^4.0.15",
|
||||||
@@ -327,6 +328,18 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.27.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||||
|
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.26.9",
|
"version": "7.26.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||||
@@ -3376,6 +3389,69 @@
|
|||||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||||
@@ -4105,9 +4181,129 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
|
||||||
@@ -4154,6 +4350,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/decode-formdata": {
|
"node_modules/decode-formdata": {
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/decode-formdata/-/decode-formdata-0.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-formdata/-/decode-formdata-0.8.0.tgz",
|
||||||
@@ -4209,6 +4411,16 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@@ -4545,6 +4757,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fast-deep-equal": {
|
"node_modules/fast-deep-equal": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
@@ -4958,6 +5176,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-arguments": {
|
"node_modules/is-arguments": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
@@ -5484,6 +5711,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -6078,6 +6311,30 @@
|
|||||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-smooth": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-equals": "^5.0.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-transition-group": "^4.4.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-smooth/node_modules/fast-equals": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-style-singleton": {
|
"node_modules/react-style-singleton": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
@@ -6100,6 +6357,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -6113,6 +6386,50 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "2.15.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz",
|
||||||
|
"integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"eventemitter3": "^4.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-is": "^18.3.1",
|
||||||
|
"react-smooth": "^4.0.4",
|
||||||
|
"recharts-scale": "^0.4.4",
|
||||||
|
"tiny-invariant": "^1.3.1",
|
||||||
|
"victory-vendor": "^36.6.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts-scale": {
|
||||||
|
"version": "0.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
|
||||||
|
"integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"decimal.js-light": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||||
|
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resize-observer-polyfill": {
|
"node_modules/resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
@@ -6712,6 +7029,28 @@
|
|||||||
"which-typed-array": "^1.1.2"
|
"which-typed-array": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "36.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
"integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.2.tgz",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"react-grid-layout": "^1.5.1",
|
"react-grid-layout": "^1.5.1",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"recharts": "^2.15.2",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.15",
|
"tailwindcss": "^4.0.15",
|
||||||
|
|||||||
146
frontend/src/components/admin/modules/ModuleForm.tsx
Normal file
146
frontend/src/components/admin/modules/ModuleForm.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useState } from "react";
|
||||||
|
//import { z } from "zod";
|
||||||
|
//import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useSessionStore } from "@/lib/store/sessionStore";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
import { getModules } from "@/utils/querys/admin/modules";
|
||||||
|
|
||||||
|
// const FormSchema = z.object({
|
||||||
|
// subModule: z.boolean(),
|
||||||
|
// });
|
||||||
|
export function ChangeModule({ module }: { module: any }) {
|
||||||
|
const { token } = useSessionStore();
|
||||||
|
const { refetch } = useQuery(getModules(token ?? ""));
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
active: module.active,
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
console.log(value);
|
||||||
|
try {
|
||||||
|
const result = await axios.patch(
|
||||||
|
`/api/server/modules/${module.module_id}`,
|
||||||
|
{ active: value.active },
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.data.success) {
|
||||||
|
setOpen(!open);
|
||||||
|
setSaving(false);
|
||||||
|
refetch();
|
||||||
|
toast.success(result.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!open) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
setOpen(isOpen);
|
||||||
|
// toast.message("Model was something", {
|
||||||
|
// description: isOpen ? "Modal is open" : "Modal is closed",
|
||||||
|
// });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Edit</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{module.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Set to active or deactivated.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<form.Field
|
||||||
|
name="active"
|
||||||
|
// validators={{
|
||||||
|
// // We can choose between form-wide and field-specific validators
|
||||||
|
// onChange: ({ value }) =>
|
||||||
|
// value.length > 3
|
||||||
|
// ? undefined
|
||||||
|
// : "Username must be longer than 3 letters",
|
||||||
|
// }}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2 flex flex-row">
|
||||||
|
<Label htmlFor="active">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
className="ml-2"
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
checked={field.state.value}
|
||||||
|
onCheckedChange={(e) =>
|
||||||
|
field.handleChange(e)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={form.handleSubmit}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<span>Saving....</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Save setting</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
frontend/src/components/admin/modules/ModulePage.tsx
Normal file
116
frontend/src/components/admin/modules/ModulePage.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { LstCard } from "@/components/extendedUI/LstCard";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { useSessionStore } from "@/lib/store/sessionStore";
|
||||||
|
import { useModuleStore } from "@/lib/store/useModuleStore";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { getModules } from "@/utils/querys/admin/modules";
|
||||||
|
import { ChangeModule } from "./ModuleForm";
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
settings_id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModulesPage() {
|
||||||
|
const { user, token } = useSessionStore();
|
||||||
|
const { modules } = useModuleStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const adminModule = modules.filter((n) => n.name === "admin");
|
||||||
|
const userLevel =
|
||||||
|
user?.roles.filter((r) => r.module_id === adminModule[0].module_id) ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (!adminModule[0].roles.includes(userLevel[0]?.role)) {
|
||||||
|
router.navigate({ to: "/" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, isError, error, isLoading } = useQuery(
|
||||||
|
getModules(token ?? "")
|
||||||
|
);
|
||||||
|
|
||||||
|
// if (isLoading) {
|
||||||
|
// return <div>Loading.....</div>;
|
||||||
|
// }
|
||||||
|
if (isError) {
|
||||||
|
return <div>{JSON.stringify(error)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LstCard className="m-2 flex place-content-center w-fit">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead>Module In</TableHead>
|
||||||
|
<TableHead>Roles</TableHead>
|
||||||
|
<TableHead>Active</TableHead>
|
||||||
|
<TableHead>Edit</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<TableBody>
|
||||||
|
{Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((i: any) => (
|
||||||
|
<TableRow key={i.submodule_id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.description}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.moduleName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{JSON.stringify(i.roles)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.active ? "Yes" : "No"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<ChangeModule module={i} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</LstCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
145
frontend/src/components/admin/supModules/SubModuleForm.tsx
Normal file
145
frontend/src/components/admin/supModules/SubModuleForm.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useState } from "react";
|
||||||
|
//import { z } from "zod";
|
||||||
|
//import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useSessionStore } from "@/lib/store/sessionStore";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { getSubModules } from "@/utils/querys/admin/subModules";
|
||||||
|
|
||||||
|
// const FormSchema = z.object({
|
||||||
|
// subModule: z.boolean(),
|
||||||
|
// });
|
||||||
|
export function ChangeSubModule({ subModule }: { subModule: any }) {
|
||||||
|
const { token } = useSessionStore();
|
||||||
|
const { refetch } = useQuery(getSubModules(token ?? ""));
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
active: subModule.active,
|
||||||
|
},
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
console.log(value);
|
||||||
|
try {
|
||||||
|
const result = await axios.patch(
|
||||||
|
`/api/server/submodules/${subModule.submodule_id}`,
|
||||||
|
{ active: value.active },
|
||||||
|
{
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.data.success) {
|
||||||
|
setOpen(!open);
|
||||||
|
setSaving(false);
|
||||||
|
refetch();
|
||||||
|
toast.success(result.data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (!open) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
setOpen(isOpen);
|
||||||
|
// toast.message("Model was something", {
|
||||||
|
// description: isOpen ? "Modal is open" : "Modal is closed",
|
||||||
|
// });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">Edit</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{subModule.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Set to active or deactivated.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<>
|
||||||
|
<form.Field
|
||||||
|
name="active"
|
||||||
|
// validators={{
|
||||||
|
// // We can choose between form-wide and field-specific validators
|
||||||
|
// onChange: ({ value }) =>
|
||||||
|
// value.length > 3
|
||||||
|
// ? undefined
|
||||||
|
// : "Username must be longer than 3 letters",
|
||||||
|
// }}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2 flex flex-row">
|
||||||
|
<Label htmlFor="active">
|
||||||
|
Active
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
className="ml-2"
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
checked={field.state.value}
|
||||||
|
onCheckedChange={(e) =>
|
||||||
|
field.handleChange(e)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex justify-end mt-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
onClick={form.handleSubmit}
|
||||||
|
>
|
||||||
|
{saving ? (
|
||||||
|
<>
|
||||||
|
<span>Saving....</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>Save setting</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
125
frontend/src/components/admin/supModules/SubModulePage.tsx
Normal file
125
frontend/src/components/admin/supModules/SubModulePage.tsx
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { LstCard } from "@/components/extendedUI/LstCard";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { useSessionStore } from "@/lib/store/sessionStore";
|
||||||
|
import { useModuleStore } from "@/lib/store/useModuleStore";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useRouter } from "@tanstack/react-router";
|
||||||
|
import { ChangeSubModule } from "./SubModuleForm";
|
||||||
|
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { getSubModules } from "@/utils/querys/admin/subModules";
|
||||||
|
|
||||||
|
export type Settings = {
|
||||||
|
settings_id?: string;
|
||||||
|
name?: string;
|
||||||
|
value?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function SubModulePage() {
|
||||||
|
const { user, token } = useSessionStore();
|
||||||
|
const { modules } = useModuleStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const adminModule = modules.filter((n) => n.name === "admin");
|
||||||
|
const userLevel =
|
||||||
|
user?.roles.filter((r) => r.module_id === adminModule[0].module_id) ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (!adminModule[0].roles.includes(userLevel[0]?.role)) {
|
||||||
|
router.navigate({ to: "/" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, isError, error, isLoading } = useQuery(
|
||||||
|
getSubModules(token ?? "")
|
||||||
|
);
|
||||||
|
|
||||||
|
// if (isLoading) {
|
||||||
|
// return <div>Loading.....</div>;
|
||||||
|
// }
|
||||||
|
if (isError) {
|
||||||
|
return <div>{JSON.stringify(error)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LstCard className="m-2 flex place-content-center w-fit">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Description</TableHead>
|
||||||
|
<TableHead>Module In</TableHead>
|
||||||
|
<TableHead>Roles</TableHead>
|
||||||
|
<TableHead>Active</TableHead>
|
||||||
|
<TableHead>Edit</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<TableBody>
|
||||||
|
{Array(10)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, i) => (
|
||||||
|
<TableRow key={i}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Skeleton className="h-4" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TableBody>
|
||||||
|
{data?.map((i: any) => (
|
||||||
|
<TableRow key={i.submodule_id}>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.description}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.moduleName}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{JSON.stringify(i.roles)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{i.active ? "Yes" : "No"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<ChangeSubModule subModule={i} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
</LstCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import ModuleForm from "./ModuleForm";
|
||||||
|
import { getModules } from "@/utils/querys/admin/modules";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
//import { getUserRoles } from "@/utils/querys/admin/userRoles";
|
||||||
|
//import { Checkbox } from "@radix-ui/react-checkbox";
|
||||||
|
|
||||||
|
export default function ModuleAccess(data: any) {
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
// const { data: userRoles } = useQuery(getUserRoles());
|
||||||
|
const {
|
||||||
|
data: modules,
|
||||||
|
isError,
|
||||||
|
isLoading,
|
||||||
|
} = useQuery(getModules(token ?? ""));
|
||||||
|
|
||||||
|
if (isError) return <div>Error gettings Roles</div>;
|
||||||
|
if (isLoading) return <div>Loading modules</div>;
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row flex-wrap">
|
||||||
|
{modules?.map((m: any) => {
|
||||||
|
return (
|
||||||
|
<ModuleForm
|
||||||
|
key={m.module_id}
|
||||||
|
i={m}
|
||||||
|
username={data.user.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
frontend/src/components/admin/user/components/ModuleForm.tsx
Normal file
119
frontend/src/components/admin/user/components/ModuleForm.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { LstCard } from "@/components/extendedUI/LstCard";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { getUserRoles } from "@/utils/querys/admin/userRoles";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export default function ModuleForm(props: any) {
|
||||||
|
const { refetch } = useQuery(getUserRoles());
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: { role: props.i.role },
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
const data = {
|
||||||
|
username: props.username,
|
||||||
|
module: props.i.name,
|
||||||
|
role: value.role,
|
||||||
|
};
|
||||||
|
console.log(data);
|
||||||
|
try {
|
||||||
|
const res = await axios.post("/api/auth/setuseraccess", data, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
toast.success(res.data.message);
|
||||||
|
refetch();
|
||||||
|
form.reset();
|
||||||
|
} else {
|
||||||
|
res.data.message;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-2 p-1">
|
||||||
|
<LstCard>
|
||||||
|
<p className="text-center">Module: {props.i.name}</p>
|
||||||
|
|
||||||
|
<p className="p-1">Current role: </p>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.Field
|
||||||
|
name="role"
|
||||||
|
//listeners={{onChange: ({value})=>{}}}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2">
|
||||||
|
<Label htmlFor={field.name}>
|
||||||
|
Select role
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={field.state.value}
|
||||||
|
onValueChange={field.handleChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue
|
||||||
|
id={field.name}
|
||||||
|
placeholder="Select Role"
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Roles</SelectLabel>
|
||||||
|
<SelectItem value="viewer">
|
||||||
|
Viewer
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="technician">
|
||||||
|
Technician
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="supervisor">
|
||||||
|
Supervisor
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="manager">
|
||||||
|
Manager
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tester">
|
||||||
|
Tester
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="admin">
|
||||||
|
Admin
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button type="submit" onClick={form.handleSubmit}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</LstCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useGetUserRoles } from "@/lib/store/useGetRoles";
|
||||||
|
import { useSubModuleStore } from "@/lib/store/useSubModuleStore";
|
||||||
|
|
||||||
|
import SubModuleForm from "./SubmoduleForm";
|
||||||
|
//import { Checkbox } from "@radix-ui/react-checkbox";
|
||||||
|
|
||||||
|
export default function UserSubRoles(data: any) {
|
||||||
|
const { userRoles } = useGetUserRoles();
|
||||||
|
const { subModules } = useSubModuleStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-row flex-wrap">
|
||||||
|
{subModules?.map((m: any) => {
|
||||||
|
const hasRole: any = userRoles.filter(
|
||||||
|
(r: any) =>
|
||||||
|
r.user_id.includes(data.user.user_id) &&
|
||||||
|
r.module_id === m.module_id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div key={m.module_id}>
|
||||||
|
<SubModuleForm
|
||||||
|
i={m}
|
||||||
|
hasRole={hasRole}
|
||||||
|
user={data.user}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { LstCard } from "@/components/extendedUI/LstCard";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
|
||||||
|
export default function SubModuleForm(props: any) {
|
||||||
|
const form = useForm({
|
||||||
|
defaultValues: { role: "" },
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
console.log(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="m-2 p-1">
|
||||||
|
<LstCard>
|
||||||
|
<p className="text-center">
|
||||||
|
Module: {props.i.moduleName}, <br />
|
||||||
|
SubModule: {props.i.name}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="p-1">
|
||||||
|
Current role:{" "}
|
||||||
|
{props.hasRole[0]?.role
|
||||||
|
? props.hasRole[0].role
|
||||||
|
: "not assigned"}
|
||||||
|
</p>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form.Field
|
||||||
|
name="role"
|
||||||
|
//listeners={{onChange: ({value})=>{}}}
|
||||||
|
children={(field) => {
|
||||||
|
return (
|
||||||
|
<div className="m-2 min-w-48 max-w-96 p-2">
|
||||||
|
<Label htmlFor={field.name}>
|
||||||
|
Select role
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={field.state.value}
|
||||||
|
onValueChange={field.handleChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
<SelectValue
|
||||||
|
id={field.name}
|
||||||
|
placeholder="Select Role"
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Roles</SelectLabel>
|
||||||
|
<SelectItem value="viewer">
|
||||||
|
Viewer
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="technician">
|
||||||
|
Technician
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="supervisor">
|
||||||
|
Supervisor
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="manager">
|
||||||
|
Manager
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="tester">
|
||||||
|
Tester
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="admin">
|
||||||
|
Admin
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</LstCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import { useForm } from "@tanstack/react-form";
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import UserRoles from "./UserRoles";
|
|
||||||
import { CardHeader } from "@/components/ui/card";
|
import { CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
export default function UserCard(data: any) {
|
export default function UserCard(data: any) {
|
||||||
@@ -270,16 +270,11 @@ export default function UserCard(data: any) {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</LstCard>
|
</LstCard>
|
||||||
<div>
|
<div className="mt-4">
|
||||||
<Button onClick={form.handleSubmit}>Save</Button>
|
<Button onClick={form.handleSubmit}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2">
|
<div className="flex flex-col"></div>
|
||||||
<LstCard>
|
|
||||||
<CardHeader>User Module / Role Access</CardHeader>
|
|
||||||
<UserRoles user={data.user} />
|
|
||||||
</LstCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useModuleStore } from "@/lib/store/useModuleStore";
|
|
||||||
//import { Checkbox } from "@radix-ui/react-checkbox";
|
|
||||||
|
|
||||||
export default function UserRoles(user: any) {
|
|
||||||
const { modules } = useModuleStore();
|
|
||||||
console.log(user);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{modules?.map((m: any) => {
|
|
||||||
console.log(m);
|
|
||||||
return <Label>{m.name}</Label>;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AlignJustify,
|
||||||
Atom,
|
Atom,
|
||||||
Logs,
|
Logs,
|
||||||
Minus,
|
Minus,
|
||||||
@@ -25,76 +26,120 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "../../ui/collapsible";
|
} from "../../ui/collapsible";
|
||||||
|
import { useSubModuleStore } from "@/lib/store/useSubModuleStore";
|
||||||
|
|
||||||
const items = [
|
const iconMap: any = {
|
||||||
{
|
ShieldCheck: ShieldCheck,
|
||||||
title: "Servers",
|
AlignJustify: AlignJustify,
|
||||||
url: "/servers",
|
Settings: Settings,
|
||||||
icon: Server,
|
Atom: Atom,
|
||||||
isActive: false,
|
Logs: Logs,
|
||||||
},
|
Users: Users,
|
||||||
];
|
Webhook: Webhook,
|
||||||
const data = {
|
Server: Server,
|
||||||
navMain: [
|
|
||||||
{
|
|
||||||
title: "Admin",
|
|
||||||
url: "#",
|
|
||||||
icon: ShieldCheck,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Settings",
|
|
||||||
url: "/settings",
|
|
||||||
icon: Settings,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Modules",
|
|
||||||
url: "/modules",
|
|
||||||
icon: Settings,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Swagger",
|
|
||||||
url: "#",
|
|
||||||
icon: Webhook,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Logs",
|
|
||||||
url: "#",
|
|
||||||
icon: Logs,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Users",
|
|
||||||
url: "/users",
|
|
||||||
icon: Users,
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "UCD",
|
|
||||||
url: "https://ucd.alpla.net:8443/",
|
|
||||||
icon: Atom,
|
|
||||||
isActive: false,
|
|
||||||
newWindow: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Lst Api",
|
|
||||||
url: "/api/docs",
|
|
||||||
icon: Webhook,
|
|
||||||
isActive: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AdminSideBar() {
|
export function AdminSideBar() {
|
||||||
|
const { subModules } = useSubModuleStore();
|
||||||
|
|
||||||
|
const items = subModules.filter((m) => m.moduleName === "admin");
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Admin section</SidebarGroupLabel>
|
<SidebarGroupLabel>Admin section</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
|
{items.map((item: any, index) => {
|
||||||
|
const Icon = iconMap[item.icon] || AlignJustify;
|
||||||
|
// drop down menu setup
|
||||||
|
return (
|
||||||
|
<SidebarMenu key={item.name}>
|
||||||
|
{item.link === "#" ? (
|
||||||
|
<Collapsible
|
||||||
|
key={item.name}
|
||||||
|
defaultOpen={index === 1}
|
||||||
|
className="group/collapsible"
|
||||||
|
>
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<SidebarMenuButton>
|
||||||
|
<Icon />
|
||||||
|
{item.name}{" "}
|
||||||
|
<Plus className="ml-auto group-data-[state=open]/collapsible:hidden" />
|
||||||
|
<Minus className="ml-auto group-data-[state=closed]/collapsible:hidden" />
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
{item.subSubModule?.length > 0 ? (
|
||||||
|
<CollapsibleContent>
|
||||||
|
<SidebarMenuSub>
|
||||||
|
{item.subSubModule.map(
|
||||||
|
(i: any) => {
|
||||||
|
const SubIcon =
|
||||||
|
iconMap[
|
||||||
|
i.icon
|
||||||
|
] ||
|
||||||
|
AlignJustify;
|
||||||
|
return (
|
||||||
|
<SidebarMenuSubItem
|
||||||
|
key={i.name}
|
||||||
|
>
|
||||||
|
{i.isActive && (
|
||||||
|
<SidebarMenuSubButton
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={
|
||||||
|
i.link
|
||||||
|
}
|
||||||
|
target={
|
||||||
|
i.newWindow
|
||||||
|
? "_blank"
|
||||||
|
: "_self"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SubIcon />
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
i.name
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuSubButton>
|
||||||
|
)}
|
||||||
|
</SidebarMenuSubItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</SidebarMenuSub>
|
||||||
|
</CollapsibleContent>
|
||||||
|
) : null}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
</Collapsible>
|
||||||
|
) : (
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
{items.map((item) => {
|
||||||
|
if (item.link === "#") return;
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={item.name}>
|
||||||
|
<SidebarMenuButton asChild>
|
||||||
|
<a href={item.link}>
|
||||||
|
<Icon />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</a>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarMenu>
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/* <SidebarMenu>
|
||||||
{data.navMain.map((item, index) => (
|
{data.navMain.map((item, index) => (
|
||||||
<Collapsible
|
<Collapsible
|
||||||
key={item.title}
|
key={item.title}
|
||||||
@@ -156,8 +201,5 @@ export function AdminSideBar() {
|
|||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
</SidebarMenu> */
|
||||||
</SidebarGroupContent>
|
|
||||||
</SidebarGroup>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//import { Cylinder, Package, Truck } from "lucide-react";
|
import { Cylinder, Package, Truck } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
SidebarGroup,
|
SidebarGroup,
|
||||||
SidebarGroupContent,
|
SidebarGroupContent,
|
||||||
@@ -10,67 +10,12 @@ import {
|
|||||||
import { hasPageAccess } from "@/utils/userAccess";
|
import { hasPageAccess } from "@/utils/userAccess";
|
||||||
import { User } from "@/types/users";
|
import { User } from "@/types/users";
|
||||||
import { useSubModuleStore } from "@/lib/store/useSubModuleStore";
|
import { useSubModuleStore } from "@/lib/store/useSubModuleStore";
|
||||||
// this will need to be moved to a links section the db to make it more easy to remove and add
|
|
||||||
// const items = [
|
const iconMap: any = {
|
||||||
// {
|
Package: Package,
|
||||||
// title: "Silo Adjustments",
|
Truck: Truck,
|
||||||
// url: "#",
|
Cylinder: Cylinder,
|
||||||
// icon: Cylinder,
|
};
|
||||||
// role: ["admin", "systemAdmin"],
|
|
||||||
// module: "logistics",
|
|
||||||
// active: true,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Bulk orders",
|
|
||||||
// moduleName: "logistics",
|
|
||||||
// description: "",
|
|
||||||
// link: "#",
|
|
||||||
// icon: Truck,
|
|
||||||
// role: ["systemAdmin"],
|
|
||||||
// active: true,
|
|
||||||
// subSubModule: [],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Forecast",
|
|
||||||
// moduleName: "logistics",
|
|
||||||
// description: "",
|
|
||||||
// link: "#",
|
|
||||||
// icon: Truck,
|
|
||||||
// role: ["systemAdmin"],
|
|
||||||
// active: true,
|
|
||||||
// subSubModule: [],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Ocme cycle counts",
|
|
||||||
// moduleName: "logistics",
|
|
||||||
// description: "",
|
|
||||||
// link: "#",
|
|
||||||
// icon: Package,
|
|
||||||
// role: ["technician", "supervisor", "manager", "admin", "systemAdmin"],
|
|
||||||
// active: false,
|
|
||||||
// subSubModule: [],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Material Helper",
|
|
||||||
// moduleName: "logistics",
|
|
||||||
// description: "",
|
|
||||||
// link: "/materialHelper/consumption",
|
|
||||||
// icon: Package,
|
|
||||||
// role: ["technician", "supervisor", "manager", "admin", "systemAdmin"],
|
|
||||||
// active: true,
|
|
||||||
// subSubModule: [],
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: "Ocme Cyclecount",
|
|
||||||
// moduleName: "logistics",
|
|
||||||
// description: "",
|
|
||||||
// link: "/cyclecount",
|
|
||||||
// icon: Package,
|
|
||||||
// role: ["technician", "supervisor", "manager", "admin", "systemAdmin"],
|
|
||||||
// active: true,
|
|
||||||
// subSubModule: [],
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
|
|
||||||
export function LogisticsSideBar({
|
export function LogisticsSideBar({
|
||||||
user,
|
user,
|
||||||
@@ -82,13 +27,14 @@ export function LogisticsSideBar({
|
|||||||
const { subModules } = useSubModuleStore();
|
const { subModules } = useSubModuleStore();
|
||||||
|
|
||||||
const items = subModules.filter((m) => m.moduleName === "logistics");
|
const items = subModules.filter((m) => m.moduleName === "logistics");
|
||||||
//console.log(items);
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Logistics</SidebarGroupLabel>
|
<SidebarGroupLabel>Logistics</SidebarGroupLabel>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{items.map((item) => {
|
{items.map((item) => {
|
||||||
|
const Icon = iconMap[item.icon];
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={item.submodule_id}>
|
<SidebarMenuItem key={item.submodule_id}>
|
||||||
<>
|
<>
|
||||||
@@ -100,6 +46,7 @@ export function LogisticsSideBar({
|
|||||||
item.active && (
|
item.active && (
|
||||||
<SidebarMenuButton asChild>
|
<SidebarMenuButton asChild>
|
||||||
<a href={item.link}>
|
<a href={item.link}>
|
||||||
|
<Icon />
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
|
|||||||
@@ -81,6 +81,30 @@ export default function SiloCard(data: any) {
|
|||||||
<hr className="m-2" />
|
<hr className="m-2" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
{silo.Stock_Total === 0 ? (
|
||||||
|
<div className="flex justify-center flex-col">
|
||||||
|
<span>
|
||||||
|
The silo is currently empty you will not be
|
||||||
|
able to do an adjustment until you have
|
||||||
|
received material in.
|
||||||
|
</span>
|
||||||
|
<hr />
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
-Someone click "Take inventory on a
|
||||||
|
empty location" in stock.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
-Silo virtualy ran empty due to
|
||||||
|
production over consumption.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
-Someone forgot to move a railcar
|
||||||
|
compartment over to this location.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -110,8 +134,12 @@ export default function SiloCard(data: any) {
|
|||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<Input
|
<Input
|
||||||
name={field.name}
|
name={field.name}
|
||||||
value={field.state.value}
|
value={
|
||||||
onBlur={field.handleBlur}
|
field.state.value
|
||||||
|
}
|
||||||
|
onBlur={
|
||||||
|
field.handleBlur
|
||||||
|
}
|
||||||
type="decimal"
|
type="decimal"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
field.handleChange(
|
field.handleChange(
|
||||||
@@ -122,7 +150,9 @@ export default function SiloCard(data: any) {
|
|||||||
<Button
|
<Button
|
||||||
className="ml-1"
|
className="ml-1"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={form.handleSubmit}
|
onClick={
|
||||||
|
form.handleSubmit
|
||||||
|
}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
@@ -137,7 +167,8 @@ export default function SiloCard(data: any) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{field.state.meta.errors.length ? (
|
{field.state.meta.errors
|
||||||
|
.length ? (
|
||||||
<em>
|
<em>
|
||||||
{field.state.meta.errors.join(
|
{field.state.meta.errors.join(
|
||||||
","
|
","
|
||||||
@@ -149,6 +180,7 @@ export default function SiloCard(data: any) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</LstCard>
|
</LstCard>
|
||||||
<div className="grow max-w-[400px]">
|
<div className="grow max-w-[400px]">
|
||||||
|
|||||||
351
frontend/src/components/ui/chart.tsx
Normal file
351
frontend/src/components/ui/chart.tsx
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as RechartsPrimitive from "recharts"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
|
|
||||||
|
export type ChartConfig = {
|
||||||
|
[k in string]: {
|
||||||
|
label?: React.ReactNode
|
||||||
|
icon?: React.ComponentType
|
||||||
|
} & (
|
||||||
|
| { color?: string; theme?: never }
|
||||||
|
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChartContextProps = {
|
||||||
|
config: ChartConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||||
|
|
||||||
|
function useChart() {
|
||||||
|
const context = React.useContext(ChartContext)
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useChart must be used within a <ChartContainer />")
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartContainer({
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
config,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
config: ChartConfig
|
||||||
|
children: React.ComponentProps<
|
||||||
|
typeof RechartsPrimitive.ResponsiveContainer
|
||||||
|
>["children"]
|
||||||
|
}) {
|
||||||
|
const uniqueId = React.useId()
|
||||||
|
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChartContext.Provider value={{ config }}>
|
||||||
|
<div
|
||||||
|
data-slot="chart"
|
||||||
|
data-chart={chartId}
|
||||||
|
className={cn(
|
||||||
|
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChartStyle id={chartId} config={config} />
|
||||||
|
<RechartsPrimitive.ResponsiveContainer>
|
||||||
|
{children}
|
||||||
|
</RechartsPrimitive.ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</ChartContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||||
|
const colorConfig = Object.entries(config).filter(
|
||||||
|
([, config]) => config.theme || config.color
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!colorConfig.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<style
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color =
|
||||||
|
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||||
|
itemConfig.color
|
||||||
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
|
})
|
||||||
|
.join("\n")}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("\n"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||||
|
|
||||||
|
function ChartTooltipContent({
|
||||||
|
active,
|
||||||
|
payload,
|
||||||
|
className,
|
||||||
|
indicator = "dot",
|
||||||
|
hideLabel = false,
|
||||||
|
hideIndicator = false,
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
labelClassName,
|
||||||
|
formatter,
|
||||||
|
color,
|
||||||
|
nameKey,
|
||||||
|
labelKey,
|
||||||
|
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||||
|
React.ComponentProps<"div"> & {
|
||||||
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: "line" | "dot" | "dashed"
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
}) {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
const tooltipLabel = React.useMemo(() => {
|
||||||
|
if (hideLabel || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const [item] = payload
|
||||||
|
const key = `${labelKey || item?.dataKey || item?.name || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const value =
|
||||||
|
!labelKey && typeof label === "string"
|
||||||
|
? config[label as keyof typeof config]?.label || label
|
||||||
|
: itemConfig?.label
|
||||||
|
|
||||||
|
if (labelFormatter) {
|
||||||
|
return (
|
||||||
|
<div className={cn("font-medium", labelClassName)}>
|
||||||
|
{labelFormatter(value, payload)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||||
|
}, [
|
||||||
|
label,
|
||||||
|
labelFormatter,
|
||||||
|
payload,
|
||||||
|
hideLabel,
|
||||||
|
labelClassName,
|
||||||
|
config,
|
||||||
|
labelKey,
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!active || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!nestLabel ? tooltipLabel : null}
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{payload.map((item, index) => {
|
||||||
|
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
const indicatorColor = color || item.payload.fill || item.color
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.dataKey}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||||
|
indicator === "dot" && "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatter && item?.value !== undefined && item.name ? (
|
||||||
|
formatter(item.value, item.name, item, index, item.payload)
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{itemConfig?.icon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
!hideIndicator && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||||
|
{
|
||||||
|
"h-2.5 w-2.5": indicator === "dot",
|
||||||
|
"w-1": indicator === "line",
|
||||||
|
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||||
|
indicator === "dashed",
|
||||||
|
"my-0.5": nestLabel && indicator === "dashed",
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--color-bg": indicatorColor,
|
||||||
|
"--color-border": indicatorColor,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 justify-between leading-none",
|
||||||
|
nestLabel ? "items-end" : "items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid gap-1.5">
|
||||||
|
{nestLabel ? tooltipLabel : null}
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{itemConfig?.label || item.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{item.value && (
|
||||||
|
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||||
|
{item.value.toLocaleString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChartLegend = RechartsPrimitive.Legend
|
||||||
|
|
||||||
|
function ChartLegendContent({
|
||||||
|
className,
|
||||||
|
hideIcon = false,
|
||||||
|
payload,
|
||||||
|
verticalAlign = "bottom",
|
||||||
|
nameKey,
|
||||||
|
}: React.ComponentProps<"div"> &
|
||||||
|
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||||
|
hideIcon?: boolean
|
||||||
|
nameKey?: string
|
||||||
|
}) {
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
if (!payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center gap-4",
|
||||||
|
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{payload.map((item) => {
|
||||||
|
const key = `${nameKey || item.dataKey || "value"}`
|
||||||
|
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.value}
|
||||||
|
className={cn(
|
||||||
|
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{itemConfig?.icon && !hideIcon ? (
|
||||||
|
<itemConfig.icon />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
style={{
|
||||||
|
backgroundColor: item.color,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{itemConfig?.label}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to extract item config from a payload.
|
||||||
|
function getPayloadConfigFromPayload(
|
||||||
|
config: ChartConfig,
|
||||||
|
payload: unknown,
|
||||||
|
key: string
|
||||||
|
) {
|
||||||
|
if (typeof payload !== "object" || payload === null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadPayload =
|
||||||
|
"payload" in payload &&
|
||||||
|
typeof payload.payload === "object" &&
|
||||||
|
payload.payload !== null
|
||||||
|
? payload.payload
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
let configLabelKey: string = key
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in payload &&
|
||||||
|
typeof payload[key as keyof typeof payload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payload[key as keyof typeof payload] as string
|
||||||
|
} else if (
|
||||||
|
payloadPayload &&
|
||||||
|
key in payloadPayload &&
|
||||||
|
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||||
|
) {
|
||||||
|
configLabelKey = payloadPayload[
|
||||||
|
key as keyof typeof payloadPayload
|
||||||
|
] as string
|
||||||
|
}
|
||||||
|
|
||||||
|
return configLabelKey in config
|
||||||
|
? config[configLabelKey]
|
||||||
|
: config[key as keyof typeof config]
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChartContainer,
|
||||||
|
ChartTooltip,
|
||||||
|
ChartTooltipContent,
|
||||||
|
ChartLegend,
|
||||||
|
ChartLegendContent,
|
||||||
|
ChartStyle,
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { Route as OcpIndexImport } from './routes/ocp/index'
|
|||||||
import { Route as EomEomImport } from './routes/_eom/eom'
|
import { Route as EomEomImport } from './routes/_eom/eom'
|
||||||
import { Route as AuthProfileImport } from './routes/_auth/profile'
|
import { Route as AuthProfileImport } from './routes/_auth/profile'
|
||||||
import { Route as AdminUsersImport } from './routes/_admin/users'
|
import { Route as AdminUsersImport } from './routes/_admin/users'
|
||||||
|
import { Route as AdminSubModulesImport } from './routes/_admin/subModules'
|
||||||
import { Route as AdminSettingsImport } from './routes/_admin/settings'
|
import { Route as AdminSettingsImport } from './routes/_admin/settings'
|
||||||
import { Route as AdminServersImport } from './routes/_admin/servers'
|
import { Route as AdminServersImport } from './routes/_admin/servers'
|
||||||
import { Route as AdminModulesImport } from './routes/_admin/modules'
|
import { Route as AdminModulesImport } from './routes/_admin/modules'
|
||||||
@@ -28,6 +29,7 @@ import { Route as ocmeCyclecountIndexImport } from './routes/(ocme)/cyclecount/i
|
|||||||
import { Route as logisticsSiloAdjustmentsIndexImport } from './routes/(logistics)/siloAdjustments/index'
|
import { Route as logisticsSiloAdjustmentsIndexImport } from './routes/(logistics)/siloAdjustments/index'
|
||||||
import { Route as logisticsMaterialHelperIndexImport } from './routes/(logistics)/materialHelper/index'
|
import { Route as logisticsMaterialHelperIndexImport } from './routes/(logistics)/materialHelper/index'
|
||||||
import { Route as EomArticleAvImport } from './routes/_eom/article/$av'
|
import { Route as EomArticleAvImport } from './routes/_eom/article/$av'
|
||||||
|
import { Route as logisticsSiloAdjustmentsHistImport } from './routes/(logistics)/siloAdjustments/$hist'
|
||||||
import { Route as logisticsMaterialHelperSiloLinkIndexImport } from './routes/(logistics)/materialHelper/siloLink/index'
|
import { Route as logisticsMaterialHelperSiloLinkIndexImport } from './routes/(logistics)/materialHelper/siloLink/index'
|
||||||
import { Route as logisticsMaterialHelperConsumptionIndexImport } from './routes/(logistics)/materialHelper/consumption/index'
|
import { Route as logisticsMaterialHelperConsumptionIndexImport } from './routes/(logistics)/materialHelper/consumption/index'
|
||||||
import { Route as logisticsSiloAdjustmentsCommentCommentImport } from './routes/(logistics)/siloAdjustments/comment/$comment'
|
import { Route as logisticsSiloAdjustmentsCommentCommentImport } from './routes/(logistics)/siloAdjustments/comment/$comment'
|
||||||
@@ -91,6 +93,12 @@ const AdminUsersRoute = AdminUsersImport.update({
|
|||||||
getParentRoute: () => AdminRoute,
|
getParentRoute: () => AdminRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const AdminSubModulesRoute = AdminSubModulesImport.update({
|
||||||
|
id: '/subModules',
|
||||||
|
path: '/subModules',
|
||||||
|
getParentRoute: () => AdminRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const AdminSettingsRoute = AdminSettingsImport.update({
|
const AdminSettingsRoute = AdminSettingsImport.update({
|
||||||
id: '/settings',
|
id: '/settings',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
@@ -135,6 +143,13 @@ const EomArticleAvRoute = EomArticleAvImport.update({
|
|||||||
getParentRoute: () => EomRoute,
|
getParentRoute: () => EomRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const logisticsSiloAdjustmentsHistRoute =
|
||||||
|
logisticsSiloAdjustmentsHistImport.update({
|
||||||
|
id: '/(logistics)/siloAdjustments/$hist',
|
||||||
|
path: '/siloAdjustments/$hist',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const logisticsMaterialHelperSiloLinkIndexRoute =
|
const logisticsMaterialHelperSiloLinkIndexRoute =
|
||||||
logisticsMaterialHelperSiloLinkIndexImport.update({
|
logisticsMaterialHelperSiloLinkIndexImport.update({
|
||||||
id: '/(logistics)/materialHelper/siloLink/',
|
id: '/(logistics)/materialHelper/siloLink/',
|
||||||
@@ -223,6 +238,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AdminSettingsImport
|
preLoaderRoute: typeof AdminSettingsImport
|
||||||
parentRoute: typeof AdminImport
|
parentRoute: typeof AdminImport
|
||||||
}
|
}
|
||||||
|
'/_admin/subModules': {
|
||||||
|
id: '/_admin/subModules'
|
||||||
|
path: '/subModules'
|
||||||
|
fullPath: '/subModules'
|
||||||
|
preLoaderRoute: typeof AdminSubModulesImport
|
||||||
|
parentRoute: typeof AdminImport
|
||||||
|
}
|
||||||
'/_admin/users': {
|
'/_admin/users': {
|
||||||
id: '/_admin/users'
|
id: '/_admin/users'
|
||||||
path: '/users'
|
path: '/users'
|
||||||
@@ -251,6 +273,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof OcpIndexImport
|
preLoaderRoute: typeof OcpIndexImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/(logistics)/siloAdjustments/$hist': {
|
||||||
|
id: '/(logistics)/siloAdjustments/$hist'
|
||||||
|
path: '/siloAdjustments/$hist'
|
||||||
|
fullPath: '/siloAdjustments/$hist'
|
||||||
|
preLoaderRoute: typeof logisticsSiloAdjustmentsHistImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/_eom/article/$av': {
|
'/_eom/article/$av': {
|
||||||
id: '/_eom/article/$av'
|
id: '/_eom/article/$av'
|
||||||
path: '/article/$av'
|
path: '/article/$av'
|
||||||
@@ -309,6 +338,7 @@ interface AdminRouteChildren {
|
|||||||
AdminModulesRoute: typeof AdminModulesRoute
|
AdminModulesRoute: typeof AdminModulesRoute
|
||||||
AdminServersRoute: typeof AdminServersRoute
|
AdminServersRoute: typeof AdminServersRoute
|
||||||
AdminSettingsRoute: typeof AdminSettingsRoute
|
AdminSettingsRoute: typeof AdminSettingsRoute
|
||||||
|
AdminSubModulesRoute: typeof AdminSubModulesRoute
|
||||||
AdminUsersRoute: typeof AdminUsersRoute
|
AdminUsersRoute: typeof AdminUsersRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,6 +346,7 @@ const AdminRouteChildren: AdminRouteChildren = {
|
|||||||
AdminModulesRoute: AdminModulesRoute,
|
AdminModulesRoute: AdminModulesRoute,
|
||||||
AdminServersRoute: AdminServersRoute,
|
AdminServersRoute: AdminServersRoute,
|
||||||
AdminSettingsRoute: AdminSettingsRoute,
|
AdminSettingsRoute: AdminSettingsRoute,
|
||||||
|
AdminSubModulesRoute: AdminSubModulesRoute,
|
||||||
AdminUsersRoute: AdminUsersRoute,
|
AdminUsersRoute: AdminUsersRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,10 +382,12 @@ export interface FileRoutesByFullPath {
|
|||||||
'/modules': typeof AdminModulesRoute
|
'/modules': typeof AdminModulesRoute
|
||||||
'/servers': typeof AdminServersRoute
|
'/servers': typeof AdminServersRoute
|
||||||
'/settings': typeof AdminSettingsRoute
|
'/settings': typeof AdminSettingsRoute
|
||||||
|
'/subModules': typeof AdminSubModulesRoute
|
||||||
'/users': typeof AdminUsersRoute
|
'/users': typeof AdminUsersRoute
|
||||||
'/profile': typeof AuthProfileRoute
|
'/profile': typeof AuthProfileRoute
|
||||||
'/eom': typeof EomEomRoute
|
'/eom': typeof EomEomRoute
|
||||||
'/ocp': typeof OcpIndexRoute
|
'/ocp': typeof OcpIndexRoute
|
||||||
|
'/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
|
||||||
'/article/$av': typeof EomArticleAvRoute
|
'/article/$av': typeof EomArticleAvRoute
|
||||||
'/materialHelper': typeof logisticsMaterialHelperIndexRoute
|
'/materialHelper': typeof logisticsMaterialHelperIndexRoute
|
||||||
'/siloAdjustments': typeof logisticsSiloAdjustmentsIndexRoute
|
'/siloAdjustments': typeof logisticsSiloAdjustmentsIndexRoute
|
||||||
@@ -372,10 +405,12 @@ export interface FileRoutesByTo {
|
|||||||
'/modules': typeof AdminModulesRoute
|
'/modules': typeof AdminModulesRoute
|
||||||
'/servers': typeof AdminServersRoute
|
'/servers': typeof AdminServersRoute
|
||||||
'/settings': typeof AdminSettingsRoute
|
'/settings': typeof AdminSettingsRoute
|
||||||
|
'/subModules': typeof AdminSubModulesRoute
|
||||||
'/users': typeof AdminUsersRoute
|
'/users': typeof AdminUsersRoute
|
||||||
'/profile': typeof AuthProfileRoute
|
'/profile': typeof AuthProfileRoute
|
||||||
'/eom': typeof EomEomRoute
|
'/eom': typeof EomEomRoute
|
||||||
'/ocp': typeof OcpIndexRoute
|
'/ocp': typeof OcpIndexRoute
|
||||||
|
'/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
|
||||||
'/article/$av': typeof EomArticleAvRoute
|
'/article/$av': typeof EomArticleAvRoute
|
||||||
'/materialHelper': typeof logisticsMaterialHelperIndexRoute
|
'/materialHelper': typeof logisticsMaterialHelperIndexRoute
|
||||||
'/siloAdjustments': typeof logisticsSiloAdjustmentsIndexRoute
|
'/siloAdjustments': typeof logisticsSiloAdjustmentsIndexRoute
|
||||||
@@ -396,10 +431,12 @@ export interface FileRoutesById {
|
|||||||
'/_admin/modules': typeof AdminModulesRoute
|
'/_admin/modules': typeof AdminModulesRoute
|
||||||
'/_admin/servers': typeof AdminServersRoute
|
'/_admin/servers': typeof AdminServersRoute
|
||||||
'/_admin/settings': typeof AdminSettingsRoute
|
'/_admin/settings': typeof AdminSettingsRoute
|
||||||
|
'/_admin/subModules': typeof AdminSubModulesRoute
|
||||||
'/_admin/users': typeof AdminUsersRoute
|
'/_admin/users': typeof AdminUsersRoute
|
||||||
'/_auth/profile': typeof AuthProfileRoute
|
'/_auth/profile': typeof AuthProfileRoute
|
||||||
'/_eom/eom': typeof EomEomRoute
|
'/_eom/eom': typeof EomEomRoute
|
||||||
'/ocp/': typeof OcpIndexRoute
|
'/ocp/': typeof OcpIndexRoute
|
||||||
|
'/(logistics)/siloAdjustments/$hist': typeof logisticsSiloAdjustmentsHistRoute
|
||||||
'/_eom/article/$av': typeof EomArticleAvRoute
|
'/_eom/article/$av': typeof EomArticleAvRoute
|
||||||
'/(logistics)/materialHelper/': typeof logisticsMaterialHelperIndexRoute
|
'/(logistics)/materialHelper/': typeof logisticsMaterialHelperIndexRoute
|
||||||
'/(logistics)/siloAdjustments/': typeof logisticsSiloAdjustmentsIndexRoute
|
'/(logistics)/siloAdjustments/': typeof logisticsSiloAdjustmentsIndexRoute
|
||||||
@@ -419,10 +456,12 @@ export interface FileRouteTypes {
|
|||||||
| '/modules'
|
| '/modules'
|
||||||
| '/servers'
|
| '/servers'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/subModules'
|
||||||
| '/users'
|
| '/users'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/eom'
|
| '/eom'
|
||||||
| '/ocp'
|
| '/ocp'
|
||||||
|
| '/siloAdjustments/$hist'
|
||||||
| '/article/$av'
|
| '/article/$av'
|
||||||
| '/materialHelper'
|
| '/materialHelper'
|
||||||
| '/siloAdjustments'
|
| '/siloAdjustments'
|
||||||
@@ -439,10 +478,12 @@ export interface FileRouteTypes {
|
|||||||
| '/modules'
|
| '/modules'
|
||||||
| '/servers'
|
| '/servers'
|
||||||
| '/settings'
|
| '/settings'
|
||||||
|
| '/subModules'
|
||||||
| '/users'
|
| '/users'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
| '/eom'
|
| '/eom'
|
||||||
| '/ocp'
|
| '/ocp'
|
||||||
|
| '/siloAdjustments/$hist'
|
||||||
| '/article/$av'
|
| '/article/$av'
|
||||||
| '/materialHelper'
|
| '/materialHelper'
|
||||||
| '/siloAdjustments'
|
| '/siloAdjustments'
|
||||||
@@ -461,10 +502,12 @@ export interface FileRouteTypes {
|
|||||||
| '/_admin/modules'
|
| '/_admin/modules'
|
||||||
| '/_admin/servers'
|
| '/_admin/servers'
|
||||||
| '/_admin/settings'
|
| '/_admin/settings'
|
||||||
|
| '/_admin/subModules'
|
||||||
| '/_admin/users'
|
| '/_admin/users'
|
||||||
| '/_auth/profile'
|
| '/_auth/profile'
|
||||||
| '/_eom/eom'
|
| '/_eom/eom'
|
||||||
| '/ocp/'
|
| '/ocp/'
|
||||||
|
| '/(logistics)/siloAdjustments/$hist'
|
||||||
| '/_eom/article/$av'
|
| '/_eom/article/$av'
|
||||||
| '/(logistics)/materialHelper/'
|
| '/(logistics)/materialHelper/'
|
||||||
| '/(logistics)/siloAdjustments/'
|
| '/(logistics)/siloAdjustments/'
|
||||||
@@ -483,6 +526,7 @@ export interface RootRouteChildren {
|
|||||||
AboutRoute: typeof AboutRoute
|
AboutRoute: typeof AboutRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
OcpIndexRoute: typeof OcpIndexRoute
|
OcpIndexRoute: typeof OcpIndexRoute
|
||||||
|
logisticsSiloAdjustmentsHistRoute: typeof logisticsSiloAdjustmentsHistRoute
|
||||||
logisticsMaterialHelperIndexRoute: typeof logisticsMaterialHelperIndexRoute
|
logisticsMaterialHelperIndexRoute: typeof logisticsMaterialHelperIndexRoute
|
||||||
logisticsSiloAdjustmentsIndexRoute: typeof logisticsSiloAdjustmentsIndexRoute
|
logisticsSiloAdjustmentsIndexRoute: typeof logisticsSiloAdjustmentsIndexRoute
|
||||||
ocmeCyclecountIndexRoute: typeof ocmeCyclecountIndexRoute
|
ocmeCyclecountIndexRoute: typeof ocmeCyclecountIndexRoute
|
||||||
@@ -499,6 +543,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
AboutRoute: AboutRoute,
|
AboutRoute: AboutRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
OcpIndexRoute: OcpIndexRoute,
|
OcpIndexRoute: OcpIndexRoute,
|
||||||
|
logisticsSiloAdjustmentsHistRoute: logisticsSiloAdjustmentsHistRoute,
|
||||||
logisticsMaterialHelperIndexRoute: logisticsMaterialHelperIndexRoute,
|
logisticsMaterialHelperIndexRoute: logisticsMaterialHelperIndexRoute,
|
||||||
logisticsSiloAdjustmentsIndexRoute: logisticsSiloAdjustmentsIndexRoute,
|
logisticsSiloAdjustmentsIndexRoute: logisticsSiloAdjustmentsIndexRoute,
|
||||||
ocmeCyclecountIndexRoute: ocmeCyclecountIndexRoute,
|
ocmeCyclecountIndexRoute: ocmeCyclecountIndexRoute,
|
||||||
@@ -527,6 +572,7 @@ export const routeTree = rootRoute
|
|||||||
"/about",
|
"/about",
|
||||||
"/login",
|
"/login",
|
||||||
"/ocp/",
|
"/ocp/",
|
||||||
|
"/(logistics)/siloAdjustments/$hist",
|
||||||
"/(logistics)/materialHelper/",
|
"/(logistics)/materialHelper/",
|
||||||
"/(logistics)/siloAdjustments/",
|
"/(logistics)/siloAdjustments/",
|
||||||
"/(ocme)/cyclecount/",
|
"/(ocme)/cyclecount/",
|
||||||
@@ -544,6 +590,7 @@ export const routeTree = rootRoute
|
|||||||
"/_admin/modules",
|
"/_admin/modules",
|
||||||
"/_admin/servers",
|
"/_admin/servers",
|
||||||
"/_admin/settings",
|
"/_admin/settings",
|
||||||
|
"/_admin/subModules",
|
||||||
"/_admin/users"
|
"/_admin/users"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -578,6 +625,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "_admin/settings.tsx",
|
"filePath": "_admin/settings.tsx",
|
||||||
"parent": "/_admin"
|
"parent": "/_admin"
|
||||||
},
|
},
|
||||||
|
"/_admin/subModules": {
|
||||||
|
"filePath": "_admin/subModules.tsx",
|
||||||
|
"parent": "/_admin"
|
||||||
|
},
|
||||||
"/_admin/users": {
|
"/_admin/users": {
|
||||||
"filePath": "_admin/users.tsx",
|
"filePath": "_admin/users.tsx",
|
||||||
"parent": "/_admin"
|
"parent": "/_admin"
|
||||||
@@ -593,6 +644,9 @@ export const routeTree = rootRoute
|
|||||||
"/ocp/": {
|
"/ocp/": {
|
||||||
"filePath": "ocp/index.tsx"
|
"filePath": "ocp/index.tsx"
|
||||||
},
|
},
|
||||||
|
"/(logistics)/siloAdjustments/$hist": {
|
||||||
|
"filePath": "(logistics)/siloAdjustments/$hist.tsx"
|
||||||
|
},
|
||||||
"/_eom/article/$av": {
|
"/_eom/article/$av": {
|
||||||
"filePath": "_eom/article/$av.tsx",
|
"filePath": "_eom/article/$av.tsx",
|
||||||
"parent": "/_eom"
|
"parent": "/_eom"
|
||||||
|
|||||||
31
frontend/src/routes/(logistics)/siloAdjustments/$hist.tsx
Normal file
31
frontend/src/routes/(logistics)/siloAdjustments/$hist.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/(logistics)/siloAdjustments/$hist")({
|
||||||
|
component: RouteComponent,
|
||||||
|
beforeLoad: async () => {
|
||||||
|
const auth = localStorage.getItem("auth_token");
|
||||||
|
if (!auth) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/login",
|
||||||
|
search: {
|
||||||
|
// Use the current location to power a redirect after login
|
||||||
|
// (Do not use `router.state.resolvedLocation` as it can
|
||||||
|
// potentially lag behind the actual current location)
|
||||||
|
redirect: location.pathname + location.search,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// In a loader
|
||||||
|
loader: ({ params }) => params.hist,
|
||||||
|
// Or in a component
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Hello "/(logistics)/siloAdjustments/$hist"! where the historical
|
||||||
|
data will be shown in a table alone with a graph
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import ModulesPage from "@/components/admin/modules/ModulePage";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
export const Route = createFileRoute('/_admin/modules')({
|
export const Route = createFileRoute("/_admin/modules")({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
return <div>Hello "/_admin/modules"!</div>
|
return (
|
||||||
|
<div>
|
||||||
|
<ModulesPage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
14
frontend/src/routes/_admin/subModules.tsx
Normal file
14
frontend/src/routes/_admin/subModules.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import SubModulePage from "@/components/admin/supModules/SubModulePage";
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_admin/subModules")({
|
||||||
|
component: RouteComponent,
|
||||||
|
});
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SubModulePage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
frontend/src/utils/querys/admin/modules.tsx
Normal file
25
frontend/src/utils/querys/admin/modules.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getModules(token: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["modules"],
|
||||||
|
queryFn: () => fetchSettings(token),
|
||||||
|
enabled: !!token,
|
||||||
|
//staleTime: 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSettings = async (token: string) => {
|
||||||
|
const { data } = await axios.get("/api/server/modules", {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
const url: string = window.location.host.split(":")[0];
|
||||||
|
let settingsData = data.data;
|
||||||
|
if (url != "localhost") {
|
||||||
|
settingsData.filter((n: any) => n.name === "devDir");
|
||||||
|
}
|
||||||
|
return settingsData;
|
||||||
|
};
|
||||||
25
frontend/src/utils/querys/admin/subModules.tsx
Normal file
25
frontend/src/utils/querys/admin/subModules.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getSubModules(token: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["submodules"],
|
||||||
|
queryFn: () => fetchSettings(token),
|
||||||
|
enabled: !!token,
|
||||||
|
//staleTime: 1000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSettings = async (token: string) => {
|
||||||
|
const { data } = await axios.get("/api/server/submodules", {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
const url: string = window.location.host.split(":")[0];
|
||||||
|
let settingsData = data.data;
|
||||||
|
if (url != "localhost") {
|
||||||
|
settingsData.filter((n: any) => n.name === "devDir");
|
||||||
|
}
|
||||||
|
return settingsData;
|
||||||
|
};
|
||||||
26
frontend/src/utils/querys/admin/userRoles.tsx
Normal file
26
frontend/src/utils/querys/admin/userRoles.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getUserRoles() {
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["getUsers"],
|
||||||
|
queryFn: () => fetchUsers(token),
|
||||||
|
enabled: !!token, // Prevents query if token is null
|
||||||
|
staleTime: 1000,
|
||||||
|
//refetchInterval: 2 * 2000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUsers = async (token: string | null) => {
|
||||||
|
const { data } = await axios.get(`/api/auth/allusersroles`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
//const url: string = window.location.host.split(":")[0];
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { queryOptions } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export function getStockSilo() {
|
||||||
|
const token = localStorage.getItem("auth_token");
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["getUsers"],
|
||||||
|
queryFn: () => fetchStockSilo(token),
|
||||||
|
enabled: !!token, // Prevents query if token is null
|
||||||
|
staleTime: 1000,
|
||||||
|
//refetchInterval: 2 * 2000,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchStockSilo = async (token: string | null) => {
|
||||||
|
const { data } = await axios.get(`/api/logistics/getsilosdjustment`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// if we are not localhost ignore the devDir setting.
|
||||||
|
//const url: string = window.location.host.split(":")[0];
|
||||||
|
return data.data ?? [];
|
||||||
|
};
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
"commit": "cz",
|
"commit": "cz",
|
||||||
"prodinstall": "npm i --omit=dev && npm run db:migrate",
|
"prodinstall": "npm i --omit=dev && npm run db:migrate",
|
||||||
"checkupdates": "npx npm-check-updates",
|
"checkupdates": "npx npm-check-updates",
|
||||||
"testingCode": "dotenvx run -f .env -- tsx watch server/services/notifications/controller/notifications/tiIntergrationV1.ts"
|
"testingCode": "dotenvx run -f .env -- tsx watch server/services/logistics/controller/siloAdjustments/migrateAdjustments.ts"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admConfig": {
|
"admConfig": {
|
||||||
"build": 174,
|
"build": 181,
|
||||||
"oldBuild": "backend-0.1.3.zip"
|
"oldBuild": "backend-0.1.3.zip"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { areRolesIn } from "./utils/roleCheck.js";
|
|||||||
import createUser from "./routes/userAdmin/createUser.js";
|
import createUser from "./routes/userAdmin/createUser.js";
|
||||||
import allUsers from "./routes/userAdmin/getUsers.js";
|
import allUsers from "./routes/userAdmin/getUsers.js";
|
||||||
import updateUser from "./routes/userAdmin/updateUser.js";
|
import updateUser from "./routes/userAdmin/updateUser.js";
|
||||||
|
import allUserRoles from "./routes/userAdmin/getAllUserRoles.js";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ const routes = [
|
|||||||
setAccess,
|
setAccess,
|
||||||
createUser,
|
createUser,
|
||||||
allUsers,
|
allUsers,
|
||||||
|
allUserRoles,
|
||||||
updateUser,
|
updateUser,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
|
import { userRoles } from "../../../../../database/schema/userRoles.js";
|
||||||
|
import { returnRes } from "../../../../globalUtils/routeDefs/returnRes.js";
|
||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
|
||||||
|
export const getAllUsersRoles = async () => {
|
||||||
|
/**
|
||||||
|
* returns all users that are in lst
|
||||||
|
*/
|
||||||
|
createLog("info", "apiAuthedRoute", "auth", "Get all users");
|
||||||
|
const { data, error } = await tryCatch(db.select().from(userRoles));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
returnRes(
|
||||||
|
false,
|
||||||
|
"There was an error getting users",
|
||||||
|
new Error("No user exists.")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnRes(true, "All users.", data);
|
||||||
|
return { success: true, message: "All users", data };
|
||||||
|
};
|
||||||
@@ -3,15 +3,27 @@ pass over a users uuid and return all modules they have permission too.
|
|||||||
in the login route we attach it to user under roles.
|
in the login route we attach it to user under roles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import {db} from "../../../../../database/dbclient.js";
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
import {userRoles} from "../../../../../database/schema/userRoles.js";
|
import { userRoles } from "../../../../../database/schema/userRoles.js";
|
||||||
|
|
||||||
export const roleCheck = async (user_id: string | undefined) => {
|
export const roleCheck = async (user_id: string | undefined) => {
|
||||||
if (!user_id) {
|
if (!user_id) {
|
||||||
throw Error("Missing user_id");
|
throw Error("Missing user_id");
|
||||||
}
|
}
|
||||||
|
let returnRoles: any = [];
|
||||||
// get the user roles by the user_id
|
// get the user roles by the user_id
|
||||||
|
returnRoles = await db
|
||||||
|
.select({
|
||||||
|
user_id: userRoles.user_id,
|
||||||
|
role_id: userRoles.role_id,
|
||||||
|
module_id: userRoles.module_id,
|
||||||
|
role: userRoles.role,
|
||||||
|
})
|
||||||
|
.from(userRoles)
|
||||||
|
.where(eq(userRoles.user_id, user_id));
|
||||||
|
|
||||||
|
if (returnRoles[0]?.role.includes("systemAdmin")) {
|
||||||
const roles = await db
|
const roles = await db
|
||||||
.select({
|
.select({
|
||||||
user_id: userRoles.user_id,
|
user_id: userRoles.user_id,
|
||||||
@@ -23,4 +35,7 @@ export const roleCheck = async (user_id: string | undefined) => {
|
|||||||
.where(eq(userRoles.user_id, user_id));
|
.where(eq(userRoles.user_id, user_id));
|
||||||
|
|
||||||
return roles;
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnRoles;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,21 +3,38 @@ pass over a users uuid and return all modules they have permission too.
|
|||||||
in the login route we attach it to user under roles.
|
in the login route we attach it to user under roles.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {eq} from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import {db} from "../../../../../database/dbclient.js";
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
import {userRoles} from "../../../../../database/schema/userRoles.js";
|
import { userRoles } from "../../../../../database/schema/userRoles.js";
|
||||||
import {users} from "../../../../../database/schema/users.js";
|
import { users } from "../../../../../database/schema/users.js";
|
||||||
import {modules} from "../../../../../database/schema/modules.js";
|
import { modules } from "../../../../../database/schema/modules.js";
|
||||||
import {roles} from "../../../../../database/schema/roles.js";
|
import { roles } from "../../../../../database/schema/roles.js";
|
||||||
import {setSysAdmin} from "./setSysAdmin.js";
|
import { setSysAdmin } from "./setSysAdmin.js";
|
||||||
|
|
||||||
export const setUserAccess = async (username: string, moduleName: string, roleName: string, override?: string) => {
|
export const setUserAccess = async (
|
||||||
|
username: string,
|
||||||
|
moduleName: string,
|
||||||
|
roleName: string,
|
||||||
|
override?: string
|
||||||
|
) => {
|
||||||
// get the user roles by the user_id
|
// get the user roles by the user_id
|
||||||
const user = await db.select().from(users).where(eq(users.username, username));
|
const user = await db
|
||||||
const module = await db.select().from(modules).where(eq(modules.name, moduleName));
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.username, username));
|
||||||
|
const module = await db
|
||||||
|
.select()
|
||||||
|
.from(modules)
|
||||||
|
.where(eq(modules.name, moduleName));
|
||||||
|
|
||||||
if (process.env.SECRETOVERRIDECODE != override && roleName === "systemAdmin") {
|
if (
|
||||||
return {success: false, message: "The override code provided is invalid."};
|
process.env.SECRETOVERRIDECODE != override &&
|
||||||
|
roleName === "systemAdmin"
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "The override code provided is invalid.",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const role = await db.select().from(roles).where(eq(roles.name, roleName));
|
const role = await db.select().from(roles).where(eq(roles.name, roleName));
|
||||||
@@ -37,9 +54,12 @@ export const setUserAccess = async (username: string, moduleName: string, roleNa
|
|||||||
|
|
||||||
// set the user
|
// set the user
|
||||||
try {
|
try {
|
||||||
const userRole = await db
|
const userRole = await db.insert(userRoles).values({
|
||||||
.insert(userRoles)
|
user_id: user[0].user_id,
|
||||||
.values({user_id: user[0].user_id, role_id: role[0].role_id, module_id: module[0].module_id, role: roleName});
|
role_id: role[0].role_id,
|
||||||
|
module_id: module[0].module_id,
|
||||||
|
role: roleName,
|
||||||
|
});
|
||||||
//.returning({user: users.username, email: users.email});
|
//.returning({user: users.username, email: users.email});
|
||||||
|
|
||||||
// return c.json({message: "User Registered", user}, 200);
|
// return c.json({message: "User Registered", user}, 200);
|
||||||
@@ -48,10 +68,38 @@ export const setUserAccess = async (username: string, moduleName: string, roleNa
|
|||||||
message: `${username} has been granted access to ${moduleName} with the role ${roleName}`,
|
message: `${username} has been granted access to ${moduleName} with the role ${roleName}`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await changeRole(
|
||||||
|
roleName,
|
||||||
|
user[0].user_id,
|
||||||
|
module[0].module_id,
|
||||||
|
role[0].role_id
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
message: `There was an error granting ${username} access to ${moduleName} with the role ${roleName}`,
|
message: `${username} access on ${moduleName} has been changed to ${roleName}`,
|
||||||
data: error,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeRole = async (
|
||||||
|
role: any,
|
||||||
|
userID: any,
|
||||||
|
moduleID: any,
|
||||||
|
roleID: any
|
||||||
|
) => {
|
||||||
|
await db
|
||||||
|
.delete(userRoles)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userRoles.user_id, userID),
|
||||||
|
eq(userRoles.module_id, moduleID)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const userRole = await db.insert(userRoles).values({
|
||||||
|
user_id: userID,
|
||||||
|
role_id: roleID,
|
||||||
|
module_id: moduleID,
|
||||||
|
role: role,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
34
server/services/auth/routes/userAdmin/getAllUserRoles.ts
Normal file
34
server/services/auth/routes/userAdmin/getAllUserRoles.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
||||||
|
|
||||||
|
import { responses } from "../../../../globalUtils/routeDefs/responses.js";
|
||||||
|
import { authMiddleware } from "../../middleware/authMiddleware.js";
|
||||||
|
import hasCorrectRole from "../../middleware/roleCheck.js";
|
||||||
|
import { getAllUsersRoles } from "../../controllers/userAdmin/getAllUserRoles.js";
|
||||||
|
|
||||||
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
tags: ["Auth:admin"],
|
||||||
|
summary: "Gets Users Roles",
|
||||||
|
method: "get",
|
||||||
|
path: "/allusersroles",
|
||||||
|
middleware: [
|
||||||
|
authMiddleware,
|
||||||
|
hasCorrectRole(["admin", "systemAdmin"], "admin"),
|
||||||
|
],
|
||||||
|
responses: responses(),
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
// apit hit
|
||||||
|
//apiHit(c, {endpoint: "api/auth/register"});
|
||||||
|
const allUsers: any = await getAllUsersRoles();
|
||||||
|
return c.json({
|
||||||
|
success: allUsers?.success,
|
||||||
|
message: allUsers?.message,
|
||||||
|
data: allUsers?.data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -4,7 +4,6 @@ import getArticles from "./route/getActiveArticles.js";
|
|||||||
import currentInv from "./route/getInventory.js";
|
import currentInv from "./route/getInventory.js";
|
||||||
import getCustomerInv from "./route/getCustomerInv.js";
|
import getCustomerInv from "./route/getCustomerInv.js";
|
||||||
import getOpenOrders from "./route/getOpenOrders.js";
|
import getOpenOrders from "./route/getOpenOrders.js";
|
||||||
import siloAdjustments from "./route/getSiloAdjustments.js";
|
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -14,7 +13,6 @@ const routes = [
|
|||||||
currentInv,
|
currentInv,
|
||||||
getCustomerInv,
|
getCustomerInv,
|
||||||
getOpenOrders,
|
getOpenOrders,
|
||||||
siloAdjustments,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const appRoutes = routes.forEach((route) => {
|
const appRoutes = routes.forEach((route) => {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const current: any = [
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: "getSiloAdjustment",
|
name: "getSiloAdjustment",
|
||||||
endpoint: "/api/v1/warehouse/getSiloAdjustment",
|
endpoint: "/api/logistics/getsilosdjustment",
|
||||||
// description: "Returns all inventory, by default excludes running numebrs, also excludes inv locations.",
|
// description: "Returns all inventory, by default excludes running numebrs, also excludes inv locations.",
|
||||||
description:
|
description:
|
||||||
"Returns all siloadjustments in selected date range IE: 1/1/2025 to 1/31/2025",
|
"Returns all siloadjustments in selected date range IE: 1/1/2025 to 1/31/2025",
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { between, desc, gte, lte } from "drizzle-orm";
|
||||||
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
|
import { siloAdjustments } from "../../../../../database/schema/siloAdjustments.js";
|
||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
|
||||||
|
export const getSiloAdjustments = async (startDate: any, endDate: any) => {
|
||||||
|
/**
|
||||||
|
* Returns silo adjustments by date or all
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
const { data: adjRange, error: adjRangeError } = await tryCatch(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(siloAdjustments)
|
||||||
|
.where(
|
||||||
|
between(
|
||||||
|
siloAdjustments.dateAdjusted,
|
||||||
|
new Date(startDate),
|
||||||
|
new Date(endDate)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(siloAdjustments.dateAdjusted))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (adjRangeError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error getting silo adjustments.",
|
||||||
|
adjRangeError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Silo adjustment data.",
|
||||||
|
data: adjRange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startDate) {
|
||||||
|
const { data: adjRange, error: adjRangeError } = await tryCatch(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(siloAdjustments)
|
||||||
|
.where(gte(siloAdjustments.dateAdjusted, new Date(startDate)))
|
||||||
|
.orderBy(desc(siloAdjustments.dateAdjusted))
|
||||||
|
);
|
||||||
|
if (adjRangeError)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error getting silo adjustments.",
|
||||||
|
adjRangeError,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Silo adjustment data.",
|
||||||
|
data: adjRange,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: adjRange, error: adjRangeError } = await tryCatch(
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(siloAdjustments)
|
||||||
|
.orderBy(desc(siloAdjustments.dateAdjusted))
|
||||||
|
);
|
||||||
|
if (adjRangeError)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error getting silo adjustments.",
|
||||||
|
adjRangeError,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Silo adjustment data.",
|
||||||
|
data: adjRange,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* 1. Get the silo adjustments from lstv1
|
||||||
|
* 2. Build the new data set to match the new system
|
||||||
|
* 3. insert the new values
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
|
import { siloAdjustments } from "../../../../../database/schema/siloAdjustments.js";
|
||||||
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
import { delay } from "../../../../globalUtils/delay.js";
|
||||||
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
|
import { settings } from "../../../../../database/schema/settings.js";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const migrateAdjustments = async () => {
|
||||||
|
/**
|
||||||
|
* Migrates the silo adjustments from v1 to v2
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { data, error } = await tryCatch(db.select().from(settings));
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
createLog("error", "silo", "logistics", "Getting settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrationCompleted = data?.filter(
|
||||||
|
(n) => n.name === "siloAdjMigrations"
|
||||||
|
);
|
||||||
|
const server = data?.filter((n) => n.name === "v1SysServer");
|
||||||
|
const port = data?.filter((n) => n.name === "v1SysPort");
|
||||||
|
createLog("info", "silo", "logistics", "Getting v1 silo data.");
|
||||||
|
|
||||||
|
if (migrationCompleted[0]?.value === "1") {
|
||||||
|
createLog(
|
||||||
|
"info",
|
||||||
|
"silo",
|
||||||
|
"logistics",
|
||||||
|
"Migrations have already been completed on this server."
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data: s, error: siloError } = await tryCatch(
|
||||||
|
axios.get(
|
||||||
|
`http://${server[0].value}:${port[0].value}/api/v1/warehouse/getSilosAdjustment?startDate=1/1/2020&endDate=4/1/2026`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (siloError) {
|
||||||
|
createLog("error", "silo", "logistics", "Getting settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate all the silo adjustments :D
|
||||||
|
*/
|
||||||
|
const silo: any = s?.data;
|
||||||
|
createLog("info", "silo", "logistics", "Starting migration.");
|
||||||
|
for (let i = 0; i < silo.length; i++) {
|
||||||
|
const migrate = await db.insert(siloAdjustments).values({
|
||||||
|
warehouseID: silo[0].warehouseID,
|
||||||
|
locationID: silo[0].locationID,
|
||||||
|
currentStockLevel: silo[0].currentStockLevel,
|
||||||
|
newLevel: silo[0].newLevel,
|
||||||
|
dateAdjusted: new Date(silo[0].dateAdjusted),
|
||||||
|
lastDateAdjusted: new Date(silo[0].lastDateAdjusted),
|
||||||
|
add_user: silo[0].add_user,
|
||||||
|
});
|
||||||
|
createLog(
|
||||||
|
"info",
|
||||||
|
"silo",
|
||||||
|
"logistics",
|
||||||
|
`Migrations for Date ${silo[0].dateAdjusted} on silo: ${silo[0].locationID}`
|
||||||
|
);
|
||||||
|
await delay(120);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* change the migration setting to be completed
|
||||||
|
*/
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(settings)
|
||||||
|
.set({ value: "1" })
|
||||||
|
.where(eq(settings.name, "siloAdjMigrations"));
|
||||||
|
createLog("info", "silo", "logistics", "Migration completed.");
|
||||||
|
};
|
||||||
|
|
||||||
|
migrateAdjustments();
|
||||||
@@ -5,6 +5,8 @@ import returnMat from "./route/returnMaterial.js";
|
|||||||
import createSiloAdjustment from "./route/siloAdjustments/createSiloAdjustment.js";
|
import createSiloAdjustment from "./route/siloAdjustments/createSiloAdjustment.js";
|
||||||
import postComment from "./route/siloAdjustments/postComment.js";
|
import postComment from "./route/siloAdjustments/postComment.js";
|
||||||
import getStockSilo from "./route/siloAdjustments/getStockData.js";
|
import getStockSilo from "./route/siloAdjustments/getStockData.js";
|
||||||
|
import { migrateAdjustments } from "./controller/siloAdjustments/migrateAdjustments.js";
|
||||||
|
import getSiloAdjustments from "./route/siloAdjustments/getSiloAdjustments.js";
|
||||||
|
|
||||||
const app = new OpenAPIHono();
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
@@ -16,6 +18,7 @@ const routes = [
|
|||||||
createSiloAdjustment,
|
createSiloAdjustment,
|
||||||
postComment,
|
postComment,
|
||||||
getStockSilo,
|
getStockSilo,
|
||||||
|
getSiloAdjustments,
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// app.route("/server", modules);
|
// app.route("/server", modules);
|
||||||
@@ -23,4 +26,8 @@ const appRoutes = routes.forEach((route) => {
|
|||||||
app.route("/logistics", route);
|
app.route("/logistics", route);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
migrateAdjustments();
|
||||||
|
}, 10 * 1000);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
import { responses } from "../../../globalUtils/routeDefs/responses.js";
|
import { responses } from "../../../../globalUtils/routeDefs/responses.js";
|
||||||
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||||
import { getOpenOrders } from "../controller/getOpenOrders.js";
|
import { getOpenOrders } from "../../../dataMart/controller/getOpenOrders.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { getSiloAdjustments } from "../../controller/siloAdjustments/getSiloAdjustments.js";
|
||||||
|
|
||||||
const app = new OpenAPIHono({ strict: false });
|
const app = new OpenAPIHono({ strict: false });
|
||||||
// const Body = z.object({
|
// const Body = z.object({
|
||||||
@@ -23,7 +24,7 @@ app.openapi(
|
|||||||
// },
|
// },
|
||||||
responses: responses(),
|
responses: responses(),
|
||||||
}),
|
}),
|
||||||
async (c) => {
|
async (c: any) => {
|
||||||
const customer: any = c.req.queries();
|
const customer: any = c.req.queries();
|
||||||
|
|
||||||
// make sure we have a vaid user being accessed thats really logged in
|
// make sure we have a vaid user being accessed thats really logged in
|
||||||
@@ -45,13 +46,20 @@ app.openapi(
|
|||||||
|
|
||||||
const dates: any = c.req.queries();
|
const dates: any = c.req.queries();
|
||||||
|
|
||||||
|
// const { data, error } = await tryCatch(
|
||||||
|
// axios.get(
|
||||||
|
// `/api/v1/warehouse/getSilosAdjustment?startDate=${dates.startDate[0]}&endDate=${dates.endDate[0]}`
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
const startDate = dates.startDate ? dates.startDate[0] : null;
|
||||||
|
const endDate = dates.endDate ? dates.endDate[0] : null;
|
||||||
|
|
||||||
const { data, error } = await tryCatch(
|
const { data, error } = await tryCatch(
|
||||||
axios.get(
|
getSiloAdjustments(startDate, endDate)
|
||||||
`http://localhost:4400/api/v1/warehouse/getSilosAdjustment?startDate=${dates.startDate[0]}&endDate=${dates.endDate[0]}`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.log(error);
|
||||||
return c.json({
|
return c.json({
|
||||||
success: false,
|
success: false,
|
||||||
message: "Error running query",
|
message: "Error running query",
|
||||||
@@ -60,9 +68,9 @@ app.openapi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: data?.data.success,
|
success: data?.success,
|
||||||
message: data?.data.message,
|
message: data?.message,
|
||||||
data: data?.data.data,
|
data: data?.data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -59,6 +59,8 @@ app.openapi(
|
|||||||
data.key,
|
data.key,
|
||||||
payload.user
|
payload.user
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log(addComment);
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
success: addComment.success,
|
success: addComment.success,
|
||||||
|
|||||||
@@ -214,9 +214,15 @@ export const labelingProcess = async ({
|
|||||||
// create the label
|
// create the label
|
||||||
const label = await createLabel(filteredLot[0], userPrinted);
|
const label = await createLabel(filteredLot[0], userPrinted);
|
||||||
|
|
||||||
// if (!label.success) {
|
if (!label.success) {
|
||||||
// return { sucess: false, message: label.message, data: label.data };
|
createLog(
|
||||||
// }
|
"error",
|
||||||
|
"labeling",
|
||||||
|
"ocp",
|
||||||
|
`There was an error creating the label: ${label.message}`
|
||||||
|
);
|
||||||
|
return { sucess: false, message: label.message, data: label.data };
|
||||||
|
}
|
||||||
|
|
||||||
// send over to be booked in if we can do it.
|
// send over to be booked in if we can do it.
|
||||||
const bookin = settingData.filter((s) => s.name === "bookin");
|
const bookin = settingData.filter((s) => s.name === "bookin");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
|||||||
import { lstAuth } from "../../../../index.js";
|
import { lstAuth } from "../../../../index.js";
|
||||||
import { db } from "../../../../../database/dbclient.js";
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
import { createLog } from "../../../logger/logger.js";
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
import { assignedPrinters } from "../../utils/checkAssignments.js";
|
||||||
|
|
||||||
export const updatePrinters = async () => {
|
export const updatePrinters = async () => {
|
||||||
const currentTime = new Date(Date.now());
|
const currentTime = new Date(Date.now());
|
||||||
@@ -80,5 +81,7 @@ export const updatePrinters = async () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await assignedPrinters();
|
||||||
|
|
||||||
return { success: true, message: "Printers were just added or updated." };
|
return { success: true, message: "Printers were just added or updated." };
|
||||||
};
|
};
|
||||||
|
|||||||
42
server/services/server/controller/module/updateSubModule.ts
Normal file
42
server/services/server/controller/module/updateSubModule.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "../../../../../database/dbclient.js";
|
||||||
|
import { createLog } from "../../../logger/logger.js";
|
||||||
|
import { subModules } from "../../../../../database/schema/subModules.js";
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
export const updateSubModule = async (data: Data, subModuleID: string) => {
|
||||||
|
createLog("info", "lst", "server", "Module being updated");
|
||||||
|
let module;
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
if (typeof data.active !== "boolean") {
|
||||||
|
createLog(
|
||||||
|
"error",
|
||||||
|
"lst",
|
||||||
|
"server",
|
||||||
|
"Invalid data type: 'active' must be a boolean"
|
||||||
|
);
|
||||||
|
throw new Error("'active' must be a boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
module = await db
|
||||||
|
.update(subModules)
|
||||||
|
.set({ active: data.active })
|
||||||
|
.where(eq(subModules.submodule_id, subModuleID))
|
||||||
|
.returning({ name: subModules.name });
|
||||||
|
//.where(sql`${userRole} = ANY(roles)`);
|
||||||
|
} catch (error) {
|
||||||
|
createLog(
|
||||||
|
"error",
|
||||||
|
"lst",
|
||||||
|
"server",
|
||||||
|
"There was an error updating the module"
|
||||||
|
);
|
||||||
|
throw new Error("There was an error updating the module");
|
||||||
|
}
|
||||||
|
return module;
|
||||||
|
};
|
||||||
134
server/services/server/route/modules/updateSubModules.ts
Normal file
134
server/services/server/route/modules/updateSubModules.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { z, createRoute, OpenAPIHono } from "@hono/zod-openapi";
|
||||||
|
import type { User } from "../../../../types/users.js";
|
||||||
|
import { verify } from "hono/jwt";
|
||||||
|
|
||||||
|
import { authMiddleware } from "../../../auth/middleware/authMiddleware.js";
|
||||||
|
import { updateSubModule } from "../../controller/module/updateSubModule.js";
|
||||||
|
|
||||||
|
// Define the response schema
|
||||||
|
const responseSchema = z.object({
|
||||||
|
message: z.string().optional(),
|
||||||
|
module_id: z
|
||||||
|
.string()
|
||||||
|
.openapi({ example: "6c922c6c-7de3-4ec4-acb0-f068abdc" })
|
||||||
|
.optional(),
|
||||||
|
name: z.string().openapi({ example: "Production" }).optional(),
|
||||||
|
active: z.boolean().openapi({ example: true }).optional(),
|
||||||
|
roles: z
|
||||||
|
.string()
|
||||||
|
.openapi({ example: `["viewer","technician"]` })
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = new OpenAPIHono();
|
||||||
|
|
||||||
|
const ParamsSchema = z.object({
|
||||||
|
id: z
|
||||||
|
.string()
|
||||||
|
.min(3)
|
||||||
|
.openapi({
|
||||||
|
param: {
|
||||||
|
name: "id",
|
||||||
|
in: "path",
|
||||||
|
},
|
||||||
|
example: "1212121",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.openapi(
|
||||||
|
createRoute({
|
||||||
|
tags: ["server"],
|
||||||
|
summary: "Updates submodule",
|
||||||
|
method: "patch",
|
||||||
|
path: "/submodules/{id}",
|
||||||
|
middleware: authMiddleware,
|
||||||
|
request: {
|
||||||
|
params: ParamsSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": { schema: responseSchema },
|
||||||
|
},
|
||||||
|
description: "Response message",
|
||||||
|
},
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({ example: "Internal Server error" }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Internal Server Error",
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({ example: "Unauthenticated" }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Unauthorized",
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: z.object({
|
||||||
|
message: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({ example: "Internal Server error" }),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Internal Server Error",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
|
const { id } = c.req.valid("param");
|
||||||
|
|
||||||
|
// make sure we have a vaid user being accessed thats really logged in
|
||||||
|
const authHeader = c.req.header("Authorization");
|
||||||
|
|
||||||
|
const token = authHeader?.split("Bearer ")[1] || "";
|
||||||
|
let user: User;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await verify(token, process.env.JWT_SECRET!);
|
||||||
|
user = payload.user as User;
|
||||||
|
} catch (error) {
|
||||||
|
return c.json({ message: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now pass all the data over to update the user info
|
||||||
|
try {
|
||||||
|
const data = await c?.req.json();
|
||||||
|
await updateSubModule(data, id ?? "");
|
||||||
|
return c.json({ success: true, message: "Module Updated" }, 200);
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
message: "Please make sure you are not missing your data.",
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
message: `Module has been updated`,
|
||||||
|
data: id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -16,6 +16,7 @@ import { setPerms } from "./utils/testServerPerms.js";
|
|||||||
import serviceControl from "./route/servers/serverContorl.js";
|
import serviceControl from "./route/servers/serverContorl.js";
|
||||||
import { areSubModulesIn } from "./utils/subModuleCheck.js";
|
import { areSubModulesIn } from "./utils/subModuleCheck.js";
|
||||||
import getSubmodules from "./route/modules/getSubModules.js";
|
import getSubmodules from "./route/modules/getSubModules.js";
|
||||||
|
import updateSubModule from "./route/modules/updateSubModules.js";
|
||||||
|
|
||||||
// making sure all modules are in properly
|
// making sure all modules are in properly
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -33,6 +34,7 @@ const routes = [
|
|||||||
updateModule,
|
updateModule,
|
||||||
addModule,
|
addModule,
|
||||||
getSubmodules,
|
getSubmodules,
|
||||||
|
updateSubModule,
|
||||||
// settings
|
// settings
|
||||||
addSetting,
|
addSetting,
|
||||||
getSettings,
|
getSettings,
|
||||||
|
|||||||
@@ -207,6 +207,14 @@ const newSettings = [
|
|||||||
serviceBelowsTo: "ocp",
|
serviceBelowsTo: "ocp",
|
||||||
roleToChange: "admin",
|
roleToChange: "admin",
|
||||||
},
|
},
|
||||||
|
// temp settings can be deleted at a later date once that code is removed
|
||||||
|
{
|
||||||
|
name: "siloAdjMigrations",
|
||||||
|
value: `0`,
|
||||||
|
description: "Migrates the old silo adjustments to lst v2.",
|
||||||
|
serviceBelowsTo: "admin",
|
||||||
|
roleToChange: "admin",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
export const areSettingsIn = async () => {
|
export const areSettingsIn = async () => {
|
||||||
// get the roles
|
// get the roles
|
||||||
|
|||||||
@@ -101,7 +101,14 @@ const newSubModules = [
|
|||||||
link: "/modules",
|
link: "/modules",
|
||||||
icon: "Settings",
|
icon: "Settings",
|
||||||
newWindow: false,
|
newWindow: false,
|
||||||
isActive: false,
|
isActive: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sub Modules",
|
||||||
|
link: "/subModules",
|
||||||
|
icon: "Settings",
|
||||||
|
newWindow: false,
|
||||||
|
isActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Swagger",
|
name: "Swagger",
|
||||||
|
|||||||
Reference in New Issue
Block a user