Compare commits
68 Commits
join
...
adae0d3db1
| Author | SHA1 | Date | |
|---|---|---|---|
| adae0d3db1 | |||
| 715a929e13 | |||
| 5b0f9b06d8 | |||
| 663e36bc4b | |||
| ddefbbbbff | |||
| 2aaa44cf14 | |||
| fbf00a55da | |||
|
|
03955743ca | ||
|
|
cdd7c6fa2b | ||
|
|
c51dcfdad4 | ||
|
|
e68fe87e9e | ||
|
|
fca77c6bd8 | ||
| aa89a10aa8 | |||
| 21af3e3310 | |||
| 08faa9f6b0 | |||
| b101c63f8d | |||
| 41820ff2b3 | |||
| 9c045f32ea | |||
| 6dd8dcd06e | |||
| f79629e97e | |||
| b52bb57fbc | |||
| 401f8f13a2 | |||
| 7b0d4e5d30 | |||
| 5c71d000f6 | |||
| 621cfc931a | |||
| 928ecb4c76 | |||
| 0ac649345d | |||
| e0456b2dba | |||
| 14ec81d98d | |||
| 0e5fab6a84 | |||
| 89e83d806e | |||
| df7f93c794 | |||
| de594acbf6 | |||
| 84d2388eb8 | |||
| 25f92e3686 | |||
| c2d07b3edf | |||
| 169b2b0e3e | |||
| 13f88efb35 | |||
| 25fc7e2d26 | |||
| 26241fd36c | |||
| 37e76d82c0 | |||
| 73bf785d13 | |||
| cc7dcccd1b | |||
| a475db688b | |||
| f93b486bbb | |||
| 06feeae9a5 | |||
| b102643675 | |||
|
|
b2f8dc3714 | ||
| 578ad51726 | |||
|
|
8a3eaa2193 | ||
|
|
cae9ed7282 | ||
|
|
2003364bff | ||
|
|
5dc83dbd35 | ||
|
|
9c96031574 | ||
|
|
841fca55d1 | ||
|
|
e009e27d47 | ||
|
|
b52da1c4bd | ||
|
|
3edcc52e74 | ||
|
|
17bd04e389 | ||
|
|
69377a3491 | ||
|
|
3e2245da29 | ||
|
|
65b24ab031 | ||
| 78b1c0ee2d | |||
| 7cc49655b4 | |||
| 6a9ce54311 | |||
| bf0083e678 | |||
|
|
fb5a859ebc | ||
|
|
e0fdb88c32 |
77
bun.lock
77
bun.lock
@@ -2,8 +2,9 @@
|
|||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "jenna-mcp",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elysiajs/bearer": "^1.4.1",
|
||||||
"@elysiajs/cors": "^1.4.0",
|
"@elysiajs/cors": "^1.4.0",
|
||||||
"@elysiajs/eden": "^1.4.4",
|
"@elysiajs/eden": "^1.4.4",
|
||||||
"@elysiajs/jwt": "^1.4.0",
|
"@elysiajs/jwt": "^1.4.0",
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"elysia": "^1.4.13",
|
"elysia": "^1.4.15",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -48,6 +49,8 @@
|
|||||||
|
|
||||||
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
"@borewit/text-codec": ["@borewit/text-codec@0.1.1", "", {}, "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA=="],
|
||||||
|
|
||||||
|
"@elysiajs/bearer": ["@elysiajs/bearer@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-vLLSMEVsLKp/8p/eoAbXZdXKRs1jEQO4OkrfcKM2x8FkiK2aKNcFgLID45bH+6rYbCf8Ihg0NKw59zxMLl43OQ=="],
|
||||||
|
|
||||||
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
|
"@elysiajs/cors": ["@elysiajs/cors@1.4.0", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-pb0SCzBfFbFSYA/U40HHO7R+YrcXBJXOWgL20eSViK33ol1e20ru2/KUaZYo5IMUn63yaTJI/bQERuQ+77ND8g=="],
|
||||||
|
|
||||||
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
|
"@elysiajs/eden": ["@elysiajs/eden@1.4.4", "", { "peerDependencies": { "elysia": ">= 1.4.0-exp.0" } }, "sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ=="],
|
||||||
@@ -66,35 +69,35 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
"@mantine/core": ["@mantine/core@8.3.5", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.5", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-PdVNLMgOS2vFhOujRi6/VC9ic8w3UDyKX7ftwDeJ7yQT8CiepUxfbWWYpVpnq23bdWh/7fIT2Pn1EY8r8GOk7g=="],
|
"@mantine/core": ["@mantine/core@8.3.6", "", { "dependencies": { "@floating-ui/react": "^0.27.16", "clsx": "^2.1.1", "react-number-format": "^5.4.4", "react-remove-scroll": "^2.7.1", "react-textarea-autosize": "8.5.9", "type-fest": "^4.41.0" }, "peerDependencies": { "@mantine/hooks": "8.3.6", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A=="],
|
||||||
|
|
||||||
"@mantine/dates": ["@mantine/dates@8.3.5", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.5", "@mantine/hooks": "8.3.5", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-LkIdC4eWPNQFv1BU1c52U3Z3RuA3yU1asvTgMEIQ/MdJsGK8GePwpgMH/jKQ8ba/AW9NfksdvtOJ6uIqPwjCkg=="],
|
"@mantine/dates": ["@mantine/dates@8.3.6", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "@mantine/core": "8.3.6", "@mantine/hooks": "8.3.6", "dayjs": ">=1.0.0", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-lSi1zvyL86SKeePH0J3vOjAR7ZIVNOrZm6ja7jAH6IBdcpQOKH8TXbrcAi5okEStvmvkne7pVaGu0VkdE8KnAw=="],
|
||||||
|
|
||||||
"@mantine/form": ["@mantine/form@8.3.5", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-i9UFiHtO1dlrJXZkquyt+71YcNNxPPSkIcJCRp7k0Tif7bPqWK2xijPDEXzqvA53YvMvEMoqaQCEQLVmH7Esdg=="],
|
"@mantine/form": ["@mantine/form@8.3.6", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "klona": "^2.0.6" }, "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-hIu0KdP1e1Vu7KUQ+cIDpor9UE9vO7iXR3dOMu6GPF3MlHFbwnCjakW9nxSCjP1PRTMwA3m43s4GIt22XfK9tg=="],
|
||||||
|
|
||||||
"@mantine/hooks": ["@mantine/hooks@8.3.5", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-0Wf08eWLKi3WkKlxnV1W5vfuN6wcvAV2VbhQlOy0R9nrWorGTtonQF6qqBE3PnJFYF1/ZE+HkYZQ/Dr7DmYSMQ=="],
|
"@mantine/hooks": ["@mantine/hooks@8.3.6", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw=="],
|
||||||
|
|
||||||
"@mantine/notifications": ["@mantine/notifications@8.3.5", "", { "dependencies": { "@mantine/store": "8.3.5", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.5", "@mantine/hooks": "8.3.5", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-8TvzrPxfdtOLGTalv7Ei1hy2F6KbR3P7/V73yw3AOKhrf1ydS89sqV2ShbsucHGJk9Pto0wjdTPd8Q7pm5MAYw=="],
|
"@mantine/notifications": ["@mantine/notifications@8.3.6", "", { "dependencies": { "@mantine/store": "8.3.6", "react-transition-group": "4.4.5" }, "peerDependencies": { "@mantine/core": "8.3.6", "@mantine/hooks": "8.3.6", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "sha512-d3A96lyrFOVXtrwASEXALfzooKnnA60T2LclMXFF/4k27Ay5Hwza4D+ylqgxf0RkPfF9J6LhBXk72OjL5RH5Kg=="],
|
||||||
|
|
||||||
"@mantine/store": ["@mantine/store@8.3.5", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-qN4fFsDMy86IV9oh1gZlDTv41RAsO0grjx90FGyT5QCv7NTgcavwxB74GBkhp45W8xn+Ms/awKy+6NxnmLmW1w=="],
|
"@mantine/store": ["@mantine/store@8.3.6", "", { "peerDependencies": { "react": "^18.x || ^19.x" } }, "sha512-fo86wF6nL8RPukY8cseAFQKk+bRVv3Ga/WmHJMYRsCbNleZOEZMXXUf/OVhmr1D3t+xzCzAlJe/sQ8MIS+c+pA=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.20.2", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.21.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-YFBsXJMFCyI1zP98u7gezMFKX4lgu/XpoZJk7ufI6UlFKXLj2hAMUuRlQX/nrmIPOmhRrG6tw2OQ2ZA/ZlXYpQ=="],
|
||||||
|
|
||||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Kd2+Ai1ttskhbJR+DNU4Y4YEDyP/cd50nWt2rAe2aE78dMOalaVGps3s8UnJkXpDL9ZqkgOHVDE5Doj2lxatw=="],
|
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OLx4XyUv5SO7k8y5FzJIoTKan+iKK53T1Ws8fBIl4zblUIWI66ZIqSVG2A2rxOBA7XfINqCz8UipGzOW9yzKcg=="],
|
||||||
|
|
||||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-/R9VbnuTp7bLIBh6ucDHjx0po0wLQODLqzy+L/Frn5z4ifMVdE63DB+LHO8QAj+WEQleQq3u/MMms7RFPulCLA=="],
|
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-srndNPiliA0rchYKqYfOdqA9kqyVQ6YChK3XJe9Lxo/YG8tTJ5K65g2A5SHTT2s1Nm5DnQa5AKZH7w+7KI/m8A=="],
|
||||||
|
|
||||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fA90bIQ1b44eNg0uULlTonqsADVIBnMz169mav6IhfZL9V6DpBCUWrV+8tEQCxbDvYC0WY1guBpPo2QWUnC/Dw=="],
|
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9+DnHDbygprpGV586BolwWES+o2raOcSJv404nOFPQjWZ09efG24nuXrg/fpyoMQb4YoW2W1fvlnyMVU+ADcw=="],
|
||||||
|
|
||||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-p7Bv9FTQ1lf4Z7OiIFwiy+cY2fxN6IJc0+2gJ4z2fpaQ0J2rQQcKdJ5RLQTxf+tAu7hyqjc6bf61EAGa9lb/GA=="],
|
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1tIMpQhKlItm7uKzs3lluG7KorZR5ItoNKd1iFYF/IPmZ+i0/iuZ7MVWXRjBcgQMhMYSdfZpSVEdFKcFz2HDxA=="],
|
||||||
|
|
||||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wIQOpTONiJ9pYPnLEq7UFuml8mpmSFTfUveNbT2rw9iXfj2nLMf7NIqGnUYQdvnnOi+maag9uei/WImXIm9LQQ=="],
|
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-xVkmk/zkIulc5o0OUWY04DyBfKotnq9+60O9I5c0DpdKAELVLhZkLmct0apx3jAX6Z/3yYPzhc6Lw1Ia3jU3VQ=="],
|
||||||
|
|
||||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HxcDX/SpTH7yC/Rn2MinjSHZmNpn79yJkBid792DWjP9bo0CnlNXOXMPXsbm+WqptvqQ9yUPCxf7KascUvxLyQ=="],
|
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IeO10dZosJV58YzN0gckhRYac+FM9s5VCKUx2ghgbKR91z/bpSRcRl8Sy5cWTkcVwu3ZTikhK8aXC6j7XIqKNw=="],
|
||||||
|
|
||||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-P1KtZ/xL+TcNTTmOtEsVrpqAdmpu2UCRAILjoqQyrYvI/CW6SdvoJfMBTntKOZaB52Peq2BHTgsYovON8q4FfQ=="],
|
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-mpdiXZm2oNuSQAbTEPRDuSeR6v1DCD7Cl/xouR2ggHZu3AKZ4XYmm29hyrzIxrYVoQ/5j+182TGdOpGYn9xQJg=="],
|
||||||
|
|
||||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-JMbMm7i1esFl12fRdOQwoeEeufWXxihOme8pZpI6jrwWK1kCIANMb5agI5Lkjf5vToQOP3DLXYc29aDm16fw6g=="],
|
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-opoIACOkcFloWQO6dubBLbcWwW52ML8+3deFdr0WE0PeM9UXdLB0jRMuLsEnplmBoy9TRvmxDJ+Pw8xc2PsOfQ=="],
|
||||||
|
|
||||||
"@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="],
|
"@prisma/client": ["@prisma/client@6.18.0", "", { "peerDependencies": { "prisma": "*", "typescript": ">=5.1.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-jnL2I9gDnPnw4A+4h5SuNn8Gc+1mL1Z79U/3I9eE2gbxJG1oSA+62ByPW4xkeDgwE0fqMzzpAZ7IHxYnLZ4iQA=="],
|
||||||
|
|
||||||
@@ -134,7 +137,7 @@
|
|||||||
|
|
||||||
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.7.0", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw=="],
|
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
@@ -148,7 +151,9 @@
|
|||||||
|
|
||||||
"add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="],
|
"add": ["add@2.0.6", "", {}, "sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q=="],
|
||||||
|
|
||||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"ansi-escapes": ["ansi-escapes@1.4.0", "", {}, "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw=="],
|
"ansi-escapes": ["ansi-escapes@1.4.0", "", {}, "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw=="],
|
||||||
|
|
||||||
@@ -240,7 +245,7 @@
|
|||||||
|
|
||||||
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
|
"dashdash": ["dashdash@1.14.1", "", { "dependencies": { "assert-plus": "^1.0.0" } }, "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g=="],
|
||||||
|
|
||||||
"dayjs": ["dayjs@1.11.18", "", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="],
|
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
@@ -274,7 +279,7 @@
|
|||||||
|
|
||||||
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
|
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
|
||||||
|
|
||||||
"elysia": ["elysia@1.4.13", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-6QaWQEm7QN1UCo1TPpEjaRJPHUmnM7R29y6LY224frDGk5PrpAnWmdHkoZxkcv+JRWp1j2ROr2IHbxHbG/jRjw=="],
|
"elysia": ["elysia@1.4.15", "", { "dependencies": { "cookie": "^1.0.2", "exact-mirror": "0.2.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-RaDqqZdLuC4UJetfVRQ4Z5aVpGgEtQ+pZnsbI4ZzEaf3l/MzuHcqSVoL/Fue3d6qE4RV9HMB2rAZaHyPIxkyzg=="],
|
||||||
|
|
||||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||||
|
|
||||||
@@ -318,6 +323,8 @@
|
|||||||
|
|
||||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||||
@@ -410,7 +417,7 @@
|
|||||||
|
|
||||||
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
"json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
"json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="],
|
||||||
|
|
||||||
@@ -478,7 +485,7 @@
|
|||||||
|
|
||||||
"os-homedir": ["os-homedir@1.0.2", "", {}, "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ=="],
|
"os-homedir": ["os-homedir@1.0.2", "", {}, "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ=="],
|
||||||
|
|
||||||
"oxlint": ["oxlint@1.24.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.24.0", "@oxlint/darwin-x64": "1.24.0", "@oxlint/linux-arm64-gnu": "1.24.0", "@oxlint/linux-arm64-musl": "1.24.0", "@oxlint/linux-x64-gnu": "1.24.0", "@oxlint/linux-x64-musl": "1.24.0", "@oxlint/win32-arm64": "1.24.0", "@oxlint/win32-x64": "1.24.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.2.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-swXlnHT7ywcCApkctIbgOSjDYHwMa12yMU0iXevfDuHlYkRUcbQrUv6nhM5v6B0+Be3zTBMNDGPAMQv0oznzRQ=="],
|
"oxlint": ["oxlint@1.25.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.25.0", "@oxlint/darwin-x64": "1.25.0", "@oxlint/linux-arm64-gnu": "1.25.0", "@oxlint/linux-arm64-musl": "1.25.0", "@oxlint/linux-x64-gnu": "1.25.0", "@oxlint/linux-x64-musl": "1.25.0", "@oxlint/win32-arm64": "1.25.0", "@oxlint/win32-x64": "1.25.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.4.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-O6iJ9xeuy9eQCi8/EghvsNO6lzSaUPs0FR1uLy51Exp3RkVpjvJKyPPhd9qv65KLnfG/BNd2HE/rH0NbEfVVzA=="],
|
||||||
|
|
||||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
@@ -550,9 +557,9 @@
|
|||||||
|
|
||||||
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.9.4", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA=="],
|
"react-router": ["react-router@7.9.5", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-JmxqrnBZ6E9hWmf02jzNn9Jm3UqyeimyiwzD69NjxGySG6lIz/1LVPsoTCwN7NBX2XjCEa1LIX5EMz1j2b6u6A=="],
|
||||||
|
|
||||||
"react-router-dom": ["react-router-dom@7.9.4", "", { "dependencies": { "react-router": "7.9.4" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA=="],
|
"react-router-dom": ["react-router-dom@7.9.5", "", { "dependencies": { "react-router": "7.9.5" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-mkEmq/K8tKN63Ae2M7Xgz3c9l9YNbY+NHH6NNeUmLA3kDkhKXRsNb/ZpxaEunvGo2/3YXdk5EJU3Hxp3ocaBPw=="],
|
||||||
|
|
||||||
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
|
||||||
|
|
||||||
@@ -570,6 +577,8 @@
|
|||||||
|
|
||||||
"request-promise": ["request-promise@3.0.0", "", { "dependencies": { "bluebird": "^3.3", "lodash": "^4.6.1", "request": "^2.34" } }, "sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw=="],
|
"request-promise": ["request-promise@3.0.0", "", { "dependencies": { "bluebird": "^3.3", "lodash": "^4.6.1", "request": "^2.34" } }, "sha512-wVGUX+BoKxYsavTA72i6qHcyLbjzM4LR4y/AmDCqlbuMAursZdDWO7PmgbGAUvD2SeEJ5iB99VSq/U51i/DNbw=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"restore-cursor": ["restore-cursor@1.0.1", "", { "dependencies": { "exit-hook": "^1.0.0", "onetime": "^1.0.0" } }, "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw=="],
|
"restore-cursor": ["restore-cursor@1.0.1", "", { "dependencies": { "exit-hook": "^1.0.0", "onetime": "^1.0.0" } }, "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw=="],
|
||||||
|
|
||||||
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
|
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
|
||||||
@@ -590,7 +599,7 @@
|
|||||||
|
|
||||||
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
|
||||||
|
|
||||||
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
@@ -610,7 +619,7 @@
|
|||||||
|
|
||||||
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
||||||
|
|
||||||
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="],
|
"string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="],
|
||||||
|
|
||||||
@@ -624,7 +633,7 @@
|
|||||||
|
|
||||||
"swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="],
|
"swr": ["swr@2.3.6", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw=="],
|
||||||
|
|
||||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
"tabbable": ["tabbable@6.3.0", "", {}, "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ=="],
|
||||||
|
|
||||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||||
|
|
||||||
@@ -632,7 +641,7 @@
|
|||||||
|
|
||||||
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
|
||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
@@ -654,7 +663,7 @@
|
|||||||
|
|
||||||
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
|
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
@@ -680,7 +689,7 @@
|
|||||||
|
|
||||||
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
"uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
|
||||||
|
|
||||||
"valtio": ["valtio@2.1.8", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-fjTPbJyKEmfVBZUOh3V0OtMHoFUGr4+4XpejjxhNJE/IS2l8rDbyJuzi3w/fZWBDyk7BJOpG+lmvTK5iiVhXuQ=="],
|
"valtio": ["valtio@2.2.0", "", { "dependencies": { "proxy-compare": "^3.0.1" }, "peerDependencies": { "@types/react": ">=18.0.0", "react": ">=18.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-l/zzQahUIm+dfUUP9fIecNVEWJLea9shMC1Bb1aK+v4XNOEzoq796Qax+yzMemmqpltuxfH7kPJy62FVGJDEtw=="],
|
||||||
|
|
||||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
@@ -708,6 +717,10 @@
|
|||||||
|
|
||||||
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
|
|
||||||
|
"har-validator/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
"inquirer/lodash": ["lodash@3.10.1", "", {}, "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ=="],
|
"inquirer/lodash": ["lodash@3.10.1", "", {}, "sha512-9mDDwqVIma6OZX79ZlDACZl8sBm0TEnkf99zV3iMA4GzkIT/9hiqP5mY0HoT1iNLCrKc/R1HByV+yJfRWVJryQ=="],
|
||||||
|
|
||||||
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||||
@@ -726,6 +739,8 @@
|
|||||||
|
|
||||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
|
"har-validator/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
"request/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"request/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"lint": "bunx oxlint src"
|
"lint": "bunx oxlint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elysiajs/bearer": "^1.4.1",
|
||||||
"@elysiajs/cors": "^1.4.0",
|
"@elysiajs/cors": "^1.4.0",
|
||||||
"@elysiajs/eden": "^1.4.4",
|
"@elysiajs/eden": "^1.4.4",
|
||||||
"@elysiajs/jwt": "^1.4.0",
|
"@elysiajs/jwt": "^1.4.0",
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
"@types/lodash": "^4.17.20",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/uuid": "^11.0.0",
|
"@types/uuid": "^11.0.0",
|
||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"elysia": "^1.4.13",
|
"elysia": "^1.4.15",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ model User {
|
|||||||
email String? @unique
|
email String? @unique
|
||||||
password String?
|
password String?
|
||||||
phone String? @unique
|
phone String? @unique
|
||||||
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
HistoryPengaduan HistoryPengaduan[]
|
HistoryPengaduan HistoryPengaduan[]
|
||||||
|
HistoryPelayanan HistoryPelayanan[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model ApiKey {
|
model ApiKey {
|
||||||
@@ -82,7 +84,7 @@ model HistoryPengaduan {
|
|||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id])
|
Pengaduan Pengaduan @relation(fields: [idPengaduan], references: [id])
|
||||||
idPengaduan String
|
idPengaduan String
|
||||||
User User? @relation(fields: [idUser], references: [id])
|
User User? @relation(fields: [idUser], references: [id])
|
||||||
idUser String?
|
idUser String?
|
||||||
deskripsi String?
|
deskripsi String?
|
||||||
status StatusPengaduan @default(antrian)
|
status StatusPengaduan @default(antrian)
|
||||||
@@ -91,12 +93,110 @@ model HistoryPengaduan {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Warga {
|
model Warga {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String?
|
name String?
|
||||||
phone String? @unique
|
phone String? @unique
|
||||||
createdAt DateTime @default(now())
|
isActive Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
Pengaduan Pengaduan[]
|
updatedAt DateTime @updatedAt
|
||||||
|
Pengaduan Pengaduan[]
|
||||||
|
PelayananAjuan PelayananAjuan[]
|
||||||
|
SuratPelayanan SuratPelayanan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model CategoryPelayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
syaratDokumen Json[]
|
||||||
|
dataText String[]
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
PelayananAjuan PelayananAjuan[]
|
||||||
|
SyaratDokumenPelayanan SyaratDokumenPelayanan[]
|
||||||
|
DataTextPelayanan DataTextPelayanan[]
|
||||||
|
SuratPelayanan SuratPelayanan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model PelayananAjuan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
Warga Warga @relation(fields: [idWarga], references: [id])
|
||||||
|
idWarga String
|
||||||
|
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
|
||||||
|
idCategory String
|
||||||
|
noPengajuan String
|
||||||
|
status StatusPengaduan @default(antrian)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
HistoryPelayanan HistoryPelayanan[]
|
||||||
|
SyaratDokumenPelayanan SyaratDokumenPelayanan[]
|
||||||
|
DataTextPelayanan DataTextPelayanan[]
|
||||||
|
SuratPelayanan SuratPelayanan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model HistoryPelayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
|
||||||
|
idPengajuanLayanan String
|
||||||
|
User User? @relation(fields: [idUser], references: [id])
|
||||||
|
idUser String?
|
||||||
|
deskripsi String?
|
||||||
|
keteranganAlasan String?
|
||||||
|
status StatusPengaduan @default(antrian)
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model SyaratDokumenPelayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
|
||||||
|
idPengajuanLayanan String
|
||||||
|
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
|
||||||
|
idCategory String
|
||||||
|
jenis String
|
||||||
|
value String
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model DataTextPelayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
|
||||||
|
idPengajuanLayanan String
|
||||||
|
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
|
||||||
|
idCategory String
|
||||||
|
jenis String
|
||||||
|
value String
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model SuratPelayanan {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
PelayananAjuan PelayananAjuan @relation(fields: [idPengajuanLayanan], references: [id])
|
||||||
|
idPengajuanLayanan String
|
||||||
|
CategoryPelayanan CategoryPelayanan @relation(fields: [idCategory], references: [id])
|
||||||
|
idCategory String
|
||||||
|
Warga Warga @relation(fields: [idWarga], references: [id])
|
||||||
|
idWarga String
|
||||||
|
noSurat String
|
||||||
|
dateExpired DateTime @db.Date
|
||||||
|
status Int
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model Configuration {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
value String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
enum StatusPengaduan {
|
enum StatusPengaduan {
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
|
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
|
||||||
|
import { confDesa } from "@/lib/configurationDesa";
|
||||||
import { prisma } from "@/server/lib/prisma";
|
import { prisma } from "@/server/lib/prisma";
|
||||||
|
|
||||||
|
const category = [
|
||||||
|
{
|
||||||
|
id: "lainnya",
|
||||||
|
name: "Lainnya"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "kebersihan",
|
||||||
|
name: "Kebersihan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "keamanan",
|
||||||
|
name: "Keamanan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pelayanan",
|
||||||
|
name: "Pelayanan"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "infrastruktur",
|
||||||
|
name: "Infrastruktur"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const role = [
|
const role = [
|
||||||
{
|
{
|
||||||
id: "developer",
|
id: "developer",
|
||||||
@@ -17,6 +42,7 @@ const role = [
|
|||||||
|
|
||||||
const user = [
|
const user = [
|
||||||
{
|
{
|
||||||
|
id: "bip",
|
||||||
name: "Bip",
|
name: "Bip",
|
||||||
email: "bip@bip.com",
|
email: "bip@bip.com",
|
||||||
password: "bip",
|
password: "bip",
|
||||||
@@ -25,6 +51,16 @@ const user = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
for (const r of role) {
|
||||||
|
await prisma.role.upsert({
|
||||||
|
where: { id: r.id },
|
||||||
|
create: r,
|
||||||
|
update: r
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Role ${r.name} seeded successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
for (const u of user) {
|
for (const u of user) {
|
||||||
await prisma.user.upsert({
|
await prisma.user.upsert({
|
||||||
where: { email: u.email },
|
where: { email: u.email },
|
||||||
@@ -35,17 +71,37 @@ const user = [
|
|||||||
console.log(`✅ User ${u.email} seeded successfully`)
|
console.log(`✅ User ${u.email} seeded successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const r of role) {
|
for (const c of category) {
|
||||||
console.log(`Seeding role ${r.name}`)
|
await prisma.categoryPengaduan.upsert({
|
||||||
await prisma.role.upsert({
|
where: { id: c.id },
|
||||||
where: { id: r.id },
|
create: c,
|
||||||
create: r,
|
update: c
|
||||||
update: r
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`✅ Role ${r.name} seeded successfully`)
|
console.log(`✅ Category ${c.name} seeded successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const cp of categoryPelayananSurat) {
|
||||||
|
await prisma.categoryPelayanan.upsert({
|
||||||
|
where: { id: cp.id },
|
||||||
|
create: cp,
|
||||||
|
update: cp
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Category Pelayanan ${cp.name} seeded successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of confDesa) {
|
||||||
|
await prisma.configuration.upsert({
|
||||||
|
where: { id: c.id },
|
||||||
|
create: c,
|
||||||
|
update: c
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`✅ Configuration ${c.name} seeded successfully`)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@@ -17,8 +17,15 @@ import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_ketera
|
|||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||||
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||||
|
import ListPelayananPage from "./pages/scr/dashboard/pelayanan-surat/list_pelayanan_page";
|
||||||
|
import DetailPelayananPage from "./pages/scr/dashboard/pelayanan-surat/detail_pelayanan_page";
|
||||||
|
import DetailWargaPage from "./pages/scr/dashboard/warga/detail_warga_page";
|
||||||
|
import ListWargaPage from "./pages/scr/dashboard/warga/list_warga_page";
|
||||||
|
import ListPage from "./pages/scr/dashboard/pengaduan/list_page";
|
||||||
|
import DetailPage from "./pages/scr/dashboard/pengaduan/detail_page";
|
||||||
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
import ApikeyPage from "./pages/scr/dashboard/apikey/apikey_page";
|
||||||
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
import DashboardLayout from "./pages/scr/dashboard/dashboard_layout";
|
||||||
|
import DetailSettingPage from "./pages/scr/dashboard/setting/detail_setting_page";
|
||||||
import ScrLayout from "./pages/scr/scr_layout";
|
import ScrLayout from "./pages/scr/scr_layout";
|
||||||
import DirPage from "./pages/dir/dir_page";
|
import DirPage from "./pages/dir/dir_page";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
@@ -92,10 +99,38 @@ export default function AppRoutes() {
|
|||||||
path="/scr/dashboard/dashboard-home"
|
path="/scr/dashboard/dashboard-home"
|
||||||
element={<DashboardHome />}
|
element={<DashboardHome />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/pelayanan-surat/list-pelayanan"
|
||||||
|
element={<ListPelayananPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/pelayanan-surat/detail-pelayanan"
|
||||||
|
element={<DetailPelayananPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/warga/detail-warga"
|
||||||
|
element={<DetailWargaPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/warga/list-warga"
|
||||||
|
element={<ListWargaPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/pengaduan/list"
|
||||||
|
element={<ListPage />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/pengaduan/detail"
|
||||||
|
element={<DetailPage />}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/scr/dashboard/apikey/apikey"
|
path="/scr/dashboard/apikey/apikey"
|
||||||
element={<ApikeyPage />}
|
element={<ApikeyPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/scr/dashboard/setting/detail-setting"
|
||||||
|
element={<DetailSettingPage />}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/dir/dir" element={<DirPage />} />
|
<Route path="/dir/dir" element={<DirPage />} />
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ const clientRoutes = {
|
|||||||
"/scr/dashboard": "/scr/dashboard",
|
"/scr/dashboard": "/scr/dashboard",
|
||||||
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
|
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
|
||||||
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
|
"/scr/dashboard/dashboard-home": "/scr/dashboard/dashboard-home",
|
||||||
|
"/scr/dashboard/pelayanan-surat/list-pelayanan": "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||||
|
"/scr/dashboard/pelayanan-surat/detail-pelayanan": "/scr/dashboard/pelayanan-surat/detail-pelayanan",
|
||||||
|
"/scr/dashboard/warga/detail-warga": "/scr/dashboard/warga/detail-warga",
|
||||||
|
"/scr/dashboard/warga/list-warga": "/scr/dashboard/warga/list-warga",
|
||||||
|
"/scr/dashboard/pengaduan/list": "/scr/dashboard/pengaduan/list",
|
||||||
|
"/scr/dashboard/pengaduan/detail": "/scr/dashboard/pengaduan/detail",
|
||||||
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
|
"/scr/dashboard/apikey/apikey": "/scr/dashboard/apikey/apikey",
|
||||||
|
"/scr/dashboard/setting/detail-setting": "/scr/dashboard/setting/detail-setting",
|
||||||
"/dir/dir": "/dir/dir",
|
"/dir/dir": "/dir/dir",
|
||||||
"/*": "/*"
|
"/*": "/*"
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import Swagger from "@elysiajs/swagger";
|
import Swagger from "@elysiajs/swagger";
|
||||||
import Elysia from "elysia";
|
import Elysia from "elysia";
|
||||||
import html from "./index.html";
|
import html from "./index.html";
|
||||||
import apiAuth from "./server/middlewares/apiAuth";
|
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
|
||||||
|
import { apiAuth } from "./server/middlewares/apiAuth";
|
||||||
|
import AduanRoute from "./server/routes/aduan_route";
|
||||||
import ApiKeyRoute from "./server/routes/apikey_route";
|
import ApiKeyRoute from "./server/routes/apikey_route";
|
||||||
import Auth from "./server/routes/auth_route";
|
import Auth from "./server/routes/auth_route";
|
||||||
import CredentialRoute from "./server/routes/credential_route";
|
import CredentialRoute from "./server/routes/credential_route";
|
||||||
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
||||||
import { convertOpenApiToMcp } from "./server/lib/mcp-converter";
|
|
||||||
import UserRoute from "./server/routes/user_route";
|
|
||||||
import LayananRoute from "./server/routes/layanan_route";
|
import LayananRoute from "./server/routes/layanan_route";
|
||||||
import AduanRoute from "./server/routes/aduan_route";
|
|
||||||
|
|
||||||
import { cors } from "@elysiajs/cors";
|
|
||||||
import { MCPRoute } from "./server/routes/mcp_route";
|
import { MCPRoute } from "./server/routes/mcp_route";
|
||||||
|
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||||
|
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||||
|
import TestRoute from "./server/routes/test";
|
||||||
|
import UserRoute from "./server/routes/user_route";
|
||||||
|
import cors from "@elysiajs/cors";
|
||||||
|
|
||||||
const Docs = new Elysia({
|
const Docs = new Elysia({
|
||||||
tags: ["docs"],
|
tags: ["docs"],
|
||||||
@@ -26,6 +28,9 @@ const Api = new Elysia({
|
|||||||
prefix: "/api",
|
prefix: "/api",
|
||||||
tags: ["api"],
|
tags: ["api"],
|
||||||
})
|
})
|
||||||
|
.use(PengaduanRoute)
|
||||||
|
.use(PelayananRoute)
|
||||||
|
.use(TestRoute)
|
||||||
.use(apiAuth)
|
.use(apiAuth)
|
||||||
.use(ApiKeyRoute)
|
.use(ApiKeyRoute)
|
||||||
.use(DarmasabaRoute)
|
.use(DarmasabaRoute)
|
||||||
@@ -38,6 +43,13 @@ const app = new Elysia()
|
|||||||
.use(Api)
|
.use(Api)
|
||||||
.use(Docs)
|
.use(Docs)
|
||||||
.use(Auth)
|
.use(Auth)
|
||||||
|
.use(
|
||||||
|
cors({
|
||||||
|
origin: "*",
|
||||||
|
methods: ["GET", "POST", "OPTIONS"],
|
||||||
|
allowedHeaders: ["Content-Type"],
|
||||||
|
}),
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/.well-known/mcp.json",
|
"/.well-known/mcp.json",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
109
src/lib/categoryPelayananSurat.ts
Normal file
109
src/lib/categoryPelayananSurat.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
export const categoryPelayananSurat = [
|
||||||
|
{
|
||||||
|
id: "skbedabiodata",
|
||||||
|
name: "Surat Keterangan Beda Biodata Diri",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||||
|
{ name: "dokumen yang beda", desc: "Fotokopi dokumen bersangkutan yang terdapat perbedaan biodata diri, misalnya: Sertifikat Tanah, Ijazah, Polis Asuransi, dan lainnya." }
|
||||||
|
],
|
||||||
|
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "dokumen", "tertulis pada dokumen a", "tertulis pada dokumen b"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skbelumkawin",
|
||||||
|
name: "Surat Keterangan Belum Kawin",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||||
|
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
|
||||||
|
],
|
||||||
|
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "status perkawinan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skdomisiliorganisasi",
|
||||||
|
name: "Surat Keterangan Domisili Organisasi",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
|
||||||
|
{name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi"}
|
||||||
|
],
|
||||||
|
dataText: ["nama organisasi", "alamat organisasi", "nama pemohon", "jabatan pemohon", "kontak", "penanggung jawab", "tanggal berdiri"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skkelahiran",
|
||||||
|
name: "Surat Keterangan Kelahiran",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "surat lahir", desc: "Fotokopi Surat Keterangan Lahir dari Bidan/Dokter (jika ada)" }
|
||||||
|
],
|
||||||
|
dataText: ["nama ayah", "nama ibu", "nama anak", "tanggal lahir", "tempat lahir", "jenis kelamin", "nama pelapor"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skkelakuanbaik",
|
||||||
|
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
|
||||||
|
],
|
||||||
|
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "keperluan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skkematian",
|
||||||
|
name: "Surat Keterangan Kematian",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||||
|
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
|
||||||
|
],
|
||||||
|
dataText: ["nama almarhum", "nik", "tempat tanggal lahir", "alamat", "tanggal kematian", "waktu kematian", "penyebab kematian"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skpenghasilan",
|
||||||
|
name: "Surat Keterangan Penghasilan",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
|
||||||
|
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
|
||||||
|
],
|
||||||
|
dataText: ["nama", "nik", "alamat", "pekerjaan", "jenis usaha", "penghasilan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sktempatusaha",
|
||||||
|
name: "Surat Keterangan Tempat Usaha",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||||
|
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
|
||||||
|
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
|
||||||
|
],
|
||||||
|
dataText: ["nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sktidakmampu",
|
||||||
|
name: "Surat Keterangan Tidak Mampu",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
|
||||||
|
],
|
||||||
|
dataText: ["nik", "nama", "tempat tanggal lahir", "alamat", "alasan permohonan"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skusaha",
|
||||||
|
name: "Surat Keterangan Usaha",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||||
|
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }
|
||||||
|
],
|
||||||
|
dataText: ["jenis usaha", "alamat usaha"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "skyatimpiatu",
|
||||||
|
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
|
||||||
|
syaratDokumen: [
|
||||||
|
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||||
|
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
|
||||||
|
],
|
||||||
|
dataText: ["nama anak", "nama ayah", "status ayah", "nama ibu", "status ibu"]
|
||||||
|
}
|
||||||
|
];
|
||||||
57
src/lib/configurationDesa.ts
Normal file
57
src/lib/configurationDesa.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export const confDesa = [
|
||||||
|
{
|
||||||
|
id: "desaNama",
|
||||||
|
name: "Nama Desa",
|
||||||
|
value: "Darmasaba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaKabupaten",
|
||||||
|
name: "Kabupaten",
|
||||||
|
value: "Badung"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaKecamatan",
|
||||||
|
name: "Kecamatan",
|
||||||
|
value: "Abiansemal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaAlamat",
|
||||||
|
name: "Alamat Kantor Desa",
|
||||||
|
value: "Jl. Raya Darmasaba No.22, Darmasaba"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaPos",
|
||||||
|
name: "Kode Pos",
|
||||||
|
value: "80352"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaTelepon",
|
||||||
|
name: "Telepon",
|
||||||
|
value: "081239580000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "desaEmail",
|
||||||
|
name: "Email",
|
||||||
|
value: "desadarmasaba@badungkab.go.id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "perbekelNama",
|
||||||
|
name: "Nama Perbekel",
|
||||||
|
value: "Ida Bagus Surya Prabhawa Manuaba, S.H., M.H., N.L.P."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "perbekelJabatan",
|
||||||
|
name: "Jabatan",
|
||||||
|
value: "Perbekel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "perbekelNIP",
|
||||||
|
name: "NIP",
|
||||||
|
value: ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "perbekelTTD",
|
||||||
|
name: "TTD",
|
||||||
|
value: ""
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -2,16 +2,16 @@ import { Tree } from "@mantine/core";
|
|||||||
|
|
||||||
// ✅ Valid data, all values are unique
|
// ✅ Valid data, all values are unique
|
||||||
const data = [
|
const data = [
|
||||||
{
|
{
|
||||||
value: 'src',
|
value: "src",
|
||||||
label: 'src',
|
label: "src",
|
||||||
children: [
|
children: [
|
||||||
{ value: 'src/components', label: 'components' },
|
{ value: "src/components", label: "components" },
|
||||||
{ value: 'src/hooks', label: 'hooks' },
|
{ value: "src/hooks", label: "hooks" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ value: 'package.json', label: 'package.json' },
|
{ value: "package.json", label: "package.json" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function DirPage() {
|
export default function DirPage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { useEffect, useState } from "react";
|
import {
|
||||||
|
default as clientRoute,
|
||||||
|
default as clientRoutes,
|
||||||
|
} from "@/clientRoutes";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
AppShell,
|
AppShell,
|
||||||
@@ -22,17 +26,17 @@ import {
|
|||||||
IconChevronLeft,
|
IconChevronLeft,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconDashboard,
|
IconDashboard,
|
||||||
|
IconFileCertificate,
|
||||||
IconKey,
|
IconKey,
|
||||||
IconLock,
|
IconLock,
|
||||||
|
IconMessageReport,
|
||||||
|
IconSettings,
|
||||||
IconUser,
|
IconUser,
|
||||||
|
IconUsersGroup,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { User } from "generated/prisma";
|
import type { User } from "generated/prisma";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import {
|
|
||||||
default as clientRoute,
|
|
||||||
default as clientRoutes,
|
|
||||||
} from "@/clientRoutes";
|
|
||||||
import apiFetch from "@/lib/apiFetch";
|
|
||||||
|
|
||||||
function Logout() {
|
function Logout() {
|
||||||
return (
|
return (
|
||||||
@@ -98,7 +102,7 @@ export default function DashboardLayout() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "rgba(255,255,255,0.05)",
|
backgroundColor: "rgba(255,255,255,0.05)",
|
||||||
boxShadow: "0 0 6px rgba(0,255,200,0.2)",
|
boxShadow: "0 0 6px hsla(167, 100%, 50%, 0.20), 0.20)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{opened ? <IconChevronLeft /> : <IconChevronRight />}
|
{opened ? <IconChevronLeft /> : <IconChevronRight />}
|
||||||
@@ -186,7 +190,7 @@ function HostView() {
|
|||||||
{host.name}
|
{host.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{host.email}
|
{host.roleId}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -219,6 +223,31 @@ function NavigationDashboard() {
|
|||||||
label: "Dashboard Overview",
|
label: "Dashboard Overview",
|
||||||
description: "Quick summary and insights",
|
description: "Quick summary and insights",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/scr/dashboard/pengaduan/list",
|
||||||
|
icon: <IconMessageReport size={20} />,
|
||||||
|
label: "Pengaduan Warga",
|
||||||
|
description: "Manage pengaduan warga",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||||
|
icon: <IconFileCertificate size={20} />,
|
||||||
|
label: "Pelayanan Surat",
|
||||||
|
description: "Manage pelayanan surat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/scr/dashboard/warga/list-warga",
|
||||||
|
icon: <IconUsersGroup size={20} />,
|
||||||
|
label: "Warga",
|
||||||
|
description: "Manage warga",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/scr/dashboard/setting/detail-setting",
|
||||||
|
icon: <IconSettings size={20} />,
|
||||||
|
label: "Setting",
|
||||||
|
description:
|
||||||
|
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/scr/dashboard/apikey/apikey",
|
path: "/scr/dashboard/apikey/apikey",
|
||||||
icon: <IconKey size={20} />,
|
icon: <IconKey size={20} />,
|
||||||
|
|||||||
@@ -0,0 +1,370 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
IconAlignJustified,
|
||||||
|
IconCategory,
|
||||||
|
IconFileCertificate,
|
||||||
|
IconInfoTriangle,
|
||||||
|
IconMapPin,
|
||||||
|
IconMessageReport,
|
||||||
|
IconPhotoScan,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import useSwr from "swr";
|
||||||
|
|
||||||
|
export default function DetailPelayananPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const id = query.get("id");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Stack gap={"xl"}>
|
||||||
|
<DetailDataPelayanan />
|
||||||
|
<DetailDataHistori />
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailUserPelayanan />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailDataPelayanan() {
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={"Konfirmasi"}
|
||||||
|
centered
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{catModal === "tolak" ? (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Textarea size="md" minRows={5} />
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={close}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button variant="filled" color="red" onClick={close}>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={close}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button variant="filled" color="green" onClick={close}>
|
||||||
|
Terima
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Pelayanan Surat
|
||||||
|
</Title>
|
||||||
|
<Title order={4} c="dimmed">
|
||||||
|
#PGf-2345-33
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Badge
|
||||||
|
size="xl"
|
||||||
|
variant="light"
|
||||||
|
radius="sm"
|
||||||
|
color={"yellow"}
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
|
>
|
||||||
|
antrian
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} />
|
||||||
|
<Text size="md">Judul</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Judul Pelayanan Surat
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} />
|
||||||
|
<Text size="md">Lokasi</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
fwef
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCategory size={20} />
|
||||||
|
<Text size="md">Kategori</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
fwef
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconPhotoScan size={20} />
|
||||||
|
<Text size="md">Gambar</Text>
|
||||||
|
</Group>
|
||||||
|
<Anchor href="https://mantine.dev/" target="_blank">
|
||||||
|
Lihat Gambar
|
||||||
|
</Anchor>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} />
|
||||||
|
<Text size="md">Detail</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||||
|
Illum, corporis iusto. Suscipit veritatis quas, non nobis
|
||||||
|
fuga, laudantium accusantium tempora sint aliquid architecto
|
||||||
|
totam esse eum excepturi nostrum fugiat ut.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconInfoTriangle size={20} />
|
||||||
|
<Text size="md">Keterangan</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
|
||||||
|
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
|
||||||
|
suscipit incidunt quos beatae modi, vel, id ullam quae
|
||||||
|
voluptas, deserunt quas placeat.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("tolak");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("terima");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Terima
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailDataHistori() {
|
||||||
|
const elements = [
|
||||||
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = elements.map((element) => (
|
||||||
|
<Table.Tr key={element.name}>
|
||||||
|
<Table.Td>{element.position}</Table.Td>
|
||||||
|
<Table.Td>{element.name}</Table.Td>
|
||||||
|
<Table.Td>{element.symbol}</Table.Td>
|
||||||
|
<Table.Td>{element.mass}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Histori Pengaduan
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Tanggal</Table.Th>
|
||||||
|
<Table.Th>Deskripsi</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>User</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailUserPelayanan() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pengaduan.list.get({
|
||||||
|
query: {
|
||||||
|
status,
|
||||||
|
search: value,
|
||||||
|
take: "",
|
||||||
|
page: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
|
const list = data?.data || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Flex direction={"column"}>
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Warga
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconUser size={20} />
|
||||||
|
<Text size="md">Nama</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Amalia Dwi Yustiani
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} />
|
||||||
|
<Text size="md">Telepon</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
08123456789
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMessageReport size={20} />
|
||||||
|
<Text size="md">Jumlah Pengaduan</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
10
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFileCertificate size={20} />
|
||||||
|
<Text size="md">Jumlah Pelayanan Surat</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
10
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
274
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
274
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
CloseButton,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
IconAlignJustified,
|
||||||
|
IconClockHour3,
|
||||||
|
IconFileSad,
|
||||||
|
IconMapPin,
|
||||||
|
IconSearch,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import useSwr from "swr";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
const state = proxy({ reload: "" });
|
||||||
|
function reloadState() {
|
||||||
|
state.reload = Math.random().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PelayananSuratListPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const status = query.get("status") as StatusKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Stack gap="xl">
|
||||||
|
<TabListPelayananSurat status={status || "semua"} />
|
||||||
|
<ListPelayananSurat status={status || "semua"} />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabListPelayananSurat({ status }: { status: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dataCount = useSwr("/pelayanan-surat/count", () =>
|
||||||
|
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue={status || "semua"} color="teal">
|
||||||
|
<Tabs.List grow>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="all"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=semua");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Semua ({dataCount?.data?.semua || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="antrian"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=antrian");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Antrian ({dataCount?.data?.antrian || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="diterima"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=diterima");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Diterima ({dataCount?.data?.diterima || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="dikerjakan"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=dikerjakan");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="selesai"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=selesai");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Selesai ({dataCount?.data?.selesai || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="ditolak"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=ditolak");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ditolak ({dataCount?.data?.ditolak || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusKey =
|
||||||
|
| "antrian"
|
||||||
|
| "diterima"
|
||||||
|
| "dikerjakan"
|
||||||
|
| "ditolak"
|
||||||
|
| "selesai"
|
||||||
|
| "semua";
|
||||||
|
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pengaduan.list.get({
|
||||||
|
query: {
|
||||||
|
status,
|
||||||
|
search: value,
|
||||||
|
take: "",
|
||||||
|
page: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="lg"
|
||||||
|
p="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Loading pengaduan...
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const list = data?.data || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Group grow>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
placeholder="Cari pengaduan..."
|
||||||
|
onChange={(event) => setValue(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSectionPointerEvents="all"
|
||||||
|
rightSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => setValue("")}
|
||||||
|
style={{ display: value ? undefined : "none" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<Flex justify="center" align="center" py={"xl"}>
|
||||||
|
<Stack gap={4} align="center">
|
||||||
|
<IconFileSad size={32} color="gray" />
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
No pelayanan surat have been added yet.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
list.map((v: any) => (
|
||||||
|
<Card
|
||||||
|
key={v.id}
|
||||||
|
radius="lg"
|
||||||
|
p="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(
|
||||||
|
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${v.id}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Flex direction={"column"}>
|
||||||
|
<Title order={3} c="gray.2">
|
||||||
|
{v.title}
|
||||||
|
</Title>
|
||||||
|
<Group>
|
||||||
|
<Title order={6} c="gray.5">
|
||||||
|
#{v.noPengaduan}
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{v.updatedAt}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
<Badge
|
||||||
|
size="xl"
|
||||||
|
variant="light"
|
||||||
|
radius="sm"
|
||||||
|
color={
|
||||||
|
v.status === "diterima"
|
||||||
|
? "green"
|
||||||
|
: v.status === "ditolak"
|
||||||
|
? "red"
|
||||||
|
: v.status === "selesai"
|
||||||
|
? "blue"
|
||||||
|
: v.status === "dikerjakan"
|
||||||
|
? "purple"
|
||||||
|
: "yellow"
|
||||||
|
}
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
|
>
|
||||||
|
{v.status}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconClockHour3 size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Tanggal Aduan
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.createdAt}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Lokasi
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.location}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Detail
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.detail}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
397
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
397
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Anchor,
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Textarea,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
IconAlignJustified,
|
||||||
|
IconCategory,
|
||||||
|
IconFileCertificate,
|
||||||
|
IconInfoTriangle,
|
||||||
|
IconMapPin,
|
||||||
|
IconMessageReport,
|
||||||
|
IconPhotoScan,
|
||||||
|
IconUser,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import useSwr from "swr";
|
||||||
|
|
||||||
|
export default function DetailPengaduanPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const id = query.get("id");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Stack gap={"xl"}>
|
||||||
|
<DetailDataPengaduan />
|
||||||
|
<DetailDataHistori />
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailUserPengaduan />
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailDataPengaduan() {
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||||
|
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||||
|
const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
|
||||||
|
useDisclosure(false);
|
||||||
|
|
||||||
|
async function handleLihatGambar() {
|
||||||
|
const res = await apiFetch.api.pengaduan.image.get({
|
||||||
|
query: {
|
||||||
|
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.error('client',res)
|
||||||
|
// const blob = await res.data?.blob();
|
||||||
|
// setImageSrc(URL.createObjectURL(blob!));
|
||||||
|
// openModalImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
opened={opened}
|
||||||
|
onClose={close}
|
||||||
|
title={"Konfirmasi"}
|
||||||
|
centered
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{catModal === "tolak" ? (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Textarea size="md" minRows={5} />
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={close}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button variant="filled" color="red" onClick={close}>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button variant="light" onClick={close}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button variant="filled" color="green" onClick={close}>
|
||||||
|
Terima
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
opened={openedModalImage}
|
||||||
|
onClose={closeModalImage}
|
||||||
|
title="Gambar Pengaduan"
|
||||||
|
centered
|
||||||
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
|
>
|
||||||
|
<Image src={imageSrc!} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Pengaduan
|
||||||
|
</Title>
|
||||||
|
<Title order={4} c="dimmed">
|
||||||
|
#PGf-2345-33
|
||||||
|
</Title>
|
||||||
|
</Group>
|
||||||
|
<Badge
|
||||||
|
size="xl"
|
||||||
|
variant="light"
|
||||||
|
radius="sm"
|
||||||
|
color={"yellow"}
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
|
>
|
||||||
|
antrian
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} />
|
||||||
|
<Text size="md">Judul</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Judul Pengaduan
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} />
|
||||||
|
<Text size="md">Lokasi</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
fwef
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={6}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCategory size={20} />
|
||||||
|
<Text size="md">Kategori</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
fwef
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconPhotoScan size={20} />
|
||||||
|
<Text size="md">Gambar</Text>
|
||||||
|
</Group>
|
||||||
|
<Anchor href="#" onClick={handleLihatGambar}>
|
||||||
|
Lihat Gambar
|
||||||
|
</Anchor>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} />
|
||||||
|
<Text size="md">Detail</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||||
|
Illum, corporis iusto. Suscipit veritatis quas, non nobis
|
||||||
|
fuga, laudantium accusantium tempora sint aliquid architecto
|
||||||
|
totam esse eum excepturi nostrum fugiat ut.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconInfoTriangle size={20} />
|
||||||
|
<Text size="md">Keterangan</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
|
||||||
|
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
|
||||||
|
suscipit incidunt quos beatae modi, vel, id ullam quae
|
||||||
|
voluptas, deserunt quas placeat.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<Group justify="center" grow>
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("tolak");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("terima");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Terima
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailDataHistori() {
|
||||||
|
const elements = [
|
||||||
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = elements.map((element) => (
|
||||||
|
<Table.Tr key={element.name}>
|
||||||
|
<Table.Td>{element.position}</Table.Td>
|
||||||
|
<Table.Td>{element.name}</Table.Td>
|
||||||
|
<Table.Td>{element.symbol}</Table.Td>
|
||||||
|
<Table.Td>{element.mass}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Histori Pengaduan
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Tanggal</Table.Th>
|
||||||
|
<Table.Th>Deskripsi</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>User</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailUserPengaduan() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pengaduan.list.get({
|
||||||
|
query: {
|
||||||
|
status,
|
||||||
|
search: value,
|
||||||
|
take: "",
|
||||||
|
page: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
|
const list = data?.data || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Flex direction={"column"}>
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Warga
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap="md">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconUser size={20} />
|
||||||
|
<Text size="md">Nama</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c={"white"}>
|
||||||
|
Amalia Dwi Yustiani
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} />
|
||||||
|
<Text size="md">Telepon</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
08123456789
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMessageReport size={20} />
|
||||||
|
<Text size="md">Jumlah Pengaduan</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
10
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFileCertificate size={20} />
|
||||||
|
<Text size="md">Jumlah Pelayanan Surat</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md" c="white">
|
||||||
|
10
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
276
src/pages/scr/dashboard/pengaduan/list_page.tsx
Normal file
276
src/pages/scr/dashboard/pengaduan/list_page.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Card,
|
||||||
|
CloseButton,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Group,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import {
|
||||||
|
IconAlignJustified,
|
||||||
|
IconClockHour3,
|
||||||
|
IconFileSad,
|
||||||
|
IconMapPin,
|
||||||
|
IconSearch,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import useSwr from "swr";
|
||||||
|
import { proxy } from "valtio";
|
||||||
|
|
||||||
|
const state = proxy({ reload: "" });
|
||||||
|
function reloadState() {
|
||||||
|
state.reload = Math.random().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PengaduanListPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const status = query.get("status") as StatusKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Stack gap="xl">
|
||||||
|
<TabListPengaduan status={status || "semua"} />
|
||||||
|
<ListPengaduan status={status || "semua"} />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabListPengaduan({ status }: { status: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const dataCount = useSwr("/pengaduan/count", () =>
|
||||||
|
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs defaultValue={status || "semua"} color="teal">
|
||||||
|
<Tabs.List grow>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="all"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=semua");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Semua ({dataCount?.data?.semua || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="antrian"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=antrian");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Antrian ({dataCount?.data?.antrian || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="diterima"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=diterima");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Diterima ({dataCount?.data?.diterima || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="dikerjakan"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=dikerjakan");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="selesai"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=selesai");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Selesai ({dataCount?.data?.selesai || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="ditolak"
|
||||||
|
onClick={() => {
|
||||||
|
navigate("?status=ditolak");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ditolak ({dataCount?.data?.ditolak || 0})
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusKey =
|
||||||
|
| "antrian"
|
||||||
|
| "diterima"
|
||||||
|
| "dikerjakan"
|
||||||
|
| "ditolak"
|
||||||
|
| "selesai"
|
||||||
|
| "semua";
|
||||||
|
|
||||||
|
function ListPengaduan({ status }: { status: StatusKey }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pengaduan.list.get({
|
||||||
|
query: {
|
||||||
|
status,
|
||||||
|
search: value,
|
||||||
|
take: "",
|
||||||
|
page: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="lg"
|
||||||
|
p="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
Loading pengaduan...
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const list = data?.data || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="xl">
|
||||||
|
<Group grow>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
placeholder="Cari pengaduan..."
|
||||||
|
onChange={(event) => setValue(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSectionPointerEvents="all"
|
||||||
|
rightSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => setValue("")}
|
||||||
|
style={{ display: value ? undefined : "none" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* <Group justify="flex-end">
|
||||||
|
<Text size="sm">Menampilkan {Number(data?.data?.length) * (page - 1) + 1} – {Math.min(10, Number(data?.data?.length) * page)} dari {Number(data?.data?.length)}</Text>
|
||||||
|
<Pagination total={Number(data?.data?.length)} value={page} onChange={setPage} withPages={false} />
|
||||||
|
</Group> */}
|
||||||
|
</Group>
|
||||||
|
{list.length === 0 ? (
|
||||||
|
<Flex justify="center" align="center" py={"xl"}>
|
||||||
|
<Stack gap={4} align="center">
|
||||||
|
<IconFileSad size={32} color="gray" />
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
No pengaduan have been added yet.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Flex>
|
||||||
|
) : (
|
||||||
|
list.map((v: any) => (
|
||||||
|
<Card
|
||||||
|
key={v.id}
|
||||||
|
radius="lg"
|
||||||
|
p="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(`/scr/dashboard/pengaduan/detail?id=${v.id}`)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Flex direction={"column"}>
|
||||||
|
<Title order={3} c="gray.2">
|
||||||
|
{v.title}
|
||||||
|
</Title>
|
||||||
|
<Group>
|
||||||
|
<Title order={6} c="gray.5">
|
||||||
|
#{v.noPengaduan}
|
||||||
|
</Title>
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{v.updatedAt}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
<Badge
|
||||||
|
size="xl"
|
||||||
|
variant="light"
|
||||||
|
radius="sm"
|
||||||
|
color={
|
||||||
|
v.status === "diterima"
|
||||||
|
? "green"
|
||||||
|
: v.status === "ditolak"
|
||||||
|
? "red"
|
||||||
|
: v.status === "selesai"
|
||||||
|
? "blue"
|
||||||
|
: v.status === "dikerjakan"
|
||||||
|
? "purple"
|
||||||
|
: "yellow"
|
||||||
|
}
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
|
>
|
||||||
|
{v.status}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconClockHour3 size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Tanggal Aduan
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.createdAt}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Lokasi
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.location}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconAlignJustified size={20} color="white" />
|
||||||
|
<Text size="md" c="white">
|
||||||
|
Detail
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text size="md">{v.detail}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/pages/scr/dashboard/setting/detail_setting_page.tsx
Normal file
150
src/pages/scr/dashboard/setting/detail_setting_page.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { Button, Card, Container, Divider, Flex, Grid, Group, Input, NavLink, Stack, Table, Title } from "@mantine/core";
|
||||||
|
import { IconCircleOff, IconGauge, IconHome2 } from "@tabler/icons-react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function DetailSettingPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const type = query.get("type");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={3}>
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavLink
|
||||||
|
href={`?type=profile`}
|
||||||
|
label="Profile"
|
||||||
|
leftSection={<IconHome2 size={16} stroke={1.5} />}
|
||||||
|
active={type === "profile" || !type}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
href={`?type=cat-pengaduan`}
|
||||||
|
label="Kategori Pengaduan"
|
||||||
|
leftSection={<IconGauge size={16} stroke={1.5} />}
|
||||||
|
active={type === "cat-pengaduan"}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
href={`?type=cat-pelayanan`}
|
||||||
|
label="Kategori Pelayanan Surat"
|
||||||
|
leftSection={<IconCircleOff size={16} stroke={1.5} />}
|
||||||
|
active={type === "cat-pelayanan"}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
href={`?type=desa`}
|
||||||
|
label="Desa"
|
||||||
|
leftSection={<IconCircleOff size={16} stroke={1.5} />}
|
||||||
|
active={type === "desa"}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={9}>
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type === "cat-pengaduan"
|
||||||
|
? <KategoriPengaduanPage />
|
||||||
|
: type === "cat-pelayanan"
|
||||||
|
? <KategoriPengaduanPage />
|
||||||
|
: type === "desa"
|
||||||
|
? <KategoriPengaduanPage />
|
||||||
|
: <ProfilePage />}
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProfilePage() {
|
||||||
|
return (
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Profile Pengguna
|
||||||
|
</Title>
|
||||||
|
<Button variant="light">Edit</Button>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Group gap="xl" grow>
|
||||||
|
<Input.Wrapper label="Nama" description="" error="">
|
||||||
|
<Input value={"Amalia Dwi Yustiani"} readOnly />
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper label="Phone" description="" error="">
|
||||||
|
<Input value={"08123456789"} readOnly />
|
||||||
|
</Input.Wrapper>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xl" grow>
|
||||||
|
<Input.Wrapper label="Email" description="" error="">
|
||||||
|
<Input value={"amaliadwiyustiani@gmail.com"} readOnly />
|
||||||
|
</Input.Wrapper>
|
||||||
|
<Input.Wrapper label="Role" description="" error="">
|
||||||
|
<Input value={"Admin"} readOnly />
|
||||||
|
</Input.Wrapper>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function KategoriPengaduanPage() {
|
||||||
|
const elements = [
|
||||||
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = elements.map((element) => (
|
||||||
|
<Table.Tr key={element.name}>
|
||||||
|
<Table.Td>{element.position}</Table.Td>
|
||||||
|
<Table.Td>{element.name}</Table.Td>
|
||||||
|
<Table.Td>{element.symbol}</Table.Td>
|
||||||
|
<Table.Td>{element.mass}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Kategori Pengaduan
|
||||||
|
</Title>
|
||||||
|
<Button variant="light">Tambah</Button>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Stack gap={"md"}>
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Tanggal</Table.Th>
|
||||||
|
<Table.Th>Deskripsi</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>User</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
182
src/pages/scr/dashboard/warga/detail_warga_page.tsx
Normal file
182
src/pages/scr/dashboard/warga/detail_warga_page.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Grid,
|
||||||
|
Group,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import { IconMail, IconMapPin, IconPhone } from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
import useSwr from "swr";
|
||||||
|
|
||||||
|
export default function DetailWargaPage() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
const query = new URLSearchParams(search);
|
||||||
|
const id = query.get("id");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Grid>
|
||||||
|
<Grid.Col span={4}>
|
||||||
|
<DetailWarga />
|
||||||
|
</Grid.Col>
|
||||||
|
<Grid.Col span={8}>
|
||||||
|
<Stack gap={"xl"}>
|
||||||
|
<DetailDataHistori />
|
||||||
|
<DetailDataHistori />
|
||||||
|
</Stack>
|
||||||
|
</Grid.Col>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailDataHistori() {
|
||||||
|
const elements = [
|
||||||
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = elements.map((element) => (
|
||||||
|
<Table.Tr key={element.name}>
|
||||||
|
<Table.Td>{element.position}</Table.Td>
|
||||||
|
<Table.Td>{element.name}</Table.Td>
|
||||||
|
<Table.Td>{element.symbol}</Table.Td>
|
||||||
|
<Table.Td>{element.mass}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={4} c="gray.2">
|
||||||
|
Histori Pengaduan
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Tanggal</Table.Th>
|
||||||
|
<Table.Th>Deskripsi</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>User</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailWarga() {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pengaduan.list.get({
|
||||||
|
query: {
|
||||||
|
status,
|
||||||
|
search: value,
|
||||||
|
take: "",
|
||||||
|
page: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, [status, value]);
|
||||||
|
|
||||||
|
const list = data?.data || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="lg"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
borderColor: "rgba(100,100,100,0.2)",
|
||||||
|
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#f7d86c",
|
||||||
|
height: 100,
|
||||||
|
borderRadius: "12px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Group>
|
||||||
|
{/* Profile image */}
|
||||||
|
<Avatar
|
||||||
|
src="https://i.pravatar.cc/150?img=32"
|
||||||
|
radius={100}
|
||||||
|
size={90}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 80,
|
||||||
|
left: 30,
|
||||||
|
border: "4px solid white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main content */}
|
||||||
|
<Stack ml={115} gap={4}>
|
||||||
|
<Text fw={700} fz="lg">
|
||||||
|
Lizbeth Moore
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
Social Media Strategies
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{/* Contact info */}
|
||||||
|
<Card radius="md" mt="md" p="md" withBorder={false}>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMail size={18} />
|
||||||
|
<Text size="sm">lizbeth.moore@email.com</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconPhone size={18} />
|
||||||
|
<Text size="sm">+1 555-7788</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconMapPin size={18} />
|
||||||
|
<Text size="sm">Greenway Ave, Los Angeles, CA, USA</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/pages/scr/dashboard/warga/list_warga_page.tsx
Normal file
97
src/pages/scr/dashboard/warga/list_warga_page.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CloseButton,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
Title,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function ListWargaPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [value, setValue] = useState("");
|
||||||
|
const elements = [
|
||||||
|
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
||||||
|
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
||||||
|
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
||||||
|
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
||||||
|
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const rows = elements.map((element) => (
|
||||||
|
<Table.Tr key={element.name}>
|
||||||
|
<Table.Td>{element.position}</Table.Td>
|
||||||
|
<Table.Td>{element.name}</Table.Td>
|
||||||
|
<Table.Td>{element.symbol}</Table.Td>
|
||||||
|
<Table.Td>{element.mass}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(
|
||||||
|
`/scr/dashboard/warga/detail-warga?id=${element.position}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
|
<Card
|
||||||
|
radius="lg"
|
||||||
|
p="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Title order={3} c="gray.2">
|
||||||
|
List Data Warga
|
||||||
|
</Title>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
placeholder="Cari warga..."
|
||||||
|
onChange={(event) => setValue(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSectionPointerEvents="all"
|
||||||
|
rightSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => setValue("")}
|
||||||
|
style={{ display: value ? undefined : "none" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Divider my={0} />
|
||||||
|
<Table>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Tanggal</Table.Th>
|
||||||
|
<Table.Th>Deskripsi</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>User</Table.Th>
|
||||||
|
<Table.Th>Aksi</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>{rows}</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/server/lib/get-last-updated.ts
Normal file
21
src/server/lib/get-last-updated.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export function getLastUpdated(date: string | Date): string {
|
||||||
|
const now = new Date();
|
||||||
|
const updated = new Date(date);
|
||||||
|
const diffMs = now.getTime() - updated.getTime();
|
||||||
|
|
||||||
|
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||||
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
if (diffMinutes < 1) return "baru saja";
|
||||||
|
if (diffMinutes < 60) return `${diffMinutes} menit lalu`;
|
||||||
|
if (diffHours < 24) return `${diffHours} jam lalu`;
|
||||||
|
if (diffDays < 7) return `${diffDays} hari lalu`;
|
||||||
|
|
||||||
|
// kalau sudah lebih dari seminggu, tampilkan tanggal
|
||||||
|
return updated.toLocaleDateString("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
}
|
||||||
108
src/server/lib/mcp_tool_convert.ts
Normal file
108
src/server/lib/mcp_tool_convert.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
interface McpTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: any;
|
||||||
|
"x-props": {
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
operationId?: string;
|
||||||
|
tag?: string;
|
||||||
|
deprecated?: boolean;
|
||||||
|
summary?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
||||||
|
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
|
||||||
|
*/
|
||||||
|
export function convertOpenApiToMcpTools(openApiJson: any): McpTool[] {
|
||||||
|
const tools: McpTool[] = [];
|
||||||
|
const paths = openApiJson.paths || {};
|
||||||
|
|
||||||
|
for (const [path, methods] of Object.entries(paths)) {
|
||||||
|
// ✅ skip semua path internal MCP
|
||||||
|
if (path.startsWith("/mcp")) continue;
|
||||||
|
|
||||||
|
for (const [method, operation] of Object.entries<any>(methods as any)) {
|
||||||
|
const tags: string[] = Array.isArray(operation.tags) ? operation.tags : [];
|
||||||
|
|
||||||
|
// ✅ exclude semua yang tidak punya tag atau tag-nya tidak mengandung "mcp"
|
||||||
|
if (!tags.length || !tags.some(t => t.toLowerCase().includes("mcp"))) continue;
|
||||||
|
|
||||||
|
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||||
|
const name = cleanToolName(rawName);
|
||||||
|
|
||||||
|
const description =
|
||||||
|
operation.description ||
|
||||||
|
operation.summary ||
|
||||||
|
`Execute ${method.toUpperCase()} ${path}`;
|
||||||
|
|
||||||
|
const schema =
|
||||||
|
operation.requestBody?.content?.["application/json"]?.schema || {
|
||||||
|
type: "object",
|
||||||
|
properties: {},
|
||||||
|
additionalProperties: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tool: McpTool = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
"x-props": {
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
path,
|
||||||
|
operationId: operation.operationId,
|
||||||
|
tag: tags[0],
|
||||||
|
deprecated: operation.deprecated || false,
|
||||||
|
summary: operation.summary,
|
||||||
|
},
|
||||||
|
inputSchema: {
|
||||||
|
...schema,
|
||||||
|
additionalProperties: true,
|
||||||
|
$schema: "http://json-schema.org/draft-07/schema#",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
tools.push(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bersihkan nama agar valid untuk digunakan sebagai tool name
|
||||||
|
* - hapus karakter spesial
|
||||||
|
* - ubah slash jadi underscore
|
||||||
|
* - hilangkan prefix umum (get_, post_, api_, dll)
|
||||||
|
* - rapikan underscore berganda
|
||||||
|
*/
|
||||||
|
function cleanToolName(name: string): string {
|
||||||
|
return name
|
||||||
|
.replace(/[{}]/g, "")
|
||||||
|
.replace(/[^a-zA-Z0-9_]/g, "_")
|
||||||
|
.replace(/_+/g, "_")
|
||||||
|
.replace(/^_|_$/g, "")
|
||||||
|
.replace(/^(get|post|put|delete|patch|api)_/i, "")
|
||||||
|
.replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "")
|
||||||
|
.replace(/(^_|_$)/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
|
||||||
|
*/
|
||||||
|
export async function getMcpTools() {
|
||||||
|
const data = await fetch(`${process.env.BUN_PUBLIC_BASE_URL}/docs/json`);
|
||||||
|
const openApiJson = await data.json();
|
||||||
|
const tools = convertOpenApiToMcpTools(openApiJson);
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CLI Mode ===
|
||||||
|
if (import.meta.main) {
|
||||||
|
const tools = await getMcpTools();
|
||||||
|
await Bun.write("./tools.json", JSON.stringify(tools, null, 2));
|
||||||
|
}
|
||||||
42
src/server/lib/mimetypeToExtension.ts
Normal file
42
src/server/lib/mimetypeToExtension.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
export function mimeToExtension(mimeType: string): string {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
"image/jpeg": "jpg",
|
||||||
|
"image/png": "png",
|
||||||
|
"image/gif": "gif",
|
||||||
|
"image/webp": "webp",
|
||||||
|
"image/svg+xml": "svg",
|
||||||
|
"image/bmp": "bmp",
|
||||||
|
"image/tiff": "tiff",
|
||||||
|
|
||||||
|
"video/mp4": "mp4",
|
||||||
|
"video/webm": "webm",
|
||||||
|
"video/ogg": "ogv",
|
||||||
|
"video/quicktime": "mov",
|
||||||
|
|
||||||
|
"audio/mpeg": "mp3",
|
||||||
|
"audio/wav": "wav",
|
||||||
|
"audio/ogg": "ogg",
|
||||||
|
"audio/webm": "weba",
|
||||||
|
|
||||||
|
"application/pdf": "pdf",
|
||||||
|
"application/zip": "zip",
|
||||||
|
"application/x-zip-compressed": "zip",
|
||||||
|
"application/json": "json",
|
||||||
|
"application/javascript": "js",
|
||||||
|
"application/x-httpd-php": "php",
|
||||||
|
"application/msword": "doc",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": "docx",
|
||||||
|
"application/vnd.ms-excel": "xls",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "xlsx",
|
||||||
|
"application/vnd.ms-powerpoint": "ppt",
|
||||||
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation": "pptx",
|
||||||
|
|
||||||
|
"text/plain": "txt",
|
||||||
|
"text/html": "html",
|
||||||
|
"text/css": "css",
|
||||||
|
"text/csv": "csv",
|
||||||
|
"text/xml": "xml",
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[mimeType.toLowerCase()] || "bin"; // default jika tidak dikenal
|
||||||
|
}
|
||||||
23
src/server/lib/no-pengajuan-surat.ts
Normal file
23
src/server/lib/no-pengajuan-surat.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { prisma } from "./prisma"
|
||||||
|
|
||||||
|
export const generateNoPengajuanSurat = async () => {
|
||||||
|
const date = new Date()
|
||||||
|
const year = String(date.getFullYear()).slice(-2) // ambil 2 digit terakhir
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0")
|
||||||
|
const day = String(date.getDate()).padStart(2, "0")
|
||||||
|
|
||||||
|
const prefix = `PS-${day}${month}${year}`
|
||||||
|
|
||||||
|
const count = await prisma.pelayananAjuan.count({
|
||||||
|
where: {
|
||||||
|
noPengajuan: {
|
||||||
|
contains: prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// pastikan nomor urut selalu 3 digit
|
||||||
|
const number = String(count + 1).padStart(3, "0")
|
||||||
|
|
||||||
|
return `${prefix}-${number}`
|
||||||
|
}
|
||||||
15
src/server/lib/normalizePhone.ts
Normal file
15
src/server/lib/normalizePhone.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export function normalizePhoneNumber({ phone }: { phone: string }) {
|
||||||
|
// Hapus semua spasi, tanda hubung, atau karakter non-digit (+ tetap dipertahankan untuk dicek)
|
||||||
|
let cleaned = phone.trim().replace(/[\s-]/g, "");
|
||||||
|
|
||||||
|
// Jika diawali dengan +62 → ganti jadi 62
|
||||||
|
if (cleaned.startsWith("+62")) {
|
||||||
|
cleaned = "62" + cleaned.slice(3);
|
||||||
|
}
|
||||||
|
// Jika diawali dengan 0 → ganti jadi 62
|
||||||
|
else if (cleaned.startsWith("0")) {
|
||||||
|
cleaned = "62" + cleaned.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
228
src/server/lib/seafile.ts
Normal file
228
src/server/lib/seafile.ts
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
const CONFIG_FILE = path.join(os.homedir(), '.note.conf');
|
||||||
|
|
||||||
|
// --- Types ---
|
||||||
|
interface Config {
|
||||||
|
TOKEN?: string;
|
||||||
|
REPO?: string;
|
||||||
|
URL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultConfigSF: Config = {
|
||||||
|
TOKEN: process.env.SF_TOKEN,
|
||||||
|
REPO: process.env.SF_REPO,
|
||||||
|
URL: process.env.SF_URL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config Management ---
|
||||||
|
// async function createDefaultConfig(): Promise<void> {
|
||||||
|
// const defaultConfig = `TOKEN=fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445
|
||||||
|
// REPO=repos/e23626dc-cc18-4bb8-8fbc-d103b7d33bc8
|
||||||
|
// URL=https://cld-dkr-makuro-seafile.wibudev.com/api2
|
||||||
|
// `;
|
||||||
|
// await fs.writeFile(CONFIG_FILE, defaultConfig, 'utf8');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async function editConfig(): Promise<void> {
|
||||||
|
// if (!(await fs.stat(CONFIG_FILE)).isFile()) {
|
||||||
|
// createDefaultConfig();
|
||||||
|
// }
|
||||||
|
// const editor = process.env.EDITOR || 'vim';
|
||||||
|
// try {
|
||||||
|
// execSync(`${editor} "${CONFIG_FILE}"`, { stdio: 'inherit' });
|
||||||
|
// } catch {
|
||||||
|
// console.error('❌ Failed to open editor');
|
||||||
|
// process.exit(1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export async function loadConfig(): Promise<Config> {
|
||||||
|
if (!(await fs.stat(CONFIG_FILE)).isFile()) {
|
||||||
|
console.error(`⚠️ Config file not found at ${CONFIG_FILE}`);
|
||||||
|
console.error('Run: bun note.ts config to create/edit it.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configContent = await fs.readFile(CONFIG_FILE, 'utf8');
|
||||||
|
const config: Config = {};
|
||||||
|
|
||||||
|
configContent.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) return;
|
||||||
|
|
||||||
|
const [key, ...valueParts] = trimmed.split('=');
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
let value = valueParts.join('=').trim();
|
||||||
|
if (
|
||||||
|
(value.startsWith('"') && value.endsWith('"')) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = value.slice(1, -1);
|
||||||
|
}
|
||||||
|
config[key as keyof Config] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config.TOKEN || !config.REPO || !config.URL) {
|
||||||
|
console.error(`❌ Config invalid. Please set TOKEN, REPO, and URL inside ${CONFIG_FILE}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- HTTP Helpers ---
|
||||||
|
export async function fetchWithAuth(config: Config, url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
const headers = {
|
||||||
|
Authorization: `Token ${config.TOKEN}`,
|
||||||
|
...options.headers,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...options, headers });
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`❌ Request failed: ${response.status} ${response.statusText}`);
|
||||||
|
console.error(`🔍 URL: ${url}`);
|
||||||
|
console.error(`🔍 Headers:`, headers);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`🔍 Response body: ${errorText}`);
|
||||||
|
} catch {
|
||||||
|
console.error('🔍 Could not read response body');
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Commands ---
|
||||||
|
export async function testConnection(config: Config): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetchWithAuth(config, `${config.URL}/ping/`);
|
||||||
|
return `✅ API connection successful: ${await response.text()}`
|
||||||
|
} catch {
|
||||||
|
// return '⚠️ API ping failed, trying repo access...'
|
||||||
|
try {
|
||||||
|
await fetchWithAuth(config, `${config.URL}/${config.REPO}/`);
|
||||||
|
return `✅ Repo access successful`
|
||||||
|
} catch {
|
||||||
|
return '❌ Both API ping and repo access failed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listFiles(config: Config): Promise<{ name: string }[]> {
|
||||||
|
const url = `${config.URL}/${config.REPO}/dir/?p=/`;
|
||||||
|
const response = await fetchWithAuth(config, url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = (await response.json()) as { name: string }[];
|
||||||
|
return files
|
||||||
|
} catch {
|
||||||
|
console.error('❌ Failed to parse response');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function catFile(config: Config, fileName: string): Promise<string> {
|
||||||
|
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
|
||||||
|
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||||
|
const content = await (await fetchWithAuth(config, downloadUrl)).text();
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||||
|
const remoteName = path.basename(file.name);
|
||||||
|
|
||||||
|
// 1. Dapatkan upload link (pakai Authorization)
|
||||||
|
const uploadUrlResponse = await fetchWithAuth(
|
||||||
|
config,
|
||||||
|
`${config.URL}/${config.REPO}/upload-link/`
|
||||||
|
);
|
||||||
|
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||||
|
|
||||||
|
// 2. Siapkan form-data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("parent_dir", "/");
|
||||||
|
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||||
|
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
|
||||||
|
|
||||||
|
// 3. Upload file TANPA Authorization header, token di query param
|
||||||
|
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||||
|
return `✅ Uploaded ${file.name} successfully`;
|
||||||
|
}
|
||||||
|
export async function uploadFileBase64(config: Config, base64File: { name: string; data: string; }): Promise<string> {
|
||||||
|
const remoteName = path.basename(base64File.name);
|
||||||
|
|
||||||
|
// 1. Dapatkan upload link (pakai Authorization)
|
||||||
|
const uploadUrlResponse = await fetchWithAuth(
|
||||||
|
config,
|
||||||
|
`${config.URL}/${config.REPO}/upload-link/`
|
||||||
|
);
|
||||||
|
const uploadUrl = (await uploadUrlResponse.text()).replace(/"/g, "");
|
||||||
|
|
||||||
|
// 2. Konversi base64 ke Blob
|
||||||
|
const binary = Buffer.from(base64File.data, "base64");
|
||||||
|
const blob = new Blob([binary]);
|
||||||
|
|
||||||
|
// 3. Siapkan form-data
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("parent_dir", "/");
|
||||||
|
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||||
|
formData.append("file", blob, remoteName);
|
||||||
|
|
||||||
|
// 4. Upload file TANPA Authorization header, token di query param
|
||||||
|
const res = await fetch(`${uploadUrl}?token=${config.TOKEN}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`Upload failed: ${text}`);
|
||||||
|
return `✅ Uploaded ${base64File.name} successfully`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function removeFile(config: Config, fileName: string): Promise<string> {
|
||||||
|
await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`, { method: 'DELETE' });
|
||||||
|
return `🗑️ Removed ${fileName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function moveFile(config: Config, oldName: string, newName: string): Promise<string> {
|
||||||
|
const url = `${config.URL}/${config.REPO}/file/?p=/${oldName}`;
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('operation', 'rename');
|
||||||
|
formData.append('newname', newName);
|
||||||
|
|
||||||
|
await fetchWithAuth(config, url, { method: 'POST', body: formData });
|
||||||
|
return `✏️ Renamed ${oldName} → ${newName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> {
|
||||||
|
const localName = localFile || remoteFile;
|
||||||
|
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`);
|
||||||
|
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||||
|
|
||||||
|
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
|
||||||
|
await fs.writeFile(localName, buffer);
|
||||||
|
return `⬇️ Downloaded ${remoteFile} → ${localName}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFileLink(config: Config, fileName: string): Promise<string> {
|
||||||
|
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
|
||||||
|
return `🔗 Link for ${fileName}:\n${(await downloadUrlResponse.text()).replace(/"/g, '')}`
|
||||||
|
}
|
||||||
@@ -5,7 +5,11 @@ import { prisma } from '../lib/prisma'
|
|||||||
|
|
||||||
const secret = process.env.JWT_SECRET
|
const secret = process.env.JWT_SECRET
|
||||||
|
|
||||||
export default function apiAuth(app: Elysia) {
|
if (!secret) {
|
||||||
|
throw new Error('JWT_SECRET is not defined')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apiAuth(app: Elysia) {
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
throw new Error('JWT_SECRET is not defined')
|
throw new Error('JWT_SECRET is not defined')
|
||||||
}
|
}
|
||||||
@@ -16,37 +20,63 @@ export default function apiAuth(app: Elysia) {
|
|||||||
secret,
|
secret,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.derive(async ({ cookie, headers, jwt }) => {
|
.derive(async ({ cookie, headers, jwt, request }) => {
|
||||||
let token: string | undefined
|
let token: string | undefined
|
||||||
|
|
||||||
if (cookie?.token?.value) {
|
// 🔸 Ambil token dari Cookie
|
||||||
token = cookie.token.value as any
|
if (cookie?.token?.value) token = cookie.token.value as string
|
||||||
}
|
|
||||||
if (headers['x-token']?.startsWith('Bearer ')) {
|
|
||||||
token = (headers['x-token'] as string).slice(7)
|
|
||||||
}
|
|
||||||
if (headers['authorization']?.startsWith('Bearer ')) {
|
|
||||||
token = (headers['authorization'] as string).slice(7)
|
|
||||||
}
|
|
||||||
|
|
||||||
let user: null | Awaited<ReturnType<typeof prisma.user.findUnique>> = null
|
// 🔸 Ambil token dari Header (case-insensitive)
|
||||||
if (token) {
|
const possibleHeaders = [
|
||||||
try {
|
'authorization',
|
||||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
'Authorization',
|
||||||
if (decoded.sub) {
|
'x-token',
|
||||||
user = await prisma.user.findUnique({
|
'X-Token',
|
||||||
where: { id: decoded.sub as string },
|
]
|
||||||
})
|
|
||||||
}
|
for (const key of possibleHeaders) {
|
||||||
} catch (err) {
|
const value = headers[key]
|
||||||
console.warn('[SERVER][apiAuth] Invalid token', err)
|
if (typeof value === 'string') {
|
||||||
|
token = value.startsWith('Bearer ') ? value.slice(7) : value
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user }
|
// 🔸 Tidak ada token
|
||||||
|
if (!token) {
|
||||||
|
console.warn(`[AUTH] No token found for ${request.method} ${request.url}`)
|
||||||
|
return { user: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔸 Verifikasi token
|
||||||
|
try {
|
||||||
|
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||||
|
|
||||||
|
if (!decoded?.sub) {
|
||||||
|
console.warn('[AUTH] Token missing sub field:', decoded)
|
||||||
|
return { user: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: decoded.sub as string },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.warn('[AUTH] User not found for sub:', decoded.sub)
|
||||||
|
return { user: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user }
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[AUTH] Invalid JWT token:', err)
|
||||||
|
return { user: null }
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.onBeforeHandle(({ user, set }) => {
|
.onBeforeHandle(({ user, set, request }) => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
console.warn(
|
||||||
|
`[AUTH] Unauthorized access: ${request.method} ${request.url}`
|
||||||
|
)
|
||||||
set.status = 401
|
set.status = 401
|
||||||
return { error: 'Unauthorized' }
|
return { error: 'Unauthorized' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +1,8 @@
|
|||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||||
|
// import tools from "./../../../tools.json";
|
||||||
|
|
||||||
// const API_KEY = process.env.MCP_API_KEY ?? "super-secret-key";
|
var tools = [] as any[];
|
||||||
// const PORT = Number(process.env.PORT ?? 3000);
|
|
||||||
|
|
||||||
// // =====================
|
|
||||||
// // Helper Functions
|
|
||||||
// // =====================
|
|
||||||
// function isAuthorized(headers: Headers) {
|
|
||||||
// const authHeader = headers.get("authorization");
|
|
||||||
// if (authHeader?.startsWith("Bearer ")) {
|
|
||||||
// const token = authHeader.substring(7);
|
|
||||||
// return token === API_KEY;
|
|
||||||
// }
|
|
||||||
// return headers.get("x-api-key") === API_KEY;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// =====================
|
|
||||||
// Tools Definition
|
|
||||||
// =====================
|
|
||||||
type Tool = {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
inputSchema: {
|
|
||||||
type: string;
|
|
||||||
properties: Record<string, any>;
|
|
||||||
required?: string[];
|
|
||||||
additionalProperties?: boolean;
|
|
||||||
$schema?: string;
|
|
||||||
};
|
|
||||||
run: (input?: any) => Promise<any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const tools: Tool[] = [
|
|
||||||
{
|
|
||||||
name: "perbekal_darmasaba",
|
|
||||||
description: "Mengembalikan nama perbekal darmasaba",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
additionalProperties: true,
|
|
||||||
$schema: "http://json-schema.org/draft-07/schema#",
|
|
||||||
},
|
|
||||||
run: async () => ({ perbekal_darmasaba: "malik kurosaki" }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uuid",
|
|
||||||
description: "Menghasilkan UUID v4 unik.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {},
|
|
||||||
additionalProperties: true,
|
|
||||||
$schema: "http://json-schema.org/draft-07/schema#",
|
|
||||||
},
|
|
||||||
run: async () => ({ uuid: uuidv4() }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "echo",
|
|
||||||
description: "Mengembalikan data yang dikirim.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
input: {
|
|
||||||
type: "string",
|
|
||||||
description: "Message to echo back",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["input"],
|
|
||||||
additionalProperties: true,
|
|
||||||
$schema: "http://json-schema.org/draft-07/schema#",
|
|
||||||
},
|
|
||||||
run: async (input) => ({ echo: input }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Calculator",
|
|
||||||
description: "Useful for getting the result of a math expression. The input to this tool should be a valid mathematical expression that could be executed by a simple calculator.",
|
|
||||||
inputSchema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
input: {
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["input"],
|
|
||||||
additionalProperties: true,
|
|
||||||
$schema: "http://json-schema.org/draft-07/schema#",
|
|
||||||
},
|
|
||||||
run: async (input) => {
|
|
||||||
try {
|
|
||||||
// Simple math evaluation (be careful in production!)
|
|
||||||
const result = Function(`"use strict"; return (${input.input})`)();
|
|
||||||
return { result: String(result) };
|
|
||||||
} catch (error: any) {
|
|
||||||
throw new Error(`Invalid expression: ${error.message}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// MCP Protocol Types
|
// MCP Protocol Types
|
||||||
@@ -119,16 +25,50 @@ type JSONRPCResponse = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type JSONRPCNotification = {
|
// =====================
|
||||||
jsonrpc: "2.0";
|
// Tool Executor
|
||||||
method: string;
|
// =====================
|
||||||
params?: any;
|
export async function executeTool(
|
||||||
};
|
tool: any,
|
||||||
|
args: Record<string, any> = {},
|
||||||
|
baseUrl: string
|
||||||
|
) {
|
||||||
|
const x = tool["x-props"] || {};
|
||||||
|
|
||||||
|
const method = (x.method || "GET").toUpperCase();
|
||||||
|
const path = x.path || `/${tool.name}`;
|
||||||
|
const url = `${baseUrl}${path}`;
|
||||||
|
|
||||||
|
const opts: RequestInit = {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||||
|
opts.body = JSON.stringify(args || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, opts);
|
||||||
|
const contentType = res.headers.get("content-type") || "";
|
||||||
|
const data = contentType.includes("application/json")
|
||||||
|
? await res.json()
|
||||||
|
: await res.text();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: res.ok,
|
||||||
|
status: res.status,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// MCP Handler
|
// MCP Handler (Async)
|
||||||
// =====================
|
// =====================
|
||||||
function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
async function handleMCPRequestAsync(
|
||||||
|
request: JSONRPCRequest
|
||||||
|
): Promise<JSONRPCResponse> {
|
||||||
const { id, method, params } = request;
|
const { id, method, params } = request;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
@@ -138,13 +78,8 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
|||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
protocolVersion: "2024-11-05",
|
protocolVersion: "2024-11-05",
|
||||||
capabilities: {
|
capabilities: { tools: {} },
|
||||||
tools: {},
|
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
||||||
},
|
|
||||||
serverInfo: {
|
|
||||||
name: "elysia-mcp-server",
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,15 +88,16 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
|||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
result: {
|
result: {
|
||||||
tools: tools.map(({ name, description, inputSchema }) => ({
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
"x-props": x,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
case "tools/call":
|
case "tools/call": {
|
||||||
const toolName = params?.name;
|
const toolName = params?.name;
|
||||||
const tool = tools.find((t) => t.name === toolName);
|
const tool = tools.find((t) => t.name === toolName);
|
||||||
|
|
||||||
@@ -169,18 +105,14 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
|||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: {
|
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
||||||
code: -32601,
|
|
||||||
message: `Tool '${toolName}' not found`,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Note: This is synchronous for simplicity
|
const baseUrl =
|
||||||
// In real implementation, you'd need to handle async properly
|
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||||
let result: any;
|
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
||||||
tool.run(params?.arguments || {}).then((r) => (result = r));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
@@ -189,7 +121,7 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: JSON.stringify(result || { pending: true }),
|
text: JSON.stringify(result, null, 2),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -198,111 +130,48 @@ function handleMCPRequest(request: JSONRPCRequest): JSONRPCResponse {
|
|||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: {
|
error: { code: -32603, message: error.message },
|
||||||
code: -32603,
|
|
||||||
message: error.message,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "ping":
|
case "ping":
|
||||||
return {
|
return { jsonrpc: "2.0", id, result: {} };
|
||||||
jsonrpc: "2.0",
|
|
||||||
id,
|
|
||||||
result: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
jsonrpc: "2.0",
|
jsonrpc: "2.0",
|
||||||
id,
|
id,
|
||||||
error: {
|
error: { code: -32601, message: `Method '${method}' not found` },
|
||||||
code: -32601,
|
|
||||||
message: `Method '${method}' not found`,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
|
|
||||||
const { id, method, params } = request;
|
|
||||||
|
|
||||||
if (method === "tools/call") {
|
|
||||||
const toolName = params?.name;
|
|
||||||
const tool = tools.find((t) => t.name === toolName);
|
|
||||||
|
|
||||||
if (!tool) {
|
|
||||||
return {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id,
|
|
||||||
error: {
|
|
||||||
code: -32601,
|
|
||||||
message: `Tool '${toolName}' not found`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await tool.run(params?.arguments || {});
|
|
||||||
return {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id,
|
|
||||||
result: {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text",
|
|
||||||
text: JSON.stringify(result),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
|
||||||
return {
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
id,
|
|
||||||
error: {
|
|
||||||
code: -32603,
|
|
||||||
message: error.message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For other methods, use sync handler
|
|
||||||
return handleMCPRequest(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =====================
|
// =====================
|
||||||
// Server Initialization
|
// Elysia MCP Server
|
||||||
// =====================
|
// =====================
|
||||||
export const MCPRoute = new Elysia()
|
export const MCPRoute = new Elysia({
|
||||||
// =====================
|
tags: ["MCP Server"]
|
||||||
// MCP HTTP Streamable Endpoint
|
})
|
||||||
// =====================
|
.post("/mcp", async ({ request, set }) => {
|
||||||
.post("/mcp/:sessionId", async ({ params, request, set }) => {
|
if (!tools.length) {
|
||||||
|
tools = await getMcpTools();
|
||||||
|
}
|
||||||
set.headers["Content-Type"] = "application/json";
|
set.headers["Content-Type"] = "application/json";
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
|
|
||||||
// Optional: Check authorization
|
|
||||||
// if (!isAuthorized(request.headers)) {
|
|
||||||
// set.status = 401;
|
|
||||||
// return { error: "Unauthorized" };
|
|
||||||
// }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
// Handle single request
|
|
||||||
if (!Array.isArray(body)) {
|
if (!Array.isArray(body)) {
|
||||||
const response = await handleMCPRequestAsync(body as JSONRPCRequest);
|
const res = await handleMCPRequestAsync(body);
|
||||||
return response;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle batch requests
|
const results = await Promise.all(
|
||||||
const responses = await Promise.all(
|
body.map((req) => handleMCPRequestAsync(req))
|
||||||
body.map((req) => handleMCPRequestAsync(req as JSONRPCRequest))
|
|
||||||
);
|
);
|
||||||
return responses;
|
return results;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
set.status = 400;
|
set.status = 400;
|
||||||
return {
|
return {
|
||||||
@@ -317,60 +186,58 @@ export const MCPRoute = new Elysia()
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// =====================
|
// Tools list (debug)
|
||||||
// Simple tools list endpoint (for debugging)
|
.get("/mcp/tools", async ({ set }) => {
|
||||||
// =====================
|
if (!tools.length) {
|
||||||
.get("/mcp/:sessionId/tools", ({ set }) => {
|
tools = await getMcpTools();
|
||||||
|
}
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
return {
|
return {
|
||||||
data: tools.map(({ name, description, inputSchema }) => ({
|
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||||
name,
|
name,
|
||||||
value: name,
|
|
||||||
description,
|
description,
|
||||||
inputSchema,
|
inputSchema,
|
||||||
|
"x-props": x,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
// =====================
|
// MCP status
|
||||||
// Session Status
|
.get("/mcp/status", ({ set }) => {
|
||||||
// =====================
|
|
||||||
.get("/mcp/:sessionId/status", ({ params, set }) => {
|
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
return {
|
return { status: "active", timestamp: Date.now() };
|
||||||
sessionId: params.sessionId,
|
|
||||||
status: "active",
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// =====================
|
// Health check
|
||||||
// Health Check
|
|
||||||
// =====================
|
|
||||||
.get("/health", ({ set }) => {
|
.get("/health", ({ set }) => {
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
|
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
||||||
|
})
|
||||||
|
.get("/mcp/init", async ({ set }) => {
|
||||||
|
|
||||||
|
const _tools = await getMcpTools();
|
||||||
|
tools = _tools;
|
||||||
return {
|
return {
|
||||||
status: "ok",
|
success: true,
|
||||||
timestamp: Date.now(),
|
message: "MCP initialized",
|
||||||
tools: tools.length,
|
tools: tools.length,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
// =====================
|
// CORS
|
||||||
// CORS preflight
|
.options("/mcp", ({ set }) => {
|
||||||
// =====================
|
|
||||||
.options("/mcp/:sessionId", ({ set }) => {
|
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
||||||
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
|
set.headers["Access-Control-Allow-Headers"] =
|
||||||
|
"Content-Type,Authorization,X-API-Key";
|
||||||
set.status = 204;
|
set.status = 204;
|
||||||
return "";
|
return "";
|
||||||
})
|
})
|
||||||
|
.options("/mcp/tools", ({ set }) => {
|
||||||
.options("/mcp/:sessionId/tools", ({ set }) => {
|
|
||||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||||
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
||||||
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
|
set.headers["Access-Control-Allow-Headers"] =
|
||||||
|
"Content-Type,Authorization,X-API-Key";
|
||||||
set.status = 204;
|
set.status = 204;
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|||||||
306
src/server/routes/pelayanan_surat_route.ts
Normal file
306
src/server/routes/pelayanan_surat_route.ts
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import Elysia, { StatusMap, t } from "elysia"
|
||||||
|
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
|
||||||
|
import { prisma } from "../lib/prisma"
|
||||||
|
import type { StatusPengaduan } from "generated/prisma"
|
||||||
|
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||||
|
|
||||||
|
const PelayananRoute = new Elysia({
|
||||||
|
prefix: "pelayanan",
|
||||||
|
tags: ["pelayanan"],
|
||||||
|
})
|
||||||
|
|
||||||
|
// --- KATEGORI PELAYANAN ---
|
||||||
|
.get("/category", async () => {
|
||||||
|
const data = await prisma.categoryPelayanan.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
orderBy:{
|
||||||
|
name: "asc"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "List Kategori Pelayanan Surat",
|
||||||
|
description: `tool untuk mendapatkan list kategori pelayanan surat beserta syaratnya untuk memenuhi syarat dokumen sesuai kategori yg dipilih saat melakukan pengajuan surat`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/category/create", async ({ body }) => {
|
||||||
|
const { name, syaratDokumen, dataText } = body
|
||||||
|
|
||||||
|
await prisma.categoryPelayanan.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
syaratDokumen,
|
||||||
|
dataText,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, message: 'kategori pelayanan surat sudah dibuat' }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||||
|
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })),
|
||||||
|
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "buat kategori pelayanan surat",
|
||||||
|
description: `tool untuk membuat kategori pelayanan surat`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/category/update", async ({ body }) => {
|
||||||
|
const { id, name, syaratDokumen, dataText } = body
|
||||||
|
|
||||||
|
await prisma.categoryPelayanan.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
syaratDokumen,
|
||||||
|
dataText,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, message: 'kategori pelayanan surat sudah diperbarui' }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
|
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||||
|
syaratDokumen: t.Array(t.String({ minLength: 1, error: "syaratDokumen harus diisi" })),
|
||||||
|
dataText: t.Array(t.String({ minLength: 1, error: "dataText harus diisi" })),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "update kategori pelayanan surat",
|
||||||
|
description: `tool untuk update kategori pelayanan surat`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/category/delete", async ({ body }) => {
|
||||||
|
const { id } = body
|
||||||
|
|
||||||
|
await prisma.categoryPelayanan.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isActive: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, message: 'kategori pelayanan surat sudah dihapus' }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "delete kategori pelayanan surat",
|
||||||
|
description: `tool untuk delete kategori pelayanan surat`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// --- PELAYANAN SURAT ---
|
||||||
|
.get("/", async () => {
|
||||||
|
const data = await prisma.pelayananAjuan.findMany({
|
||||||
|
where: {
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "List Ajuan Pelayanan Surat",
|
||||||
|
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/detail", async ({ query }) => {
|
||||||
|
const { id } = query
|
||||||
|
const data = await prisma.pelayananAjuan.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Detail Ajuan Pelayanan Surat",
|
||||||
|
description: `tool untuk mendapatkan detail ajuan pelayanan surat`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/create", async ({ body }) => {
|
||||||
|
const { idCategory, idWarga, phone, dataText, syaratDokumen } = body
|
||||||
|
const noPengajuan = await generateNoPengajuanSurat()
|
||||||
|
let idCategoryFix = idCategory
|
||||||
|
let idWargaFix = idWarga
|
||||||
|
const category = await prisma.categoryPelayanan.findUnique({
|
||||||
|
where: {
|
||||||
|
id: idCategory,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
const cariCategory = await prisma.categoryPelayanan.findFirst({
|
||||||
|
where: {
|
||||||
|
name: idCategory,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cariCategory) {
|
||||||
|
throw new Error("kategori pelayanan surat tidak ditemukan")
|
||||||
|
} else {
|
||||||
|
idCategoryFix = cariCategory.id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const warga = await prisma.warga.findUnique({
|
||||||
|
where: {
|
||||||
|
id: idWarga,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!warga) {
|
||||||
|
const nomorHP = normalizePhoneNumber({ phone })
|
||||||
|
const cariWarga = await prisma.warga.findFirst({
|
||||||
|
where: {
|
||||||
|
phone: nomorHP,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cariWarga) {
|
||||||
|
const wargaCreate = await prisma.warga.create({
|
||||||
|
data: {
|
||||||
|
name: idWarga,
|
||||||
|
phone: nomorHP,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
idWargaFix = wargaCreate.id
|
||||||
|
} else {
|
||||||
|
idWargaFix = cariWarga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const pengaduan = await prisma.pelayananAjuan.create({
|
||||||
|
data: {
|
||||||
|
noPengajuan,
|
||||||
|
idCategory: idCategoryFix,
|
||||||
|
idWarga: idWargaFix,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!pengaduan.id) {
|
||||||
|
throw new Error("gagal membuat pengajuan surat")
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataInsertSyaratDokumen = []
|
||||||
|
let dataInsertDataText = []
|
||||||
|
|
||||||
|
for (const item of syaratDokumen) {
|
||||||
|
dataInsertSyaratDokumen.push({
|
||||||
|
idPengajuanLayanan: pengaduan.id,
|
||||||
|
idCategory: idCategoryFix,
|
||||||
|
jenis: item.jenis,
|
||||||
|
value: item.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of dataText) {
|
||||||
|
dataInsertDataText.push({
|
||||||
|
idPengajuanLayanan: pengaduan.id,
|
||||||
|
idCategory: idCategoryFix,
|
||||||
|
jenis: item.jenis,
|
||||||
|
value: item.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.syaratDokumenPelayanan.createMany({
|
||||||
|
data: dataInsertSyaratDokumen,
|
||||||
|
})
|
||||||
|
|
||||||
|
await prisma.dataTextPelayanan.createMany({
|
||||||
|
data: dataInsertDataText,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
await prisma.historyPelayanan.create({
|
||||||
|
data: {
|
||||||
|
idPengajuanLayanan: pengaduan.id,
|
||||||
|
deskripsi: "Pengajuan surat dibuat",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, message: 'pengajuan surat sudah dibuat' }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
||||||
|
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
||||||
|
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||||
|
dataText: t.Array(t.Object({
|
||||||
|
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
|
||||||
|
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
||||||
|
})),
|
||||||
|
syaratDokumen: t.Array(t.Object({
|
||||||
|
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
|
||||||
|
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Create Pengajuan Pelayanan Surat",
|
||||||
|
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/update-status", async ({ body }) => {
|
||||||
|
const { id, status, keterangan, idUser } = body
|
||||||
|
|
||||||
|
const pengajuan = await prisma.pelayananAjuan.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: status as StatusPengaduan,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!pengajuan) {
|
||||||
|
throw new Error("gagal membuat pengajuan")
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.historyPelayanan.create({
|
||||||
|
data: {
|
||||||
|
idPengajuanLayanan: pengajuan.id,
|
||||||
|
deskripsi: "Pengajuan surat diperbarui",
|
||||||
|
keteranganAlasan: keterangan,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return { success: true, message: 'pengajuan surat sudah diperbarui' }
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
|
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||||
|
keterangan: t.String({ minLength: 1, error: "keterangan harus diisi" }),
|
||||||
|
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Update Status Pengajuan Pelayanan Surat",
|
||||||
|
description: `tool untuk update status pengajuan pelayanan surat`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default PelayananRoute
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import Elysia, { t } from "elysia"
|
import Elysia, { t } from "elysia"
|
||||||
import type { StatusPengaduan } from "generated/prisma"
|
import type { StatusPengaduan } from "generated/prisma"
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
import { getLastUpdated } from "../lib/get-last-updated"
|
||||||
|
import { mimeToExtension } from "../lib/mimetypeToExtension"
|
||||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||||
|
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||||
import { prisma } from "../lib/prisma"
|
import { prisma } from "../lib/prisma"
|
||||||
|
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||||
|
|
||||||
const PengaduanRoute = new Elysia({
|
const PengaduanRoute = new Elysia({
|
||||||
prefix: "pengaduan",
|
prefix: "pengaduan",
|
||||||
@@ -13,13 +18,17 @@ const PengaduanRoute = new Elysia({
|
|||||||
const data = await prisma.categoryPengaduan.findMany({
|
const data = await prisma.categoryPengaduan.findMany({
|
||||||
where: {
|
where: {
|
||||||
isActive: true
|
isActive: true
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
name: "asc"
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
}, {
|
}, {
|
||||||
detail: {
|
detail: {
|
||||||
summary: "get kategori pengaduan",
|
summary: "List Kategori Pengaduan",
|
||||||
description: `tool untuk mendapatkan kategori pengaduan`
|
description: `tool untuk mendapatkan list kategori pengaduan`,
|
||||||
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/category/create", async ({ body }) => {
|
.post("/category/create", async ({ body }) => {
|
||||||
@@ -31,10 +40,7 @@ const PengaduanRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
return { success: true, message: 'kategori pengaduan sudah dibuat' }
|
||||||
${JSON.stringify(body)}
|
|
||||||
|
|
||||||
kategori pengaduan sudah dibuat`
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||||
@@ -56,10 +62,7 @@ const PengaduanRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
return { success: true, message: 'kategori pengaduan sudah diperbarui' }
|
||||||
${JSON.stringify(body)}
|
|
||||||
|
|
||||||
kategori pengaduan sudah diperbarui`
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
@@ -82,10 +85,7 @@ const PengaduanRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
return { success: true, message: 'kategori pengaduan sudah dihapus' }
|
||||||
${JSON.stringify(body)}
|
|
||||||
|
|
||||||
kategori pengaduan sudah dihapus`
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
@@ -100,15 +100,68 @@ const PengaduanRoute = new Elysia({
|
|||||||
|
|
||||||
// --- PENGADUAN ---
|
// --- PENGADUAN ---
|
||||||
.post("/create", async ({ body }) => {
|
.post("/create", async ({ body }) => {
|
||||||
const { title, detail, location, image, idCategory, idWarga } = body
|
const { title, detail, location, image, idCategory, idWarga, phone } = body
|
||||||
const noPengaduan = await generateNoPengaduan()
|
const noPengaduan = await generateNoPengaduan()
|
||||||
|
let idCategoryFix = idCategory
|
||||||
|
let idWargaFix = idWarga
|
||||||
|
const category = await prisma.categoryPengaduan.findUnique({
|
||||||
|
where: {
|
||||||
|
id: idCategory,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||||
|
where: {
|
||||||
|
name: idCategory,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cariCategory) {
|
||||||
|
idCategoryFix = "lainnya"
|
||||||
|
} else {
|
||||||
|
idCategoryFix = cariCategory.id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const warga = await prisma.warga.findUnique({
|
||||||
|
where: {
|
||||||
|
id: idWarga,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!warga) {
|
||||||
|
const nomorHP = normalizePhoneNumber({ phone })
|
||||||
|
const cariWarga = await prisma.warga.findUnique({
|
||||||
|
where: {
|
||||||
|
phone: nomorHP,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!cariWarga) {
|
||||||
|
const wargaCreate = await prisma.warga.create({
|
||||||
|
data: {
|
||||||
|
name: idWarga,
|
||||||
|
phone: nomorHP,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
idWargaFix = wargaCreate.id
|
||||||
|
} else {
|
||||||
|
idWargaFix = cariWarga.id
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
const pengaduan = await prisma.pengaduan.create({
|
const pengaduan = await prisma.pengaduan.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
detail,
|
detail,
|
||||||
idCategory,
|
idCategory: idCategoryFix,
|
||||||
idWarga,
|
idWarga: idWargaFix,
|
||||||
location,
|
location,
|
||||||
image,
|
image,
|
||||||
noPengaduan,
|
noPengaduan,
|
||||||
@@ -129,26 +182,25 @@ const PengaduanRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
return { success: true, message: 'pengaduan sudah dibuat' }
|
||||||
${JSON.stringify(body)}
|
|
||||||
|
|
||||||
pengaduan sudah dibuat`
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
||||||
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
||||||
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
||||||
image: t.String({ minLength: 1, error: "image harus diisi" }),
|
image: t.Any(),
|
||||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
||||||
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
||||||
|
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "buat pengaduan",
|
summary: "Create Pengaduan Warga",
|
||||||
description: `tool untuk membuat pengaduan`
|
description: `tool untuk membuat pengaduan warga`,
|
||||||
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/update-status", async ({ body }) => {
|
.post("/update-status", async ({ body }) => {
|
||||||
const { id, status, keterangan } = body
|
const { id, status, keterangan, idUser } = body
|
||||||
let deskripsi = ""
|
let deskripsi = ""
|
||||||
|
|
||||||
const pengaduan = await prisma.pengaduan.update({
|
const pengaduan = await prisma.pengaduan.update({
|
||||||
@@ -165,13 +217,13 @@ const PengaduanRoute = new Elysia({
|
|||||||
throw new Error("gagal membuat pengaduan")
|
throw new Error("gagal membuat pengaduan")
|
||||||
}
|
}
|
||||||
|
|
||||||
if(status === "diterima") {
|
if (status === "diterima") {
|
||||||
deskripsi = "Pengaduan diterima oleh admin"
|
deskripsi = "Pengaduan diterima oleh admin"
|
||||||
} else if(status === "dikerjakan") {
|
} else if (status === "dikerjakan") {
|
||||||
deskripsi = "Pengaduan dikerjakan oleh petugas"
|
deskripsi = "Pengaduan dikerjakan oleh petugas"
|
||||||
} else if(status === "ditolak") {
|
} else if (status === "ditolak") {
|
||||||
deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan
|
deskripsi = "Pengaduan ditolak dengan keterangan " + keterangan
|
||||||
} else if(status === "selesai") {
|
} else if (status === "selesai") {
|
||||||
deskripsi = "Pengaduan selesai"
|
deskripsi = "Pengaduan selesai"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,24 +232,420 @@ const PengaduanRoute = new Elysia({
|
|||||||
idPengaduan: pengaduan.id,
|
idPengaduan: pengaduan.id,
|
||||||
deskripsi,
|
deskripsi,
|
||||||
status: status as StatusPengaduan,
|
status: status as StatusPengaduan,
|
||||||
idUser: ""
|
idUser,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return `
|
return { success: true, message: 'status pengaduan sudah diupdate' }
|
||||||
${JSON.stringify(body)}
|
|
||||||
|
|
||||||
status pengaduan sudah diupdate`
|
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||||
keterangan: t.Any()
|
keterangan: t.Any(),
|
||||||
|
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
detail: {
|
detail: {
|
||||||
summary: "update status pengaduan",
|
summary: "Update status pengaduan",
|
||||||
description: `tool untuk update status pengaduan`
|
description: `tool untuk update status pengaduan`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.get("/detail", async ({ query }) => {
|
||||||
|
const { id } = query
|
||||||
|
const data = await prisma.pengaduan.findUnique({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengaduan: true,
|
||||||
|
title: true,
|
||||||
|
detail: true,
|
||||||
|
location: true,
|
||||||
|
image: true,
|
||||||
|
idCategory: true,
|
||||||
|
idWarga: true,
|
||||||
|
status: true,
|
||||||
|
keterangan: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
CategoryPengaduan: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Warga: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||||
|
where: {
|
||||||
|
idPengaduan: id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
deskripsi: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
idUser: true,
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataHistoryFix = dataHistory.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
deskripsi: item.deskripsi,
|
||||||
|
status: item.status,
|
||||||
|
createdAt: item.createdAt,
|
||||||
|
idUser: item.idUser,
|
||||||
|
nameUser: item.User?.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const datafix = {
|
||||||
|
id: data?.id,
|
||||||
|
noPengaduan: data?.noPengaduan,
|
||||||
|
title: data?.title,
|
||||||
|
detail: data?.detail,
|
||||||
|
location: data?.location,
|
||||||
|
image: data?.image,
|
||||||
|
CategoryPengaduan: data?.CategoryPengaduan.name,
|
||||||
|
idWarga: data?.idWarga,
|
||||||
|
nameWarga: data?.Warga?.name,
|
||||||
|
status: data?.status,
|
||||||
|
keterangan: data?.keterangan,
|
||||||
|
createdAt: data?.createdAt,
|
||||||
|
updatedAt: data?.updatedAt,
|
||||||
|
history: dataHistoryFix,
|
||||||
|
}
|
||||||
|
|
||||||
|
return datafix
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Detail Pengaduan Warga",
|
||||||
|
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/", async ({ query }) => {
|
||||||
|
const { take, page, search, phone } = query
|
||||||
|
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||||
|
|
||||||
|
const data = await prisma.pengaduan.findMany({
|
||||||
|
skip,
|
||||||
|
take: !take ? 10 : Number(take),
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc"
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noPengaduan: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
AND: {
|
||||||
|
Warga: {
|
||||||
|
phone: phone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengaduan: true,
|
||||||
|
title: true,
|
||||||
|
detail: true,
|
||||||
|
location: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
CategoryPengaduan: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Warga: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataFix = data.map((item) => {
|
||||||
|
return {
|
||||||
|
noPengaduan: item.noPengaduan,
|
||||||
|
title: item.title,
|
||||||
|
detail: item.detail,
|
||||||
|
status: item.status,
|
||||||
|
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataFix
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
take: t.String({ optional: true }),
|
||||||
|
page: t.String({ optional: true }),
|
||||||
|
search: t.String({ optional: true }),
|
||||||
|
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "List Pengaduan Warga By Phone",
|
||||||
|
description: `tool untuk mendapatkan list pengaduan warga by phone`,
|
||||||
|
tags: ["mcp"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.post("/upload", async ({ body }) => {
|
||||||
|
const { file } = body;
|
||||||
|
|
||||||
|
// Validasi file
|
||||||
|
if (!file) {
|
||||||
|
return { success: false, message: "File tidak ditemukan" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||||
|
// const buffer = await file.arrayBuffer();
|
||||||
|
const result = await uploadFile(defaultConfigSF, file);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Upload berhasil",
|
||||||
|
filename: file.name,
|
||||||
|
size: file.size,
|
||||||
|
seafileResult: result
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
file: t.File({ format: "binary" })
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Upload File",
|
||||||
|
description: "Tool untuk upload file ke Seafile",
|
||||||
|
tags: ["mcp"],
|
||||||
|
consumes: ["multipart/form-data"]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.post("/upload-base64", async ({ body }) => {
|
||||||
|
const { data, mimetype } = body;
|
||||||
|
const ext = mimeToExtension(mimetype)
|
||||||
|
const name = `${uuidv4()}.${ext}`
|
||||||
|
|
||||||
|
// Validasi file
|
||||||
|
if (!data) {
|
||||||
|
return { success: false, message: "File tidak ditemukan" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konversi file ke base64
|
||||||
|
// const buffer = await file.arrayBuffer();
|
||||||
|
// const base64String = Buffer.from(buffer).toString("base64");
|
||||||
|
|
||||||
|
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
|
||||||
|
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Upload berhasil",
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
mimetype,
|
||||||
|
ext,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
data: t.String(),
|
||||||
|
mimetype: t.String()
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Upload File (Base64)",
|
||||||
|
description: "Tool untuk upload file ke Seafile dalam format Base64",
|
||||||
|
tags: ["mcp"],
|
||||||
|
consumes: ["multipart/form-data"]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.get("/list", async ({ query }) => {
|
||||||
|
const { take, page, search, status } = query
|
||||||
|
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||||
|
|
||||||
|
let where: any = {
|
||||||
|
isActive: true,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
title: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noPengaduan: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Warga: {
|
||||||
|
phone: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status !== "semua") {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await prisma.pengaduan.findMany({
|
||||||
|
skip,
|
||||||
|
take: !take ? 10 : Number(take),
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc"
|
||||||
|
},
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengaduan: true,
|
||||||
|
title: true,
|
||||||
|
detail: true,
|
||||||
|
location: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
CategoryPengaduan: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Warga: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataFix = data.map((item) => {
|
||||||
|
return {
|
||||||
|
noPengaduan: item.noPengaduan,
|
||||||
|
id: item.id,
|
||||||
|
title: item.title,
|
||||||
|
detail: item.detail,
|
||||||
|
status: item.status,
|
||||||
|
location: item.location,
|
||||||
|
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||||
|
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataFix
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
take: t.String({ optional: true }),
|
||||||
|
page: t.String({ optional: true }),
|
||||||
|
search: t.String({ optional: true }),
|
||||||
|
status: t.String({ optional: true }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "List Pengaduan Warga",
|
||||||
|
description: `tool untuk mendapatkan list pengaduan warga`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/count", async ({ query }) => {
|
||||||
|
const counts = await prisma.pengaduan.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped = Object.fromEntries(
|
||||||
|
counts.map(c => [c.status, c._count.status])
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = await prisma.pengaduan.count({
|
||||||
|
where: { isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
antrian: grouped?.antrian || 0,
|
||||||
|
diterima: grouped?.diterima || 0,
|
||||||
|
dikerjakan: grouped?.dikerjakan || 0,
|
||||||
|
ditolak: grouped?.ditolak || 0,
|
||||||
|
selesai: grouped?.selesai || 0,
|
||||||
|
semua: total,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Jumlah Pengaduan Warga",
|
||||||
|
description: `tool untuk mendapatkan jumlah pengaduan warga`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/image", async ({ query, set }) => {
|
||||||
|
const { fileName } = query
|
||||||
|
|
||||||
|
const connect = await testConnection(defaultConfigSF)
|
||||||
|
console.log({connect})
|
||||||
|
|
||||||
|
const hasil = await catFile(defaultConfigSF, fileName)
|
||||||
|
console.log('hasilnya', hasil)
|
||||||
|
// Tentukan tipe MIME berdasarkan ekstensi
|
||||||
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
|
const mime =
|
||||||
|
ext === "jpg" || ext === "jpeg"
|
||||||
|
? "image/jpeg"
|
||||||
|
: ext === "png"
|
||||||
|
? "image/png"
|
||||||
|
: "application/octet-stream";
|
||||||
|
|
||||||
|
set.headers["Content-Type"] = mime;
|
||||||
|
return new Response(hasil);
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
fileName: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Gambar Pengaduan Warga",
|
||||||
|
description: `tool untuk mendapatkan gambar pengaduan warga`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
;
|
||||||
|
|
||||||
export default PengaduanRoute
|
export default PengaduanRoute
|
||||||
|
|||||||
53
src/server/routes/test.ts
Normal file
53
src/server/routes/test.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Elysia, { t } from "elysia";
|
||||||
|
|
||||||
|
const TestRoute = new Elysia({
|
||||||
|
prefix: "test",
|
||||||
|
tags: ["mcp", "test"],
|
||||||
|
})
|
||||||
|
.get("/info-rapat-list", () => {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "data info rapat berhasil diambil",
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
judul: "Info Rapat",
|
||||||
|
tanggal: "2025-11-10",
|
||||||
|
deskripsi: "Info rapat",
|
||||||
|
gambar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "mendapatkan list rapat",
|
||||||
|
description: "mendapatkan list rapat dari database",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.post("/simpan-rapat", ({ body }) => {
|
||||||
|
if (!body.gambar) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "gambar harus diisi",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "data info rapat berhasil diambil",
|
||||||
|
chunk: body.gambar.substring(22)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
body: t.Object({
|
||||||
|
judul: t.String(),
|
||||||
|
tanggal: t.String(),
|
||||||
|
deskripsi: t.String(),
|
||||||
|
gambar: t.Required(t.String()),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "simpan data rapat",
|
||||||
|
description: "simpan data rapat memerlukan base64 gambar",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TestRoute
|
||||||
|
|
||||||
612
tools.json
Normal file
612
tools.json
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "apikey_create",
|
||||||
|
"description": "create api key by user",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/apikey/create",
|
||||||
|
"operationId": "postApiApikeyCreate",
|
||||||
|
"tag": "apikey",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "create"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expiredAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "apikey_list",
|
||||||
|
"description": "get api key list by user",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/apikey/list",
|
||||||
|
"operationId": "getApiApikeyList",
|
||||||
|
"tag": "apikey",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "list"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "apikey_delete",
|
||||||
|
"description": "delete api key by id",
|
||||||
|
"x-props": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/api/apikey/delete",
|
||||||
|
"operationId": "deleteApiApikeyDelete",
|
||||||
|
"tag": "apikey",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "delete"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_repos",
|
||||||
|
"description": "get list of repositories",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/repos",
|
||||||
|
"operationId": "getApiDarmasabaRepos",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "repos"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_ls",
|
||||||
|
"description": "get list of dir in darmasaba",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/ls",
|
||||||
|
"operationId": "getApiDarmasabaLs",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "ls"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_ls_by_dir",
|
||||||
|
"description": "get list of files in darmasaba/<dir>",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/ls/{dir}",
|
||||||
|
"operationId": "getApiDarmasabaLsByDir",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "ls"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_file_by_dir_by_file_name",
|
||||||
|
"description": "get content of file in darmasaba/<dir>/<file_name>",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/file/{dir}/{file_name}",
|
||||||
|
"operationId": "getApiDarmasabaFileByDirByFile_name",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "file"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_list_pengetahuan_umum",
|
||||||
|
"description": "get list of files in darmasaba/pengetahuan-umum",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/list-pengetahuan-umum",
|
||||||
|
"operationId": "getApiDarmasabaList-pengetahuan-umum",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "list-pengetahuan-umum"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_pengetahuan_umum_by_file_name",
|
||||||
|
"description": "get content of file in darmasaba/pengetahuan-umum/<file_name>",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/darmasaba/pengetahuan-umum/{file_name}",
|
||||||
|
"operationId": "getApiDarmasabaPengetahuan-umumByFile_name",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "pengetahuan-umum"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_buat_pengaduan",
|
||||||
|
"description": "tool untuk membuat pengaduan atau pelaporan warga kepada desa darmasaba",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/darmasaba/buat-pengaduan",
|
||||||
|
"operationId": "postApiDarmasabaBuat-pengaduan",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "buat-pengaduan atau pelaporan"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"jenis_laporan": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "jenis laporan harus diisi",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "name harus diisi",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "phone harus diisi",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "detail harus diisi",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"jenis_laporan",
|
||||||
|
"name",
|
||||||
|
"phone",
|
||||||
|
"detail"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "darmasaba_status_pengaduan",
|
||||||
|
"description": "melikat status pengaduan dari user",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/darmasaba/status-pengaduan",
|
||||||
|
"operationId": "postApiDarmasabaStatus-pengaduan",
|
||||||
|
"tag": "darmasaba",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "lihat status pengaduan"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "credential_create",
|
||||||
|
"description": "create credential",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/credential/create",
|
||||||
|
"operationId": "postApiCredentialCreate",
|
||||||
|
"tag": "credential",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "create"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "credential_list",
|
||||||
|
"description": "get credential list",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/credential/list",
|
||||||
|
"operationId": "getApiCredentialList",
|
||||||
|
"tag": "credential",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "list"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "credential_rm",
|
||||||
|
"description": "delete credential by id",
|
||||||
|
"x-props": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/api/credential/rm",
|
||||||
|
"operationId": "deleteApiCredentialRm",
|
||||||
|
"tag": "credential",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "rm"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_find",
|
||||||
|
"description": "find user",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/user/find",
|
||||||
|
"operationId": "getApiUserFind",
|
||||||
|
"tag": "user",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "find"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "user_upsert",
|
||||||
|
"description": "upsert user",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/user/upsert",
|
||||||
|
"operationId": "postApiUserUpsert",
|
||||||
|
"tag": "user",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "upsert"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "name is required",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"phone": {
|
||||||
|
"minLength": 1,
|
||||||
|
"error": "phone is required",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"phone"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "layanan_list",
|
||||||
|
"description": "Returns the list of all available public services.",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/layanan/list",
|
||||||
|
"operationId": "getApiLayananList",
|
||||||
|
"tag": "layanan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "List Layanan"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "layanan_create_ktp",
|
||||||
|
"description": "Create a new service request for KTP or KK.",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/layanan/create-ktp",
|
||||||
|
"operationId": "postApiLayananCreate-ktp",
|
||||||
|
"tag": "layanan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "Create Layanan KTP/KK"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"jenis": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"const": "ktp",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"const": "kk",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nama": {
|
||||||
|
"minLength": 3,
|
||||||
|
"description": "Nama pemohon layanan",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deskripsi": {
|
||||||
|
"minLength": 5,
|
||||||
|
"description": "Deskripsi singkat permohonan layanan",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"jenis",
|
||||||
|
"nama",
|
||||||
|
"deskripsi"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "layanan_status_ktp",
|
||||||
|
"description": "Retrieve the current status of a KTP/KK request by unique ID.",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/layanan/status-ktp",
|
||||||
|
"operationId": "postApiLayananStatus-ktp",
|
||||||
|
"tag": "layanan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "Cek Status KTP"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uniqid": {
|
||||||
|
"description": "Unique ID layanan",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"uniqid"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aduan_create",
|
||||||
|
"description": "create aduan",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/aduan/create",
|
||||||
|
"operationId": "postApiAduanCreate",
|
||||||
|
"tag": "aduan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "create"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"description"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aduan_aduan_sampah",
|
||||||
|
"description": "tool untuk membuat aduan sampah liar",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/api/aduan/aduan-sampah",
|
||||||
|
"operationId": "postApiAduanAduan-sampah",
|
||||||
|
"tag": "aduan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "aduan sampah"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"judul": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deskripsi": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"judul",
|
||||||
|
"deskripsi"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "aduan_list_aduan_sampah",
|
||||||
|
"description": "tool untuk melihat list aduan sampah liar",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/aduan/list-aduan-sampah",
|
||||||
|
"operationId": "getApiAduanList-aduan-sampah",
|
||||||
|
"tag": "aduan",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "list aduan sampah"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "auth_login",
|
||||||
|
"description": "Login with phone; auto-register if not found",
|
||||||
|
"x-props": {
|
||||||
|
"method": "POST",
|
||||||
|
"path": "/auth/login",
|
||||||
|
"operationId": "postAuthLogin",
|
||||||
|
"tag": "auth",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "login"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"email",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "auth_logout",
|
||||||
|
"description": "Logout (clear token cookie)",
|
||||||
|
"x-props": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"path": "/auth/logout",
|
||||||
|
"operationId": "deleteAuthLogout",
|
||||||
|
"tag": "auth",
|
||||||
|
"deprecated": false,
|
||||||
|
"summary": "logout"
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "health",
|
||||||
|
"description": "Execute GET /health",
|
||||||
|
"x-props": {
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/health",
|
||||||
|
"operationId": "getHealth",
|
||||||
|
"deprecated": false
|
||||||
|
},
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": true,
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
2
upload.sh
Normal file
2
upload.sh
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
curl -s -X POST http://localhost:3000/api/pengaduan/upload \
|
||||||
|
-F file=@package.json
|
||||||
5
upload_base64.sh
Normal file
5
upload_base64.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
IMAGE_BASE64=$(base64 image.png | tr -d '\n')
|
||||||
|
|
||||||
|
curl -X POST http://localhost:3000/api/pengaduan/upload-base64 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"file\": \"$IMAGE_BASE64\"}"
|
||||||
101
x.sh
101
x.sh
@@ -1,6 +1,97 @@
|
|||||||
# curl -N -v -X GET "https://cld-dkr-prod-jenna-mcp.wibudev.com/mcp/test-session-id"
|
# curl -X POST https://cld-dkr-makuro-seafile.wibudev.com/api2/auth-token/ \
|
||||||
|
# -d "username=wibu@bip.com" \
|
||||||
|
# -d "password=Production_123"
|
||||||
|
|
||||||
curl -X POST http://localhost:3000/mcp/test-room \
|
# {
|
||||||
-H "Content-Type: application/json" \
|
# "relay_id": "44e8f253849ad910dc142247227c8ece8ec0f971",
|
||||||
-H "X-API-Key: super-secret-key" \
|
# "relay_addr": "127.0.0.1",
|
||||||
-d '{"event":"notice","data":{"msg":"hello world"}}'
|
# "relay_port": "80",
|
||||||
|
# "email": "wibu@bip.com",
|
||||||
|
# "token": "32c2de2d576f40f5a7db2be2e94818439f65ad73",
|
||||||
|
# "repo_id": "e27bc199-445a-4d55-939c-939df83efec8",
|
||||||
|
# "repo_name": "jenna-mcp",
|
||||||
|
# "repo_desc": "",
|
||||||
|
# "repo_size": 0,
|
||||||
|
# "repo_size_formatted": "0 bytes",
|
||||||
|
# "mtime": 1762327478,
|
||||||
|
# "mtime_relative": "<time datetime=\"2025-11-05T15:24:38\" is=\"relative-time\" title=\"Wed, 05 Nov 2025 15:24:38 +0800\" >Just now</time>",
|
||||||
|
# "encrypted": "",
|
||||||
|
# "enc_version": 0,
|
||||||
|
# "salt": "",
|
||||||
|
# "magic": "",
|
||||||
|
# "random_key": "",
|
||||||
|
# "repo_version": 1,
|
||||||
|
# "head_commit_id": "e107155ad1845933f5e57fc2b07e491765271432",
|
||||||
|
# "permission": "rw"
|
||||||
|
# }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# list repo
|
||||||
|
# curl -H "Authorization: Token $TOKEN" https://cld-dkr-makuro-seafile.wibudev.com/api2/repos/
|
||||||
|
|
||||||
|
URL="https://cld-dkr-makuro-seafile.wibudev.com"
|
||||||
|
TOKEN="fa49bf1774cad2ec89d2882ae2c6ac1f5d7df445"
|
||||||
|
REPO_ID="5a540fc1-a7fb-44af-884b-cb9a915b92e8"
|
||||||
|
|
||||||
|
list_repo(){
|
||||||
|
curl -H "Authorization: Token $TOKEN" "$URL/api2/repos/" | jq
|
||||||
|
}
|
||||||
|
|
||||||
|
create_dir(){
|
||||||
|
FOLDER_PATH="/test-folder"
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: Token $TOKEN" \
|
||||||
|
-d "operation=mkdir" \
|
||||||
|
"$URL/api2/repos/$REPO_ID/dir/?p=$FOLDER_PATH" | jq
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ping(){
|
||||||
|
echo "ping"
|
||||||
|
curl -H "Authorization: Token $TOKEN" \
|
||||||
|
"$URL/api2/auth/ping/"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_permission(){
|
||||||
|
echo "check_permission"
|
||||||
|
curl -s -H "Authorization: Token $TOKEN" \
|
||||||
|
"$URL/api2/repos/$REPO_ID/" | jq
|
||||||
|
}
|
||||||
|
|
||||||
|
check_dir(){
|
||||||
|
echo "check_dir"
|
||||||
|
curl -s -H "Authorization: Token $TOKEN" \
|
||||||
|
"$URL/api2/repos/$REPO_ID/dir/?p=/" | jq
|
||||||
|
}
|
||||||
|
|
||||||
|
check_test_folder(){
|
||||||
|
echo "check_test_folder"
|
||||||
|
curl -s -H "Authorization: Token $TOKEN" \
|
||||||
|
"$URL/api2/repos/$REPO_ID/dir/?p=/test-folder" | jq
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_file(){
|
||||||
|
echo "upload_file"
|
||||||
|
# 1. GET upload-link (ini pakai Authorization)
|
||||||
|
UPLOAD_URL=$(curl -s \
|
||||||
|
-H "Authorization: Token $TOKEN" \
|
||||||
|
"$URL/api2/repos/$REPO_ID/upload-link/" | tr -d '"')
|
||||||
|
|
||||||
|
echo "UPLOAD_URL = $UPLOAD_URL"
|
||||||
|
|
||||||
|
# 2. upload file — TIDAK boleh pakai -H Authorization
|
||||||
|
# token HARUS ditaruh di query param
|
||||||
|
curl -s -X POST \
|
||||||
|
-F file=@README.md \
|
||||||
|
-F "parent_dir=/" \
|
||||||
|
-F "relative_path=syarat-dokumen" \
|
||||||
|
"$UPLOAD_URL?token=$TOKEN"
|
||||||
|
}
|
||||||
|
|
||||||
|
ping
|
||||||
|
check_permission
|
||||||
|
check_dir
|
||||||
|
check_test_folder
|
||||||
|
upload_file
|
||||||
154
x.ts
154
x.ts
@@ -1,133 +1,39 @@
|
|||||||
/**
|
import fs from "fs";
|
||||||
* src/utils/swagger-to-mcp.ts
|
|
||||||
*
|
|
||||||
* Auto-converter: Swagger (OpenAPI) → MCP manifest (real-time)
|
|
||||||
*
|
|
||||||
* - Fetch swagger JSON dynamically from process.env.BUN_PUBLIC_BASE_URL + "/docs/json"
|
|
||||||
* - Generate MCP manifest for AI discovery (/.well-known/mcp.json)
|
|
||||||
* - Can be used as Bun CLI or integrated in Elysia route
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { writeFileSync } from "fs"
|
// 1️⃣ File yang mau diupload
|
||||||
|
const filePath = "image.png";
|
||||||
|
const apiUrl = "http://localhost:3000/api/pengaduan/upload-base64";
|
||||||
|
|
||||||
interface OpenAPI {
|
// 2️⃣ Baca file dan ubah ke base64
|
||||||
info: { title?: string; description?: string; version?: string }
|
const fileBuffer = fs.readFileSync(filePath);
|
||||||
paths: Record<string, any>
|
const base64Data = fileBuffer.toString("base64");
|
||||||
}
|
|
||||||
|
|
||||||
interface McpManifest {
|
// 3️⃣ Buat payload JSON
|
||||||
schema_version: string
|
const payload = {
|
||||||
name: string
|
data: base64Data,
|
||||||
description: string
|
mimetype: "image/png"
|
||||||
version?: string
|
};
|
||||||
endpoints: Record<string, string>
|
|
||||||
capabilities: Record<string, any>
|
|
||||||
contact?: { email?: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// 4️⃣ Kirim ke server pakai fetch
|
||||||
* Convert OpenAPI JSON to MCP manifest format
|
async function uploadBase64() {
|
||||||
*/
|
try {
|
||||||
async function convertOpenApiToMcp(baseUrl: string): Promise<McpManifest> {
|
const res = await fetch(apiUrl, {
|
||||||
const res = await fetch(`${baseUrl}/docs/json`)
|
method: "POST",
|
||||||
if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`)
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
const openapi: OpenAPI = await res.json()
|
if (!res.ok) {
|
||||||
|
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
|
||||||
const manifest: McpManifest = {
|
|
||||||
schema_version: "1.0",
|
|
||||||
name: openapi.info?.title ?? "MCP Server",
|
|
||||||
description: openapi.info?.description ?? "Auto-generated MCP manifest from Swagger",
|
|
||||||
version: openapi.info?.version ?? "0.0.0",
|
|
||||||
endpoints: {
|
|
||||||
openapi: `${baseUrl}/docs/json`,
|
|
||||||
mcp: `${baseUrl}/.well-known/mcp.json`
|
|
||||||
},
|
|
||||||
capabilities: {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [path, methods] of Object.entries(openapi.paths || {})) {
|
const result = await res.json();
|
||||||
for (const [method, def] of Object.entries<any>(methods)) {
|
console.log("✅ Upload sukses:", result);
|
||||||
const tags = def.tags || ["default"]
|
} catch (err) {
|
||||||
const tag = tags[0]
|
console.error("❌ Upload gagal:", err);
|
||||||
const operationId = def.operationId || `${method}_${path.replace(/[\/{}]/g, "_")}`
|
}
|
||||||
|
|
||||||
manifest.capabilities[tag] ??= {}
|
|
||||||
|
|
||||||
// Extract parameters and body schema
|
|
||||||
const params: Record<string, string> = {}
|
|
||||||
const required: string[] = []
|
|
||||||
|
|
||||||
if (Array.isArray(def.parameters)) {
|
|
||||||
for (const p of def.parameters) {
|
|
||||||
const type = p.schema?.type || "string"
|
|
||||||
params[p.name] = type
|
|
||||||
if (p.required) required.push(p.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bodySchema = def.requestBody?.content?.["application/json"]?.schema
|
|
||||||
if (bodySchema?.properties) {
|
|
||||||
for (const [key, prop] of Object.entries<any>(bodySchema.properties)) {
|
|
||||||
params[key] = prop.type || "string"
|
|
||||||
}
|
|
||||||
if (Array.isArray(bodySchema.required))
|
|
||||||
required.push(...bodySchema.required)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate example cURL
|
|
||||||
const sampleCurl = [
|
|
||||||
`curl -X ${method.toUpperCase()} ${baseUrl}${path}`,
|
|
||||||
Object.keys(params).length > 0
|
|
||||||
? ` -H 'Content-Type: application/json' -d '${JSON.stringify(
|
|
||||||
Object.fromEntries(Object.keys(params).map(k => [k, params[k] === "string" ? k : "value"]))
|
|
||||||
)}'`
|
|
||||||
: ""
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" \\\n")
|
|
||||||
|
|
||||||
manifest.capabilities[tag][operationId] = {
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
path,
|
|
||||||
summary: def.summary || def.description || "",
|
|
||||||
parameters: Object.keys(params).length > 0 ? params : undefined,
|
|
||||||
required: required.length > 0 ? required : undefined,
|
|
||||||
command: sampleCurl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return manifest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
uploadBase64();
|
||||||
* CLI entry
|
|
||||||
* bun run src/utils/swagger-to-mcp.ts
|
|
||||||
*/
|
|
||||||
if (import.meta.main) {
|
|
||||||
const baseUrl = process.env.BUN_PUBLIC_BASE_URL
|
|
||||||
if (!baseUrl) {
|
|
||||||
console.error("❌ Missing BUN_PUBLIC_BASE_URL environment variable.")
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
convertOpenApiToMcp(baseUrl)
|
|
||||||
.then(manifest => {
|
|
||||||
writeFileSync(".well-known/mcp.json", JSON.stringify(manifest, null, 2))
|
|
||||||
console.log("✅ Generated .well-known/mcp.json")
|
|
||||||
})
|
|
||||||
.catch(err => console.error("❌ Failed to convert Swagger → MCP:", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional: Elysia integration
|
|
||||||
* Automatically serve /.well-known/mcp.json
|
|
||||||
*/
|
|
||||||
// import Elysia from "elysia"
|
|
||||||
// new Elysia()
|
|
||||||
// .get("/.well-known/mcp.json", async () => {
|
|
||||||
// const baseUrl = process.env.BUN_PUBLIC_BASE_URL!
|
|
||||||
// return await convertOpenApiToMcp(baseUrl)
|
|
||||||
// })
|
|
||||||
// .listen(3000)
|
|
||||||
|
|||||||
176
xx.ts
176
xx.ts
@@ -1,127 +1,65 @@
|
|||||||
import { readdirSync, statSync, writeFileSync } from "fs";
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import _ from "lodash";
|
import { Elysia } from 'elysia'
|
||||||
import { basename, extname, join, relative } from "path";
|
import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||||
|
import bearer from '@elysiajs/bearer'
|
||||||
|
import { prisma } from '../lib/prisma'
|
||||||
|
|
||||||
const PAGES_DIR = join(process.cwd(), "src/pages");
|
// =========================================================
|
||||||
const OUTPUT_FILE = join(process.cwd(), "src/AppRoutes.tsx");
|
// JWT Secret Validation
|
||||||
|
// =========================================================
|
||||||
|
const secret = process.env.JWT_SECRET
|
||||||
|
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||||
|
|
||||||
// 🧩 Ubah nama file ke nama komponen (PascalCase)
|
// =========================================================
|
||||||
const toComponentName = (fileName: string) =>
|
// Auth Middleware Plugin
|
||||||
fileName
|
// =========================================================
|
||||||
.replace(/_/g, " ")
|
export default function apiAuth(app: Elysia) {
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase())
|
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||||
.replace(/\s/g, "");
|
return app
|
||||||
|
// Register Bearer and JWT plugins
|
||||||
|
.use(bearer()) // ✅ Extracts Bearer token automatically (case-insensitive)
|
||||||
|
.use(
|
||||||
|
jwt({
|
||||||
|
name: 'jwt',
|
||||||
|
secret,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
// 🧩 Ubah nama file ke path route
|
// Derive user from JWT or cookie
|
||||||
function toRoutePath(name: string): string {
|
.derive(async ({ bearer, cookie, jwt }) => {
|
||||||
if (name.toLowerCase() === "home") return "/";
|
// Normalize token type to string or undefined
|
||||||
if (name.toLowerCase() === "login") return "/login";
|
const token =
|
||||||
if (name.toLowerCase() === "notfound") return "/*";
|
(typeof bearer === 'string' ? bearer : undefined) ??
|
||||||
if (name.endsWith("_page")) return name.replace("_page", "").toLowerCase();
|
(typeof cookie?.token?.value === 'string' ? cookie.token.value : undefined)
|
||||||
if (name.startsWith("form_")) return name.replace("form_", "").toLowerCase();
|
|
||||||
return name.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧭 Scan folder pages secara rekursif
|
let user: Awaited<ReturnType<typeof prisma.user.findUnique>> | null = null
|
||||||
function scan(dir: string): any[] {
|
|
||||||
const items = readdirSync(dir);
|
|
||||||
const routes: any[] = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
if (token) {
|
||||||
const full = join(dir, item);
|
try {
|
||||||
const stat = statSync(full);
|
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (decoded?.sub && typeof decoded.sub === 'string') {
|
||||||
routes.push({
|
user = await prisma.user.findUnique({
|
||||||
name: item,
|
where: { id: decoded.sub },
|
||||||
path: item.toLowerCase(),
|
})
|
||||||
children: scan(full),
|
}
|
||||||
});
|
} catch (err) {
|
||||||
} else if (extname(item) === ".tsx") {
|
console.warn('[SERVER][apiAuth] Invalid token:', (err as Error).message)
|
||||||
routes.push({
|
|
||||||
name: basename(item, ".tsx"),
|
|
||||||
filePath: relative(join(process.cwd(), "src"), full).replace(/\\/g, "/"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return routes;
|
|
||||||
|
return { user }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Protect all routes by default
|
||||||
|
.onBeforeHandle(({ user, set, request }) => {
|
||||||
|
// Whitelist public routes if needed
|
||||||
|
const publicPaths = ['/auth/login', '/auth/register', '/public']
|
||||||
|
if (publicPaths.some((path) => request.url.includes(path))) return
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
set.status = 401
|
||||||
|
return { error: 'Unauthorized' }
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🏗️ Generate <Route> JSX dari struktur folder
|
|
||||||
function generateJSX(routes: any[], parentPath = ""): string {
|
|
||||||
let jsx = "";
|
|
||||||
|
|
||||||
for (const route of routes) {
|
|
||||||
if (route.children) {
|
|
||||||
// cari layout di folder
|
|
||||||
const layout = route.children.find((r: any) => r.name.endsWith("_layout"));
|
|
||||||
if (layout) {
|
|
||||||
const LayoutComponent = toComponentName(layout.name.replace("_layout", "Layout"));
|
|
||||||
const nested = route.children.filter((r: any) => r !== layout);
|
|
||||||
const nestedRoutes = generateJSX(nested, `${parentPath}/${route.path}`);
|
|
||||||
jsx += `
|
|
||||||
<Route path="${parentPath}/${route.path}" element={<${LayoutComponent} />}>
|
|
||||||
${nestedRoutes}
|
|
||||||
</Route>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
jsx += generateJSX(route.children, `${parentPath}/${route.path}`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const Component = toComponentName(route.name);
|
|
||||||
const routePath = toRoutePath(route.name);
|
|
||||||
|
|
||||||
// Hapus duplikasi segmen
|
|
||||||
const fullPath =
|
|
||||||
routePath.startsWith("/")
|
|
||||||
? routePath
|
|
||||||
: `${parentPath}/${_.kebabCase(routePath)}`.replace(/\/+/g, "/");
|
|
||||||
|
|
||||||
jsx += `<Route path="${fullPath}" element={<${Component} />} />\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return jsx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧾 Generate import otomatis
|
|
||||||
function generateImports(routes: any[]): string {
|
|
||||||
const imports = new Set<string>();
|
|
||||||
|
|
||||||
function collect(rs: any[]) {
|
|
||||||
for (const r of rs) {
|
|
||||||
if (r.children) collect(r.children);
|
|
||||||
else {
|
|
||||||
const Comp = toComponentName(r.name);
|
|
||||||
imports.add(`import ${Comp} from "./${r.filePath.replace(/\.tsx$/, "")}";`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collect(routes);
|
|
||||||
return Array.from(imports).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🧠 Main generator
|
|
||||||
const allRoutes = scan(PAGES_DIR);
|
|
||||||
const imports = generateImports(allRoutes);
|
|
||||||
const jsxRoutes = generateJSX(allRoutes);
|
|
||||||
|
|
||||||
const finalCode = `
|
|
||||||
// ⚡ Auto-generated by generateRoutes.ts — DO NOT EDIT MANUALLY
|
|
||||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
||||||
import ProtectedRoute from "./components/ProtectedRoute";
|
|
||||||
${imports}
|
|
||||||
|
|
||||||
export default function AppRoutes() {
|
|
||||||
return (
|
|
||||||
<BrowserRouter>
|
|
||||||
<Routes>
|
|
||||||
${jsxRoutes}
|
|
||||||
</Routes>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
writeFileSync(OUTPUT_FILE, finalCode);
|
|
||||||
console.log("✅ Routes generated successfully → src/AppRoutes.generated.tsx");
|
|
||||||
Bun.spawnSync(["bunx", "prettier", "--write", "src/**/*.tsx"])
|
|
||||||
|
|||||||
Reference in New Issue
Block a user