Compare commits
90 Commits
amalia/30-
...
amalia/19-
| Author | SHA1 | Date | |
|---|---|---|---|
| 236d6cfc72 | |||
| 482227a502 | |||
| fe52fb52c6 | |||
| 99247b7a44 | |||
| 73e247c87f | |||
| 4b914e1852 | |||
| dfc35e88d5 | |||
| 79660a766c | |||
| 083eb11bb0 | |||
| 3e09c934d4 | |||
| 282b9678b3 | |||
| ceed3e67c7 | |||
| 04b5d26507 | |||
| 327434b42e | |||
| c4e4aaffe7 | |||
| 51e323c264 | |||
| b5d3b2bd08 | |||
| 67c066990e | |||
| 6c6ee02cf0 | |||
|
|
a3c07ca255 | ||
|
|
76c425700a | ||
|
|
467dbfa296 | ||
|
|
719f92cbe8 | ||
|
|
2075d0fba1 | ||
|
|
7d98f9f61c | ||
|
|
1a5bd72237 | ||
|
|
85cb36289c | ||
| 8d535793b1 | |||
|
|
77cbb6062b | ||
|
|
e0bef23eab | ||
|
|
4ea72fb846 | ||
|
|
d380c859a4 | ||
|
|
78f2263c86 | ||
| 254d05f5ea | |||
| 039524d092 | |||
| cc293d3bad | |||
| 6e1d3ecb56 | |||
| 5101a21f54 | |||
| 503c3e330d | |||
| 001c3df47d | |||
| a4167cfc8b | |||
| 5b240b782a | |||
| 14e2d711b3 | |||
| dc3ae99c05 | |||
| 63c88161d3 | |||
| eacc8fc220 | |||
| 422ca5a2cc | |||
| 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 |
120
bun.lock
120
bun.lock
@@ -2,7 +2,7 @@
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun-react-template",
|
||||
"name": "jenna-mcp",
|
||||
"dependencies": {
|
||||
"@elysiajs/bearer": "^1.4.1",
|
||||
"@elysiajs/cors": "^1.4.0",
|
||||
@@ -21,7 +21,9 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"add": "^2.0.6",
|
||||
"elysia": "^1.4.13",
|
||||
"elysia": "^1.4.15",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.2.0",
|
||||
@@ -69,35 +71,35 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -137,12 +139,18 @@
|
||||
|
||||
"@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/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
|
||||
|
||||
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/uuid": ["@types/uuid@11.0.0", "", { "dependencies": { "uuid": "*" } }, "sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA=="],
|
||||
|
||||
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
|
||||
@@ -151,7 +159,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -173,6 +183,8 @@
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"biome": ["biome@0.3.3", "", { "dependencies": { "bluebird": "^3.4.1", "chalk": "^1.1.3", "commander": "^2.9.0", "editor": "^1.0.0", "fs-promise": "^0.5.0", "inquirer-promise": "0.0.3", "request-promise": "^3.0.0", "untildify": "^3.0.2", "user-home": "^2.0.0" }, "bin": { "biome": "./dist/index.js" } }, "sha512-4LXjrQYbn9iTXu9Y4SKT7ABzTV0WnLDHCVSd2fPUOKsy1gQ+E4xPFmlY1zcWexoi0j7fGHItlL6OWA2CZ/yYAQ=="],
|
||||
@@ -195,6 +207,8 @@
|
||||
|
||||
"camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="],
|
||||
|
||||
"canvg": ["canvg@3.0.11", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@types/raf": "^3.4.0", "core-js": "^3.8.3", "raf": "^3.4.1", "regenerator-runtime": "^0.13.7", "rgbcolor": "^1.0.1", "stackblur-canvas": "^2.0.0", "svg-pathdata": "^6.0.3" } }, "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA=="],
|
||||
|
||||
"caseless": ["caseless@0.12.0", "", {}, "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="],
|
||||
|
||||
"chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="],
|
||||
@@ -229,7 +243,7 @@
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
"core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="],
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="],
|
||||
|
||||
@@ -237,13 +251,15 @@
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -263,6 +279,8 @@
|
||||
|
||||
"dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||
|
||||
"dompurify": ["dompurify@3.3.0", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ=="],
|
||||
|
||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
@@ -277,7 +295,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -321,6 +339,10 @@
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-png": ["fast-png@6.4.0", "", { "dependencies": { "@types/pako": "^2.0.3", "iobuffer": "^5.3.2", "pako": "^2.1.0" } }, "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
|
||||
@@ -375,6 +397,8 @@
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"http-signature": ["http-signature@1.2.0", "", { "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", "sshpk": "^1.7.0" } }, "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ=="],
|
||||
@@ -391,6 +415,8 @@
|
||||
|
||||
"inquirer-promise": ["inquirer-promise@0.0.3", "", { "dependencies": { "earlgrey-runtime": ">=0.0.11", "inquirer": "^0.11.3" } }, "sha512-82CQX586JAV9GAgU9yXZsMDs+NorjA0nLhkfFx9+PReyOnuoHRbHrC1Z90sS95bFJI1Tm1gzMObuE0HabzkJpg=="],
|
||||
|
||||
"iobuffer": ["iobuffer@5.4.0", "", {}, "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="],
|
||||
@@ -413,12 +439,14 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"jsonfile": ["jsonfile@2.4.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw=="],
|
||||
|
||||
"jspdf": ["jspdf@3.0.3", "", { "dependencies": { "@babel/runtime": "^7.26.9", "fast-png": "^6.2.0", "fflate": "^0.8.1" }, "optionalDependencies": { "canvg": "^3.0.11", "core-js": "^3.6.0", "dompurify": "^3.2.4", "html2canvas": "^1.0.0-rc.5" } }, "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ=="],
|
||||
|
||||
"jsprim": ["jsprim@1.4.2", "", { "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", "json-schema": "0.4.0", "verror": "1.10.0" } }, "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw=="],
|
||||
|
||||
"jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="],
|
||||
@@ -481,7 +509,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
@@ -535,6 +565,8 @@
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"raf": ["raf@3.4.1", "", { "dependencies": { "performance-now": "^2.1.0" } }, "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.1", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.7.0", "unpipe": "1.0.0" } }, "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA=="],
|
||||
@@ -553,9 +585,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-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=="],
|
||||
|
||||
@@ -567,14 +599,18 @@
|
||||
|
||||
"readline2": ["readline2@1.0.1", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "mute-stream": "0.0.5" } }, "sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.9.6", "", {}, "sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw=="],
|
||||
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||
|
||||
"request": ["request@2.88.2", "", { "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", "caseless": "~0.12.0", "combined-stream": "~1.0.6", "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", "mime-types": "~2.1.19", "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" } }, "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"rgbcolor": ["rgbcolor@1.0.1", "", {}, "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw=="],
|
||||
|
||||
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
@@ -593,7 +629,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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -613,7 +649,9 @@
|
||||
|
||||
"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=="],
|
||||
"stackblur-canvas": ["stackblur-canvas@2.7.0", "", {}, "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -625,9 +663,13 @@
|
||||
|
||||
"supports-color": ["supports-color@2.0.0", "", {}, "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g=="],
|
||||
|
||||
"svg-pathdata": ["svg-pathdata@6.0.3", "", {}, "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
|
||||
|
||||
"thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="],
|
||||
|
||||
@@ -635,7 +677,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -657,7 +699,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -681,9 +723,11 @@
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -705,12 +749,20 @@
|
||||
|
||||
"c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"earlgrey-runtime/core-js": ["core-js@2.6.12", "", {}, "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ=="],
|
||||
|
||||
"earlgrey-runtime/regenerator-runtime": ["regenerator-runtime@0.9.6", "", {}, "sha512-D0Y/JJ4VhusyMOd/o25a3jdUqN/bC85EFsaoL9Oqmy/O4efCh+xhp7yj2EEOsj974qvMkcW8AwUzJ1jB/MbxCw=="],
|
||||
|
||||
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
@@ -729,6 +781,8 @@
|
||||
|
||||
"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=="],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"add": "^2.0.6",
|
||||
"elysia": "^1.4.13",
|
||||
"elysia": "^1.4.15",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^19.2.0",
|
||||
|
||||
@@ -142,6 +142,7 @@ model HistoryPelayanan {
|
||||
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())
|
||||
@@ -183,8 +184,8 @@ model SuratPelayanan {
|
||||
Warga Warga @relation(fields: [idWarga], references: [id])
|
||||
idWarga String
|
||||
noSurat String
|
||||
dateExpired DateTime @db.Date
|
||||
status Int
|
||||
dateExpired DateTime? @db.Date
|
||||
status Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -192,7 +193,7 @@ model SuratPelayanan {
|
||||
|
||||
model Configuration {
|
||||
id String @id @default(cuid())
|
||||
category String
|
||||
name String
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
|
||||
import { confDesa } from "@/lib/configurationDesa";
|
||||
import { prisma } from "@/server/lib/prisma";
|
||||
|
||||
const category = [
|
||||
@@ -51,7 +52,6 @@ const user = [
|
||||
|
||||
(async () => {
|
||||
for (const r of role) {
|
||||
console.log(`Seeding role ${r.name}`)
|
||||
await prisma.role.upsert({
|
||||
where: { id: r.id },
|
||||
create: r,
|
||||
@@ -81,7 +81,7 @@ const user = [
|
||||
console.log(`✅ Category ${c.name} seeded successfully`)
|
||||
}
|
||||
|
||||
for (const cp of categoryPelayananSurat){
|
||||
for (const cp of categoryPelayananSurat) {
|
||||
await prisma.categoryPelayanan.upsert({
|
||||
where: { id: cp.id },
|
||||
create: cp,
|
||||
@@ -91,6 +91,15 @@ const user = [
|
||||
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`)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "@mantine/core/styles.css";
|
||||
import "@mantine/notifications/styles.css";
|
||||
import "@mantine/dates/styles.css";
|
||||
import { Notifications } from "@mantine/notifications";
|
||||
import "@mantine/notifications/styles.css";
|
||||
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import AppRoutes from "./AppRoutes";
|
||||
|
||||
@@ -17,8 +17,15 @@ import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_ketera
|
||||
import Home from "./pages/Home";
|
||||
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||
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 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 DirPage from "./pages/dir/dir_page";
|
||||
import NotFound from "./pages/NotFound";
|
||||
@@ -92,10 +99,38 @@ export default function AppRoutes() {
|
||||
path="/scr/dashboard/dashboard-home"
|
||||
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
|
||||
path="/scr/dashboard/apikey/apikey"
|
||||
element={<ApikeyPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/scr/dashboard/setting/detail-setting"
|
||||
element={<DetailSettingPage />}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="/dir/dir" element={<DirPage />} />
|
||||
|
||||
@@ -19,7 +19,14 @@ const clientRoutes = {
|
||||
"/scr/dashboard": "/scr/dashboard",
|
||||
"/scr/dashboard/credential/credential": "/scr/dashboard/credential/credential",
|
||||
"/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/setting/detail-setting": "/scr/dashboard/setting/detail-setting",
|
||||
"/dir/dir": "/dir/dir",
|
||||
"/*": "/*"
|
||||
} as const;
|
||||
|
||||
171
src/components/DesaSetting.tsx
Normal file
171
src/components/DesaSetting.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function DesaSetting() {
|
||||
const [btnDisable, setBtnDisable] = useState(false);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api["configuration-desa"].list.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
value: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your settings have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit configuration",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit configuration",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; value: string; name: string };
|
||||
}) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({ kat, value }: { kat: "value"; value: string }) {
|
||||
if (value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
|
||||
if (kat === "value") {
|
||||
setDataEdit({ ...dataEdit, value: value });
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.value.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input
|
||||
value={dataEdit.value}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "value", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleEdit}
|
||||
disabled={btnDisable}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Pengaturan Desa
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Value</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>{v.value}</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label="Edit Setting">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
612
src/components/KategoriPelayananSurat.tsx
Normal file
612
src/components/KategoriPelayananSurat.tsx
Normal file
@@ -0,0 +1,612 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
List,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
TagsInput,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPelayananSurat() {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [openedDetail, { open: openDetail, close: closeDetail }] =
|
||||
useDisclosure(false);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.pelayanan.category.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const [dataChoose, setDataChoose] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const cleanedDataText = dataTambah.dataText
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v !== "");
|
||||
const cleanedSyarat = dataTambah.syaratDokumen
|
||||
.map((item) => ({
|
||||
name: item.name.trim(),
|
||||
desc: item.desc.trim(),
|
||||
}))
|
||||
.filter((item) => item.name !== "" && item.desc !== "");
|
||||
|
||||
const cleanedTambah = {
|
||||
name: dataTambah.name.trim(),
|
||||
syaratDokumen: cleanedSyarat,
|
||||
dataText: cleanedDataText,
|
||||
};
|
||||
const res =
|
||||
await apiFetch.api.pelayanan.category.create.post(cleanedTambah);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const cleanedDataText = dataChoose.dataText
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => v !== "");
|
||||
const cleanedSyarat = dataChoose.syaratDokumen
|
||||
.map((item) => ({
|
||||
name: item.name.trim(),
|
||||
desc: item.desc.trim(),
|
||||
}))
|
||||
.filter((item) => item.name !== "" && item.desc !== "");
|
||||
|
||||
const res = await apiFetch.api.pelayanan.category.update.post({
|
||||
id: dataChoose.id,
|
||||
name: dataChoose.name,
|
||||
syaratDokumen: cleanedSyarat,
|
||||
dataText: cleanedDataText,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pelayanan.category.delete.post({
|
||||
id: dataDelete,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeDelete();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been deleted",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddSyarat() {
|
||||
setDataChoose({
|
||||
...dataChoose,
|
||||
syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }],
|
||||
});
|
||||
}
|
||||
|
||||
function handleDeleteSyarat(index: number) {
|
||||
setDataChoose({
|
||||
...dataChoose,
|
||||
syaratDokumen: dataChoose.syaratDokumen.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditSyarat(
|
||||
index: number,
|
||||
data: { name: string; desc: string },
|
||||
) {
|
||||
setDataChoose({
|
||||
...dataChoose,
|
||||
syaratDokumen: dataChoose.syaratDokumen.map((v, i) =>
|
||||
i === index ? data : v,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
size="xl"
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Input.Wrapper label="Kategori">
|
||||
<Input
|
||||
value={dataChoose.name}
|
||||
onChange={(e) =>
|
||||
setDataChoose({ ...dataChoose, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<TagsInput
|
||||
label="Data Pelengkap"
|
||||
placeholder="Tambah data pelengkap"
|
||||
splitChars={[","]}
|
||||
value={dataChoose.dataText}
|
||||
onChange={(value) =>
|
||||
setDataChoose({ ...dataChoose, dataText: value })
|
||||
}
|
||||
/>
|
||||
<Flex direction={"column"} gap={"md"}>
|
||||
<Group>
|
||||
<Text size="sm" c={"white"}>
|
||||
Syarat dokumen
|
||||
</Text>
|
||||
<Tooltip label="Tambah Syarat Dokumen">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="blue"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={handleAddSyarat}
|
||||
>
|
||||
<IconPlus size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{dataChoose?.syaratDokumen?.map((v: any, i: number) => (
|
||||
<Grid
|
||||
key={i}
|
||||
style={{
|
||||
borderBottom: "1px solid gray",
|
||||
paddingBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<Grid.Col
|
||||
span={1}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Delete Syarat Dokumen">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
handleDeleteSyarat(i);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={5}>
|
||||
<Input.Wrapper label="Nama">
|
||||
<Input
|
||||
value={v.name}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
name: e.target.value,
|
||||
desc: v.desc,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Input.Wrapper label="Deskripsi">
|
||||
<Input
|
||||
value={v.desc}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
name: v.name,
|
||||
desc: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleEdit} loading={btnLoading}>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size={"lg"}
|
||||
>
|
||||
<Stack gap="lg">
|
||||
<Input.Wrapper label="Tambah Kategori Pelayanan Surat">
|
||||
<Input
|
||||
value={dataTambah.name}
|
||||
onChange={(e) =>
|
||||
setDataTambah({ ...dataTambah, name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<TagsInput
|
||||
label="Data Pelengkap"
|
||||
placeholder="Tambah data pelengkap"
|
||||
splitChars={[","]}
|
||||
value={dataTambah.dataText}
|
||||
onChange={(value) =>
|
||||
setDataTambah({ ...dataTambah, dataText: value })
|
||||
}
|
||||
/>
|
||||
<Flex direction={"column"} gap={"md"}>
|
||||
<Group>
|
||||
<Text size="sm" c={"white"}>
|
||||
Syarat dokumen
|
||||
</Text>
|
||||
<Tooltip label="Tambah Syarat Dokumen">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="blue"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataTambah({
|
||||
...dataTambah,
|
||||
syaratDokumen: [
|
||||
...dataTambah.syaratDokumen,
|
||||
{ name: "", desc: "" },
|
||||
],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconPlus size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{dataTambah?.syaratDokumen?.map((v: any, index: number) => (
|
||||
<Grid
|
||||
key={index}
|
||||
style={{
|
||||
borderBottom: "1px solid gray",
|
||||
paddingBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<Grid.Col
|
||||
span={1}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Delete Syarat Dokumen">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataTambah({
|
||||
...dataTambah,
|
||||
syaratDokumen: dataTambah.syaratDokumen.filter(
|
||||
(v: any, i: number) => i !== index,
|
||||
),
|
||||
});
|
||||
}}
|
||||
disabled={dataTambah?.syaratDokumen?.length === 1}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={5}>
|
||||
<Input.Wrapper label="Nama">
|
||||
<Input
|
||||
value={dataTambah?.syaratDokumen[index]?.name}
|
||||
onChange={(e) =>
|
||||
setDataTambah({
|
||||
...dataTambah,
|
||||
syaratDokumen: dataTambah.syaratDokumen.map(
|
||||
(v: any, i: number) =>
|
||||
i === index ? { ...v, name: e.target.value } : v,
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Input.Wrapper label="Deskripsi">
|
||||
<Input
|
||||
value={dataTambah?.syaratDokumen[index]?.desc}
|
||||
onChange={(e) =>
|
||||
setDataTambah({
|
||||
...dataTambah,
|
||||
syaratDokumen: dataTambah.syaratDokumen.map(
|
||||
(v: any, i: number) =>
|
||||
i === index ? { ...v, desc: e.target.value } : v,
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
))}
|
||||
</Flex>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleCreate}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
opened={openedDelete}
|
||||
onClose={closeDelete}
|
||||
title={"Delete Kategori Pelayanan Surat"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="md" color="gray.6">
|
||||
Apakah anda yakin ingin menghapus kategori ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeDelete}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Detail */}
|
||||
<Modal
|
||||
opened={openedDetail}
|
||||
onClose={closeDetail}
|
||||
title={"Detail Kategori"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size={"lg"}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Flex direction={"column"}>
|
||||
<Text size="sm" color="white">
|
||||
Kategori
|
||||
</Text>
|
||||
<Text size="md">{dataChoose?.name ?? ""}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"}>
|
||||
<Text size="sm" color="white">
|
||||
Syarat Dokumen
|
||||
</Text>
|
||||
<List>
|
||||
{dataChoose?.syaratDokumen?.map((v: any) => (
|
||||
<List.Item key={v.id}>{v.desc}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Flex>
|
||||
<Flex direction={"column"}>
|
||||
<Text size="sm" color="white">
|
||||
Data Pelengkap
|
||||
</Text>
|
||||
<List>
|
||||
{dataChoose?.dataText?.map((v: any) => (
|
||||
<List.Item key={v.id}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Table */}
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pelayanan Surat
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Kategori</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="View Detail">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="green"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataChoose(v);
|
||||
openDetail();
|
||||
}}
|
||||
>
|
||||
<IconEye size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Edit Kategori">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataChoose(v);
|
||||
open();
|
||||
}}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
355
src/components/KategoriPengaduan.tsx
Normal file
355
src/components/KategoriPengaduan.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPengaduan() {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.pengaduan.category.get(),
|
||||
);
|
||||
const list = data?.data?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState("");
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.create.post({
|
||||
name: dataTambah,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah("");
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; value: string; name: string };
|
||||
}) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({
|
||||
kat,
|
||||
value,
|
||||
aksi,
|
||||
}: {
|
||||
kat: "name";
|
||||
value: string;
|
||||
aksi: "edit" | "tambah";
|
||||
}) {
|
||||
if (value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
|
||||
if (kat === "name") {
|
||||
if (aksi === "edit") {
|
||||
setDataEdit({ ...dataEdit, name: value });
|
||||
} else {
|
||||
setDataTambah(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.delete.post({
|
||||
id: dataDelete,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeDelete();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been deleted",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataTambah.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataTambah]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Edit Kategori">
|
||||
<Input
|
||||
value={dataEdit.name}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "edit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleEdit}
|
||||
disabled={btnDisable}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Tambah Kategori">
|
||||
<Input
|
||||
value={dataTambah}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleCreate}
|
||||
disabled={btnDisable}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
opened={openedDelete}
|
||||
onClose={closeDelete}
|
||||
title={"Delete"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="md" color="gray.6">
|
||||
Apakah anda yakin ingin menghapus kategori ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeDelete}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pengaduan
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Kategori</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit Kategori">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
126
src/components/ModalSurat.tsx
Normal file
126
src/components/ModalSurat.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { ActionIcon, Flex, Modal } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconDownload, IconX } from "@tabler/icons-react";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
import { useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
import SKKelahiran from "./surat/SKKelahiran";
|
||||
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
|
||||
import SKPenghasilan from "./surat/SKPenghasilan";
|
||||
import SKTidakMampu from "./surat/SKTidakMampu";
|
||||
import SKUsaha from "./surat/SKUsaha";
|
||||
import SKYatim from "./surat/SKYatimPiatu";
|
||||
|
||||
export default function ModalSurat({ open, onClose, surat }: { open: boolean, onClose: () => void, surat: string }) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
color: "#000",
|
||||
fontSize: "14px",
|
||||
fontFamily: "Times New Roman",
|
||||
};
|
||||
const hiddenRef = useRef<any>(null);
|
||||
const { data, mutate, isLoading } = useSWR("surat", () =>
|
||||
apiFetch.api.surat.detail.get({
|
||||
query: {
|
||||
id: surat,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
const downloadPDF = async () => {
|
||||
const element = hiddenRef.current;
|
||||
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL("image/jpeg", 1.0);
|
||||
|
||||
const pdf = new jsPDF("p", "mm", "a4");
|
||||
const pageWidth = 210; // A4 width mm
|
||||
const pageHeight = 297; // A4 height mm
|
||||
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
|
||||
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
||||
|
||||
pdf.save("surat-keterangan-usaha.pdf");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onClose()}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
styles={{
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px 16px",
|
||||
},
|
||||
title: {
|
||||
width: "100%",
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
Preview Surat
|
||||
</div>
|
||||
|
||||
<Flex gap={8}>
|
||||
<ActionIcon size={32} variant="default">
|
||||
<IconDownload size={20} onClick={downloadPDF} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon size={32} variant="default" onClick={onClose}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{
|
||||
data && data.data
|
||||
? data.data.surat.idCategory == "skusaha"
|
||||
? <SKUsaha data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelahiran"
|
||||
? <SKKelahiran data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelakuanbaik"
|
||||
? <SKKelakuanBaik data={data.data} />
|
||||
: data.data.surat.idCategory == "skpenghasilan"
|
||||
? <SKPenghasilan data={data.data} />
|
||||
: data.data.surat.idCategory == "sktidakmampu"
|
||||
? <SKTidakMampu data={data.data} />
|
||||
: data.data.surat.idCategory == "skyatimpiatu"
|
||||
? <SKYatim data={data.data} />
|
||||
: <></>
|
||||
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
246
src/components/ProfileUser.tsx
Normal file
246
src/components/ProfileUser.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function ProfileUser() {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [openedPassword, setOpenedPassword] = useState(false);
|
||||
const [pwdBaru, setPwdBaru] = useState("");
|
||||
const [host, setHost] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
roleId: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
phone: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost({
|
||||
id: data?.user?.id ?? "",
|
||||
name: data?.user?.name ?? "",
|
||||
phone: data?.user?.phone ?? "",
|
||||
roleId: data?.user?.roleId ?? "",
|
||||
email: data?.user?.email ?? "",
|
||||
});
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
function onValidation({
|
||||
kat,
|
||||
value,
|
||||
}: {
|
||||
kat: "name" | "email" | "phone";
|
||||
value: string;
|
||||
}) {
|
||||
if (value.length < 1) {
|
||||
setError({ ...error, [kat]: true });
|
||||
} else {
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
setHost({ ...host, [kat]: value });
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
try {
|
||||
const res = await apiFetch.api.user.update.post(host);
|
||||
if (res.status === 200) {
|
||||
setOpened(false);
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your profile have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update profile",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update profile",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdatePassword() {
|
||||
try {
|
||||
const res = await apiFetch.api.user["update-password"].post({
|
||||
password: pwdBaru,
|
||||
id: host.id,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
setPwdBaru("");
|
||||
setOpenedPassword(false);
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your password have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update password",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update password",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Profile Pengguna
|
||||
</Title>
|
||||
<Group gap="md">
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Nama" description="" error="">
|
||||
<Input value={host?.name ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Phone" description="" error="">
|
||||
<Input value={host?.phone ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
<Group gap="xl" grow>
|
||||
<Input.Wrapper label="Email" description="" error="">
|
||||
<Input value={host?.email ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper label="Role" description="" error="">
|
||||
<Input value={host?.roleId ?? ""} readOnly />
|
||||
</Input.Wrapper>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title={"Edit Profile"}
|
||||
size={"lg"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap={"md"}>
|
||||
<Input.Wrapper
|
||||
label="Nama"
|
||||
description=""
|
||||
error={error.name ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={host?.name ?? ""}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "name", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Phone"
|
||||
description=""
|
||||
error={error.phone ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={host?.phone ?? ""}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "phone", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Email"
|
||||
description=""
|
||||
error={error.email ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={host?.email ?? ""}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "email", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group grow>
|
||||
<Button variant="light" onClick={() => setOpened(false)}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => handleUpdate()}
|
||||
disabled={error.name || error.phone || error.email}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
opened={openedPassword}
|
||||
onClose={() => setOpenedPassword(false)}
|
||||
title={"Ubah Password"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap={"md"}>
|
||||
<Input.Wrapper label="Password Baru" description="">
|
||||
<Input
|
||||
value={pwdBaru}
|
||||
onChange={(e) => setPwdBaru(e.target.value)}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group grow>
|
||||
<Button variant="light" onClick={() => setOpenedPassword(false)}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => handleUpdatePassword()}
|
||||
disabled={pwdBaru.length < 1}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
466
src/components/UserSetting.tsx
Normal file
466
src/components/UserSetting.tsx
Normal file
@@ -0,0 +1,466 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function UserSetting() {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const {
|
||||
data: dataRole,
|
||||
mutate: mutateRole,
|
||||
isLoading: isLoadingRole,
|
||||
} = useSWR("user-role", () => apiFetch.api.user.role.get());
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("user-list", () =>
|
||||
apiFetch.api.user.list.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const listRole = dataRole?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
password: "",
|
||||
phone: "",
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
email: false,
|
||||
roleId: false,
|
||||
password: false,
|
||||
phone: false,
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user.create.post(dataTambah);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
email: "",
|
||||
roleId: "",
|
||||
password: "",
|
||||
phone: "",
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your user have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create user ",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create user",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user.delete.post({ id: dataDelete });
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeDelete();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your user have been deleted",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete user",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete user",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: {
|
||||
id: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
roleId: string;
|
||||
};
|
||||
}) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({
|
||||
kat,
|
||||
value,
|
||||
aksi,
|
||||
}: {
|
||||
kat: "name" | "email" | "roleId" | "password" | "phone";
|
||||
value: string | null;
|
||||
aksi: "edit" | "tambah";
|
||||
}) {
|
||||
if (value == null || value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
setError({ ...error, [kat]: true });
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
if (aksi === "edit") {
|
||||
setDataEdit({ ...dataEdit, [kat]: value });
|
||||
} else {
|
||||
setDataTambah({ ...dataTambah, [kat]: value });
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
centered
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Edit Kategori">
|
||||
<Input
|
||||
value={dataEdit.name}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "edit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleEdit}
|
||||
disabled={btnDisable}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper
|
||||
label="Nama"
|
||||
description=""
|
||||
error={error.name ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={dataTambah.name}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Select
|
||||
label="Role"
|
||||
placeholder="Pilih Role"
|
||||
data={listRole.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: r.name,
|
||||
}))}
|
||||
value={dataTambah.roleId || null}
|
||||
error={error.roleId ? "Field is required" : ""}
|
||||
onChange={(_value, option) => {
|
||||
onValidation({
|
||||
kat: "roleId",
|
||||
value: option?.value,
|
||||
aksi: "tambah",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input.Wrapper label="Phone" description="">
|
||||
<Input
|
||||
value={dataTambah.phone}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "phone",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Email"
|
||||
description=""
|
||||
error={error.email ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={dataTambah.email}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "email",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Password"
|
||||
description=""
|
||||
error={error.password ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={dataTambah.password}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "password",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
btnDisable ||
|
||||
dataTambah.name.length < 1 ||
|
||||
dataTambah.email.length < 1 ||
|
||||
dataTambah.password.length < 1 ||
|
||||
dataTambah.roleId.length < 1 ||
|
||||
dataTambah.phone.length < 1
|
||||
}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
opened={openedDelete}
|
||||
onClose={closeDelete}
|
||||
title={"Delete"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="md" color="gray.6">
|
||||
Apakah anda yakin ingin menghapus user ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeDelete}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar User
|
||||
</Title>
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Nama</Table.Th>
|
||||
<Table.Th>Telepon</Table.Th>
|
||||
<Table.Th>Email</Table.Th>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list.length > 0 ? (
|
||||
list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>{v.phone}</Table.Td>
|
||||
<Table.Td>{v.email}</Table.Td>
|
||||
<Table.Td>{v.roleId}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit User">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete User">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
Data User Tidak Ditemukan
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
src/components/notificationGlobal.ts
Normal file
38
src/components/notificationGlobal.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { showNotification } from "@mantine/notifications";
|
||||
|
||||
export default function notification({ title, message, type }: { title: string, message: string, type: "success" | "error" | "warning" | "info" }) {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "green",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "error":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "red",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "warning":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "orange",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
case "info":
|
||||
return showNotification({
|
||||
title,
|
||||
message,
|
||||
color: "blue",
|
||||
autoClose: 3000,
|
||||
})
|
||||
break;
|
||||
}
|
||||
}
|
||||
113
src/components/surat/SKKelahiran.tsx
Normal file
113
src/components/surat/SKKelahiran.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKKelahiran({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.2" }}>
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b>PEMERINTAH KABUPATEN/KOTA {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
</div>
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN KELAHIRAN</u></b><br />
|
||||
Nomor : {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div>
|
||||
Yang bertanda tangan di bawah ini, {data.setting.perbekelJabatan}
|
||||
{` ${data.setting.desaNama}, Kecamatan ${data.setting.desaKecamatan}, Kabupaten/Kota ${data.setting.desaKabupaten}`}
|
||||
, dengan ini menerangkan bahwa:
|
||||
</div>
|
||||
|
||||
{/* DATA KELAHIRAN ANAK */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Telah lahir seorang anak pada:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Tanggal Lahir</td><td>:</td><td>{getValue("tanggal lahir anak")}</td></tr>
|
||||
<tr><td>Pukul</td><td>:</td><td>{getValue("pukul lahir anak")}</td></tr>
|
||||
<tr><td>Tempat Kelahiran</td><td>:</td><td>{getValue("tempat lahir anak")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin anak")}</td></tr>
|
||||
<tr><td>Anak ke</td><td>:</td><td>{getValue("anak ke")}</td></tr>
|
||||
<tr><td>Nama Anak</td><td>:</td><td>{getValue("nama anak")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA IBU */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dari seorang ibu bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Lengkap Ibu</td><td>:</td><td>{getValue("nama ibu")}</td></tr>
|
||||
<tr><td>NIK</td><td>:</td><td>{getValue("nik ibu")}</td></tr>
|
||||
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ibu")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ibu")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ibu")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA AYAH */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan seorang ayah bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Lengkap Ayah</td><td>:</td><td>{getValue("nama ayah")}</td></tr>
|
||||
<tr><td>NIK</td><td>:</td><td>{getValue("nik ayah")}</td></tr>
|
||||
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ayah")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ayah")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ayah")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA PELAPOR */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan laporan dari:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Pelapor</td><td>:</td><td>{getValue("nama pelapor")}</td></tr>
|
||||
<tr><td>Hubungan dengan Anak</td><td>:</td><td>{getValue("hubungan pelapor")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat pelapor")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PENUTUP */}
|
||||
<div style={{ marginTop: "20px", textAlign: "justify" }}>
|
||||
Demikian Surat Keterangan Kelahiran ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TEMPAT TANGGAL */}
|
||||
<table style={{ width: "100%", marginTop: "20px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Dikeluarkan di</td><td>:</td><td>{data.setting.desaNama}</td></tr>
|
||||
<tr><td>Pada tanggal</td><td>:</td><td>{data.surat.createdAt}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ marginTop: "40px", width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
114
src/components/surat/SKKelakuanBaik.tsx
Normal file
114
src/components/surat/SKKelakuanBaik.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKKelakuanBaik({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "30px" }}>
|
||||
<b style={{ fontSize: "18px" }}>SURAT KETERANGAN KELAKUAN BAIK</b><br />
|
||||
(PENGANTAR SKCK)<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini menerangkan dengan sebenarnya bahwa:
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS PENDUDUK */}
|
||||
<table style={{ width: "100%", marginBottom: "15px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama lengkap</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tgl Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tempat tanggal lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ISI */}
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Adalah benar penduduk yang berdomisili di wilayah kami dan selama tinggal di lingkungan
|
||||
Desa {data.setting.desaNama}, berkelakuan baik, tidak pernah terlibat perbuatan melanggar hukum,
|
||||
serta dikenal sopan dan aktif dalam kegiatan kemasyarakatan.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat keterangan ini diberikan sebagai pengantar permohonan penerbitan Surat Keterangan
|
||||
Catatan Kepolisian (SKCK) ke Polsek/Polres {getValue("polsek")}.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat ini berlaku selama 6 (enam) bulan sejak tanggal diterbitkan, kecuali terdapat perubahan
|
||||
data yang mendasar.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL */}
|
||||
<table style={{ width: "100%", marginBottom: "40px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>:</td>
|
||||
<td>{data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama}<br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
112
src/components/surat/SKPenghasilan.tsx
Normal file
112
src/components/surat/SKPenghasilan.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKPenghasilan({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN PENGHASILAN</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Nama</td><td>:</td><td>{getValue("nama")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
|
||||
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PENGHASILAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan keterangan yang bersangkutan, orang tersebut memiliki penghasilan rata-rata:
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Penghasilan</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>
|
||||
Rp {getValue("penghasilan")}
|
||||
{" "}
|
||||
({getValue("penghasilan terbilang")}) per bulan
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* KEPERLUAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan permohonan")}</b>.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL & TANDA TANGAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
81
src/components/surat/SKTidakMampu.tsx
Normal file
81
src/components/surat/SKTidakMampu.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKTidakMampu({ data }: { data: any }) {
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TIDAK MAMPU</b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* ISI */}
|
||||
<div>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
Yang bertanda tangan dibawah ini, saya
|
||||
</div>
|
||||
|
||||
{/* DATA PEJABAT */}
|
||||
<div style={{ marginLeft: "20px" }}>
|
||||
|
||||
<Row label="Nama" value={data.setting.perbekelNama} />
|
||||
<Row label="Alamat" value={data.setting.desaAlamat} />
|
||||
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
{/* DATA WARGA */}
|
||||
<div style={{ marginLeft: "20px" }}>
|
||||
|
||||
<Row label="Nama" value={getValue("nama")} />
|
||||
<Row label="Tempat Tgl Lahir" value={getValue("tempat tanggal lahir")} />
|
||||
<Row label="Alamat" value={getValue("alamat")} />
|
||||
<Row label="NIK" value={getValue("nik")} />
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan termasuk keluarga tidak mampu.
|
||||
Surat keterangan ini dipergunakan untuk
|
||||
<b>{getValue("alasan permohonan")}.</b>
|
||||
</p>
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Demikian surat keterangan ini kami buat dengan sebenar-benarnya untuk dapat dipergunakan
|
||||
sebagaimana mestinya.
|
||||
</p>
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{data.setting.desaNama}, {data.surat.createdAt} <br /><br /><br />
|
||||
|
||||
<b><u>{data.setting.perbekelNama}</u></b><br />
|
||||
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string, value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
118
src/components/surat/SKUsaha.tsx
Normal file
118
src/components/surat/SKUsaha.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKUsaha({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN USAHA</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan dengan sesungguhnya bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
|
||||
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
|
||||
<tr><td>Warga Negara</td><td>:</td><td>{getValue("negara")}</td></tr>
|
||||
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
|
||||
<tr><td>Status</td><td>:</td><td>{getValue("status perkawinan")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DOMISILI */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Bahwa orang tersebut di atas benar-benar penduduk:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Desa / Kelurahan</td><td style={{ width: "10px" }}>:</td><td>{data.setting.desaNama}</td></tr>
|
||||
<tr><td>Kecamatan</td><td>:</td><td>{data.setting.desaKecamatan}</td></tr>
|
||||
<tr><td>Kabupaten</td><td>:</td><td>{data.setting.desaKabupaten}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* USAHA */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan yang bersangkutan benar memiliki usaha:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Jenis Usaha</td><td style={{ width: "10px" }}>:</td><td>{getValue("jenis usaha")}</td></tr>
|
||||
<tr><td>Alamat Usaha</td><td>:</td><td>{getValue("alamat usaha")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
|
||||
<br /><br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
163
src/components/surat/SKYatimPiatu.tsx
Normal file
163
src/components/surat/SKYatimPiatu.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export default function SKYatim({ data }: { data: any }) {
|
||||
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}. Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "center", marginTop: "15px" }}>
|
||||
<b><u>SURAT KETERANGAN YATIM / PIATU / YATIM PIATU</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN PENANDATANGAN */}
|
||||
<div>Yang bertanda tangan di bawah ini:</div>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>: {data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat Kantor</td>
|
||||
<td>: {data.setting.desaAlamat}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN IDENTITAS ANAK */}
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>: {getValue("tempat tanggal lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>: {getValue("jenis kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>: {getValue("alamat")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>: {getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>: {getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* KETERANGAN ORANG TUA */}
|
||||
<div>
|
||||
Benar bahwa yang bersangkutan adalah <b>anak (Yatim / Piatu / Yatim Piatu)</b>,
|
||||
dengan keterangan sebagai berikut:
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div><b>1. Nama Ayah</b></div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ayah</td>
|
||||
<td>: {getValue("nama ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status ayah")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div><b>2. Nama Ibu</b></div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ibu</td>
|
||||
<td>: {getValue("nama ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status ibu")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Dengan demikian, berdasarkan keterangan pihak keluarga dan data di Kantor Desa,
|
||||
maka benar bahwa yang bersangkutan adalah
|
||||
<b> anak (Yatim / Piatu / Yatim Piatu).</b>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* TANGGAL & TEMPAT */}
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td>: {data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>: {data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* TTD */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
import cors from "@elysiajs/cors";
|
||||
import Swagger from "@elysiajs/swagger";
|
||||
import Elysia from "elysia";
|
||||
import html from "./index.html";
|
||||
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 Auth from "./server/routes/auth_route";
|
||||
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
|
||||
import CredentialRoute from "./server/routes/credential_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 AduanRoute from "./server/routes/aduan_route";
|
||||
import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import TestPengaduanRoute from "./server/routes/test_pengaduan";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import WargaRoute from "./server/routes/warga_route";
|
||||
import SuratRoute from "./server/routes/surat_route";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -26,6 +32,11 @@ const Api = new Elysia({
|
||||
tags: ["api"],
|
||||
})
|
||||
.use(PengaduanRoute)
|
||||
.use(PelayananRoute)
|
||||
.use(ConfigurationDesaRoute)
|
||||
.use(WargaRoute)
|
||||
.use(SuratRoute)
|
||||
.use(TestPengaduanRoute)
|
||||
.use(apiAuth)
|
||||
.use(ApiKeyRoute)
|
||||
.use(DarmasabaRoute)
|
||||
@@ -38,6 +49,13 @@ const app = new Elysia()
|
||||
.use(Api)
|
||||
.use(Docs)
|
||||
.use(Auth)
|
||||
.use(
|
||||
cors({
|
||||
origin: "*",
|
||||
methods: ["GET", "POST", "OPTIONS"],
|
||||
allowedHeaders: ["Content-Type"],
|
||||
}),
|
||||
)
|
||||
.get(
|
||||
"/.well-known/mcp.json",
|
||||
async () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const categoryPelayananSurat = [
|
||||
name: "Surat Keterangan Belum Kawin",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP dan Kartu Keluarga" },
|
||||
{ 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"]
|
||||
@@ -25,7 +25,7 @@ export const categoryPelayananSurat = [
|
||||
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"}
|
||||
{ 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"]
|
||||
},
|
||||
@@ -36,16 +36,16 @@ export const categoryPelayananSurat = [
|
||||
{ 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"]
|
||||
dataText: ["nama ayah", "nama ibu", "nama anak", "tanggal lahir anak", "pukul lahir anak", "tempat lahir anak", "jenis kelamin anak", "anak ke", "nik ibu", "tempat tanggal lahir ibu", "pekerjaan ibu", "alamat ibu", "nik ayah", "tempat tanggal lahir ayah", "pekerjaan ayah", "alamat ayah", "nama pelapor", "hubungan pelapor", "alamat 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 dan Kartu Keluarga" }
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
|
||||
],
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "keperluan"]
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "polsek"]
|
||||
},
|
||||
{
|
||||
id: "skkematian",
|
||||
@@ -65,7 +65,7 @@ export const categoryPelayananSurat = [
|
||||
{ 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"]
|
||||
dataText: ["nama", "nik", "alamat", "pekerjaan", "jenis usaha", "penghasilan", "alasan permohonan"]
|
||||
},
|
||||
{
|
||||
id: "sktempatusaha",
|
||||
@@ -95,7 +95,7 @@ export const categoryPelayananSurat = [
|
||||
{ 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"]
|
||||
dataText: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"]
|
||||
},
|
||||
{
|
||||
id: "skyatimpiatu",
|
||||
|
||||
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: "1122334455"
|
||||
},
|
||||
{
|
||||
id: "perbekelTTD",
|
||||
name: "TTD",
|
||||
value: ""
|
||||
},
|
||||
];
|
||||
@@ -2,16 +2,16 @@ import { Tree } from "@mantine/core";
|
||||
|
||||
// ✅ Valid data, all values are unique
|
||||
const data = [
|
||||
{
|
||||
value: 'src',
|
||||
label: 'src',
|
||||
children: [
|
||||
{ value: 'src/components', label: 'components' },
|
||||
{ value: 'src/hooks', label: 'hooks' },
|
||||
],
|
||||
},
|
||||
{ value: 'package.json', label: 'package.json' },
|
||||
];
|
||||
{
|
||||
value: "src",
|
||||
label: "src",
|
||||
children: [
|
||||
{ value: "src/components", label: "components" },
|
||||
{ value: "src/hooks", label: "hooks" },
|
||||
],
|
||||
},
|
||||
{ value: "package.json", label: "package.json" },
|
||||
];
|
||||
|
||||
export default function DirPage() {
|
||||
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 {
|
||||
ActionIcon,
|
||||
AppShell,
|
||||
@@ -22,17 +26,17 @@ import {
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconDashboard,
|
||||
IconFileCertificate,
|
||||
IconKey,
|
||||
IconLock,
|
||||
IconMessageReport,
|
||||
IconSettings,
|
||||
IconUser,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
default as clientRoute,
|
||||
default as clientRoutes,
|
||||
} from "@/clientRoutes";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
|
||||
function Logout() {
|
||||
return (
|
||||
@@ -98,7 +102,7 @@ export default function DashboardLayout() {
|
||||
size="lg"
|
||||
style={{
|
||||
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 />}
|
||||
@@ -186,7 +190,7 @@ function HostView() {
|
||||
{host.name}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{host.email}
|
||||
{host.roleId}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
@@ -219,6 +223,31 @@ function NavigationDashboard() {
|
||||
label: "Dashboard Overview",
|
||||
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",
|
||||
icon: <IconKey size={20} />,
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
import ModalSurat from "@/components/ModalSurat";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
List,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
ThemeIcon,
|
||||
Title
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconCheck,
|
||||
IconFileCertificate,
|
||||
IconFileCheck,
|
||||
IconMessageReport,
|
||||
IconPhone,
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
|
||||
export default function DetailPengajuanPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const id = query.get("id");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.pelayanan.detail.get({
|
||||
query: {
|
||||
id: id!,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
|
||||
<DetailDataHistori data={data?.data?.history} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<DetailUserPengajuan data={data?.data?.warga} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
const [noSurat, setNoSurat] = useState("");
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost(data?.user ?? null);
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||
try {
|
||||
const res = await apiFetch.api.pelayanan["update-status"].post({
|
||||
id: data?.id,
|
||||
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
|
||||
keterangan: keterangan,
|
||||
idUser: host?.id ?? "",
|
||||
noSurat: noSurat
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
onAction();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Success update pengajuan surat",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update pengajuan surat",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update pengajuan surat",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Konfirmasi"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{catModal === "tolak" ? (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
|
||||
</Text>
|
||||
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
|
||||
Tolak
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
|
||||
{
|
||||
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
|
||||
}
|
||||
</Text>
|
||||
{
|
||||
data.status == 'diterima' && (
|
||||
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" />
|
||||
)
|
||||
}
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
{
|
||||
data?.status == "selesai" &&
|
||||
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />)
|
||||
}
|
||||
|
||||
|
||||
<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">
|
||||
Pengajuan {data?.category}
|
||||
</Title>
|
||||
<Title order={4} c="dimmed">
|
||||
#{data?.noPengajuan}
|
||||
</Title>
|
||||
</Group>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color={
|
||||
data?.status === "diterima"
|
||||
? "green"
|
||||
: data?.status === "ditolak"
|
||||
? "red"
|
||||
: data?.status === "selesai"
|
||||
? "blue"
|
||||
: data?.status === "dikerjakan"
|
||||
? "gray"
|
||||
: "yellow"
|
||||
}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
{data?.status}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<Stack gap="lg">
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconFileCheck size={20} />
|
||||
<Text size="md">Syarat Dokumen</Text>
|
||||
</Group>
|
||||
<List
|
||||
spacing="sm"
|
||||
pt={10}
|
||||
icon={
|
||||
<ThemeIcon variant="default" size={20} radius="xl">
|
||||
<IconCheck size={13} />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
{syaratDokumen?.map((v: any) => (
|
||||
<List.Item key={v.id}>
|
||||
<Anchor href="https://mantine.dev/" target="_blank">
|
||||
{v.jenis}
|
||||
</Anchor>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Flex>
|
||||
|
||||
|
||||
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} />
|
||||
<Text size="md">Data Pelengkap</Text>
|
||||
</Group>
|
||||
|
||||
<Table withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{
|
||||
dataText?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setOpenedPreview(!openedPreview)}
|
||||
>
|
||||
Surat
|
||||
</Button>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori({ data }: { data: any }) {
|
||||
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 Pengajuan Surat
|
||||
</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>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailUserPengajuan({ data }: { data: any }) {
|
||||
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"}>
|
||||
{data?.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconPhone size={20} />
|
||||
<Text size="md">Telepon</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">
|
||||
{data?.phone}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconMessageReport size={20} />
|
||||
<Text size="md">Jumlah Pengaduan</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">
|
||||
{data?.pengaduan}
|
||||
</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">
|
||||
{data?.pelayanan}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
268
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
268
src/pages/scr/dashboard/pelayanan-surat/list_pelayanan_page.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
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 {
|
||||
IconClockHour3,
|
||||
IconFileSad,
|
||||
IconSearch,
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
import { proxy, subscribe } 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 { data, mutate, isLoading } = useSwr("/pelayanan-surat/count", () =>
|
||||
apiFetch.api.pelayanan.count.get().then((res) => res.data),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={status || "semua"} color="teal">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab
|
||||
value="semua"
|
||||
onClick={() => {
|
||||
navigate("?status=semua");
|
||||
}}
|
||||
>
|
||||
Semua ({data?.semua || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="antrian"
|
||||
onClick={() => {
|
||||
navigate("?status=antrian");
|
||||
}}
|
||||
>
|
||||
Antrian ({data?.antrian || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="diterima"
|
||||
onClick={() => {
|
||||
navigate("?status=diterima");
|
||||
}}
|
||||
>
|
||||
Diterima ({data?.diterima || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="selesai"
|
||||
onClick={() => {
|
||||
navigate("?status=selesai");
|
||||
}}
|
||||
>
|
||||
Selesai ({data?.selesai || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="ditolak"
|
||||
onClick={() => {
|
||||
navigate("?status=ditolak");
|
||||
}}
|
||||
>
|
||||
Ditolak ({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("/", async () => {
|
||||
const res = await apiFetch.api.pelayanan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
},
|
||||
});
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
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 pelayanan surat...
|
||||
</Text>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data || [];
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group grow>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengajuan..."
|
||||
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>
|
||||
) : (
|
||||
Array.isArray(list) && 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.category}
|
||||
</Title>
|
||||
<Group>
|
||||
<Title order={6} c="gray.5">
|
||||
#{v.noPengajuan}
|
||||
</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"
|
||||
? "gray"
|
||||
: "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 Ajuan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconUser size={20} color="white" />
|
||||
<Text size="md" c="white">
|
||||
Warga
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.warga}</Text>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
458
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
458
src/pages/scr/dashboard/pengaduan/detail_page.tsx
Normal file
@@ -0,0 +1,458 @@
|
||||
import notification from "@/components/notificationGlobal";
|
||||
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,
|
||||
IconPhone,
|
||||
IconPhotoScan,
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import _ from "lodash";
|
||||
import { useEffect, 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");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.pengaduan.detail.get({
|
||||
query: {
|
||||
id: id!,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
|
||||
<DetailDataHistori data={data?.data?.history} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<DetailUserPengaduan data={data?.data?.warga} />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
|
||||
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);
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost(data?.user ?? null);
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||
try {
|
||||
const res = await apiFetch.api.pengaduan["update-status"].post({
|
||||
id: data?.id,
|
||||
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : data.status == 'diterima' ? 'dikerjakan' : 'selesai',
|
||||
keterangan: keterangan,
|
||||
idUser: host?.id ?? ""
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
onAction();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Success update pengaduan",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update pengaduan",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update pengaduan",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Konfirmasi"}
|
||||
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} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
|
||||
Tolak
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} pengaduan ini?</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* MODAL GAMBAR */}
|
||||
<Modal
|
||||
opened={openedModalImage}
|
||||
onClose={closeModalImage}
|
||||
title="Gambar Pengaduan"
|
||||
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">
|
||||
#{data?.noPengaduan}
|
||||
</Title>
|
||||
</Group>
|
||||
<Badge
|
||||
size="xl"
|
||||
variant="light"
|
||||
radius="sm"
|
||||
color={
|
||||
data?.status === "diterima"
|
||||
? "green"
|
||||
: data?.status === "ditolak"
|
||||
? "red"
|
||||
: data?.status === "selesai"
|
||||
? "blue"
|
||||
: data?.status === "dikerjakan"
|
||||
? "gray"
|
||||
: "yellow"
|
||||
}
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
{data?.status}
|
||||
</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"}>
|
||||
{_.upperFirst(data?.title)}
|
||||
</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">
|
||||
{_.upperFirst(data?.location)}
|
||||
</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">
|
||||
{_.upperFirst(data?.category)}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconPhotoScan size={20} />
|
||||
<Text size="md">Gambar</Text>
|
||||
</Group>
|
||||
<Anchor href="#" onClick={() => { }}>
|
||||
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">
|
||||
{_.upperFirst(data?.detail)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{
|
||||
data?.keterangan && (
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">Keterangan</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
{_.upperFirst(data?.keterangan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Kerjakan
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "dikerjakan" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</Group>
|
||||
) : <></>
|
||||
}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori({ data }: { data: any }) {
|
||||
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>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailUserPengaduan({ data }: { data: any }) {
|
||||
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"}>
|
||||
{data?.name}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconPhone size={20} />
|
||||
<Text size="md">Telepon</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">
|
||||
{data?.phone}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Group gap="xs">
|
||||
<IconMessageReport size={20} />
|
||||
<Text size="md">Jumlah Pengaduan</Text>
|
||||
</Group>
|
||||
<Text size="md" c="white">
|
||||
{data?.pengaduan}
|
||||
</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">
|
||||
{data?.pelayanan}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
287
src/pages/scr/dashboard/pengaduan/list_page.tsx
Normal file
287
src/pages/scr/dashboard/pengaduan/list_page.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
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, subscribe } 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 { data, mutate, isLoading } = useSwr("/pengaduan/count", () =>
|
||||
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue={status || "semua"} color="teal">
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab
|
||||
value="semua"
|
||||
onClick={() => {
|
||||
navigate("?status=semua");
|
||||
}}
|
||||
>
|
||||
Semua ({data?.semua || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="antrian"
|
||||
onClick={() => {
|
||||
navigate("?status=antrian");
|
||||
}}
|
||||
>
|
||||
Antrian ({data?.antrian || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="diterima"
|
||||
onClick={() => {
|
||||
navigate("?status=diterima");
|
||||
}}
|
||||
>
|
||||
Diterima ({data?.diterima || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="dikerjakan"
|
||||
onClick={() => {
|
||||
navigate("?status=dikerjakan");
|
||||
}}
|
||||
>
|
||||
Dikerjakan ({data?.dikerjakan || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="selesai"
|
||||
onClick={() => {
|
||||
navigate("?status=selesai");
|
||||
}}
|
||||
>
|
||||
Selesai ({data?.selesai || 0})
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="ditolak"
|
||||
onClick={() => {
|
||||
navigate("?status=ditolak");
|
||||
}}
|
||||
>
|
||||
Ditolak ({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("/", async () => {
|
||||
const res = await apiFetch.api.pengaduan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
},
|
||||
});
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
return () => unsubscribe();
|
||||
}, []);
|
||||
|
||||
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 || [];
|
||||
|
||||
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>
|
||||
) : (
|
||||
Array.isArray(list) && 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"
|
||||
? "gray"
|
||||
: "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>
|
||||
);
|
||||
}
|
||||
107
src/pages/scr/dashboard/setting/detail_setting_page.tsx
Normal file
107
src/pages/scr/dashboard/setting/detail_setting_page.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import DesaSetting from "@/components/DesaSetting";
|
||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||
import ProfileUser from "@/components/ProfileUser";
|
||||
import UserSetting from "@/components/UserSetting";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
NavLink,
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconBuildingBank,
|
||||
IconCategory2,
|
||||
IconMailSpark,
|
||||
IconUserCog,
|
||||
IconUsersGroup,
|
||||
} 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={<IconUserCog size={16} stroke={1.5} />}
|
||||
active={type === "profile" || !type}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=user`}
|
||||
label="User"
|
||||
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
||||
active={type === "user"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pengaduan`}
|
||||
label="Kategori Pengaduan"
|
||||
leftSection={<IconCategory2 size={16} stroke={1.5} />}
|
||||
active={type === "cat-pengaduan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pelayanan`}
|
||||
label="Kategori Pelayanan Surat"
|
||||
leftSection={<IconMailSpark size={16} stroke={1.5} />}
|
||||
active={type === "cat-pelayanan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=desa`}
|
||||
label="Desa"
|
||||
leftSection={<IconBuildingBank 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" ? (
|
||||
<KategoriPengaduan />
|
||||
) : type === "cat-pelayanan" ? (
|
||||
<KategoriPelayananSurat />
|
||||
) : type === "desa" ? (
|
||||
<DesaSetting />
|
||||
) : type === "user" ? (
|
||||
<UserSetting />
|
||||
) : (
|
||||
<ProfileUser />
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
191
src/pages/scr/dashboard/warga/detail_warga_page.tsx
Normal file
191
src/pages/scr/dashboard/warga/detail_warga_page.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconPhone } from "@tabler/icons-react";
|
||||
import _ from "lodash";
|
||||
import { useLocation, useNavigate } 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");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.warga.detail.get({
|
||||
query: {
|
||||
id: id!,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
<DetailWarga data={data?.data?.warga} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
|
||||
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
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 {_.upperFirst(kategori)}
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>No {_.upperFirst(kategori)}</Table.Th>
|
||||
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
kategori == "pengaduan" ?
|
||||
navigate(
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
) :
|
||||
navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailWarga({ data }: { data: any }) {
|
||||
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 #00ffc814",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to left top, #23633a, #00685b, #006984, #0065a5, #0059b1, #114ca3, #193f94, #1d3285, #202864, #1d1f45, #171628, #0b0b0b)",
|
||||
height: 100,
|
||||
borderRadius: "12px",
|
||||
position: "relative",
|
||||
}}
|
||||
/>
|
||||
<Group>
|
||||
<Avatar
|
||||
radius={100}
|
||||
size={90}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 80,
|
||||
left: 30,
|
||||
border: "3x solid white",
|
||||
backgroundColor: "#099268",
|
||||
}}
|
||||
>
|
||||
A
|
||||
</Avatar>
|
||||
|
||||
{/* Main content */}
|
||||
<Stack ml={115} gap={4}>
|
||||
<Text fw={700} fz="lg">
|
||||
{data?.name}
|
||||
</Text>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Warga Desa
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
{/* Contact info */}
|
||||
<Card radius="md" mt="md" p="md" withBorder={false}>
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs">
|
||||
<IconPhone size={18} />
|
||||
<Text size="sm">{data?.phone}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
112
src/pages/scr/dashboard/warga/list_warga_page.tsx
Normal file
112
src/pages/scr/dashboard/warga/list_warga_page.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CloseButton,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Input,
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function ListWargaPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.warga.list.get({
|
||||
query: {
|
||||
search: value,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const list = data?.data || [];
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [value]);
|
||||
|
||||
|
||||
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>Nama</Table.Th>
|
||||
<Table.Th>No Telepon</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
list?.map((item, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>{item.phone}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
24
src/server/lib/create-surat.ts
Normal file
24
src/server/lib/create-surat.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { prisma } from "./prisma"
|
||||
|
||||
export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }: { idPengajuan: string, idCategory: string, idWarga: string, noSurat: string }) {
|
||||
try {
|
||||
const surat = await prisma.suratPelayanan.create({
|
||||
data: {
|
||||
idPengajuanLayanan: idPengajuan,
|
||||
idCategory,
|
||||
idWarga,
|
||||
noSurat,
|
||||
}
|
||||
})
|
||||
|
||||
if (!surat.id) {
|
||||
return { success: false, message: 'gagal membuat surat' }
|
||||
}
|
||||
|
||||
return { success: true, message: 'surat sudah dibuat' }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return { success: false, message: 'gagal membuat surat' }
|
||||
}
|
||||
|
||||
}
|
||||
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",
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import _ from "lodash";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
interface McpTool {
|
||||
name: string;
|
||||
@@ -16,57 +15,50 @@ interface McpTool {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions (without run()).
|
||||
* Hanya menyertakan endpoint yang memiliki tag berisi "mcp".
|
||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions.
|
||||
*/
|
||||
export function convertOpenApiToMcpTools(openApiJson: any): McpTool[] {
|
||||
export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): McpTool[] {
|
||||
const tools: McpTool[] = [];
|
||||
|
||||
if (!openApiJson || typeof openApiJson !== "object") {
|
||||
console.warn("Invalid OpenAPI JSON");
|
||||
return tools;
|
||||
}
|
||||
|
||||
const paths = openApiJson.paths || {};
|
||||
|
||||
if (Object.keys(paths).length === 0) {
|
||||
console.warn("No paths found in OpenAPI spec");
|
||||
return tools;
|
||||
}
|
||||
|
||||
for (const [path, methods] of Object.entries(paths)) {
|
||||
// ✅ skip semua path internal MCP
|
||||
if (!path || typeof path !== "string") continue;
|
||||
if (path.startsWith("/mcp")) continue;
|
||||
|
||||
for (const [method, operation] of Object.entries<any>(methods as any)) {
|
||||
if (!methods || typeof methods !== "object") continue;
|
||||
|
||||
for (const [method, operation] of Object.entries<any>(methods)) {
|
||||
const validMethods = ["get", "post", "put", "delete", "patch", "head", "options"];
|
||||
if (!validMethods.includes(method.toLowerCase())) continue;
|
||||
|
||||
if (!operation || typeof operation !== "object") continue;
|
||||
|
||||
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;
|
||||
if (!tags.length || !tags.some(t =>
|
||||
typeof t === "string" && t.toLowerCase().includes(filterTag)
|
||||
)) 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);
|
||||
try {
|
||||
const tool = createToolFromOperation(path, method, operation, tags);
|
||||
if (tool) {
|
||||
tools.push(tool);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error creating tool for ${method.toUpperCase()} ${path}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,35 +66,316 @@ export function convertOpenApiToMcpTools(openApiJson: any): McpTool[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Buat MCP tool dari operation OpenAPI
|
||||
*/
|
||||
function createToolFromOperation(
|
||||
path: string,
|
||||
method: string,
|
||||
operation: any,
|
||||
tags: string[]
|
||||
): McpTool | null {
|
||||
try {
|
||||
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||
const name = cleanToolName(rawName);
|
||||
|
||||
if (!name || name === "unnamed_tool") {
|
||||
console.warn(`Invalid tool name for ${method} ${path}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const description =
|
||||
operation.description ||
|
||||
operation.summary ||
|
||||
`Execute ${method.toUpperCase()} ${path}`;
|
||||
|
||||
// ✅ Extract schema berdasarkan method
|
||||
let schema;
|
||||
if (method.toLowerCase() === "get") {
|
||||
// ✅ Untuk GET, ambil dari parameters (query/path)
|
||||
schema = extractParametersSchema(operation.parameters || []);
|
||||
} else {
|
||||
// ✅ Untuk POST/PUT/etc, ambil dari requestBody
|
||||
schema = extractRequestBodySchema(operation);
|
||||
}
|
||||
|
||||
const inputSchema = createInputSchema(schema);
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
"x-props": {
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
operationId: operation.operationId,
|
||||
tag: tags[0],
|
||||
deprecated: operation.deprecated || false,
|
||||
summary: operation.summary,
|
||||
},
|
||||
inputSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to create tool from operation:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract schema dari parameters (untuk GET requests)
|
||||
*/
|
||||
function extractParametersSchema(parameters: any[]): any {
|
||||
if (!Array.isArray(parameters) || parameters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties: any = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const param of parameters) {
|
||||
if (!param || typeof param !== "object") continue;
|
||||
|
||||
// ✅ Support path, query, dan header parameters
|
||||
if (["path", "query", "header"].includes(param.in)) {
|
||||
const paramName = param.name;
|
||||
if (!paramName || typeof paramName !== "string") continue;
|
||||
|
||||
properties[paramName] = {
|
||||
type: param.schema?.type || "string",
|
||||
description: param.description || `${param.in} parameter: ${paramName}`,
|
||||
};
|
||||
|
||||
// ✅ Copy field tambahan dari schema
|
||||
if (param.schema) {
|
||||
const allowedFields = ["examples", "example", "default", "enum", "pattern", "minLength", "maxLength", "minimum", "maximum", "format"];
|
||||
for (const field of allowedFields) {
|
||||
if (param.schema[field] !== undefined) {
|
||||
properties[paramName][field] = param.schema[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (param.required === true) {
|
||||
required.push(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(properties).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
required,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract schema dari requestBody (untuk POST/PUT/etc requests)
|
||||
*/
|
||||
function extractRequestBodySchema(operation: any): any {
|
||||
if (!operation.requestBody?.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = operation.requestBody.content;
|
||||
|
||||
const contentTypes = [
|
||||
"application/json",
|
||||
"multipart/form-data",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
for (const contentType of contentTypes) {
|
||||
if (content[contentType]?.schema) {
|
||||
return content[contentType].schema;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, value] of Object.entries<any>(content)) {
|
||||
if (value?.schema) {
|
||||
return value.schema;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat input schema yang valid untuk MCP
|
||||
*/
|
||||
function createInputSchema(schema: any): any {
|
||||
const defaultSchema = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return defaultSchema;
|
||||
}
|
||||
|
||||
try {
|
||||
const properties: any = {};
|
||||
const required: string[] = [];
|
||||
const originalRequired = Array.isArray(schema.required) ? schema.required : [];
|
||||
|
||||
if (schema.properties && typeof schema.properties === "object") {
|
||||
for (const [key, prop] of Object.entries<any>(schema.properties)) {
|
||||
if (!key || typeof key !== "string") continue;
|
||||
|
||||
try {
|
||||
const cleanProp = cleanProperty(prop);
|
||||
if (cleanProp) {
|
||||
properties[key] = cleanProp;
|
||||
|
||||
// ✅ PERBAIKAN: Check optional flag dengan benar
|
||||
const isOptional = prop?.optional === true || prop?.optional === "true";
|
||||
const isInRequired = originalRequired.includes(key);
|
||||
|
||||
// ✅ Hanya masukkan ke required jika memang required DAN bukan optional
|
||||
if (isInRequired && !isOptional) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning property ${key}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating input schema:", error);
|
||||
return defaultSchema;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan property dari field custom
|
||||
*/
|
||||
function cleanProperty(prop: any): any | null {
|
||||
if (!prop || typeof prop !== "object") {
|
||||
return { type: "string" };
|
||||
}
|
||||
|
||||
try {
|
||||
const cleaned: any = {
|
||||
type: prop.type || "string",
|
||||
};
|
||||
|
||||
const allowedFields = [
|
||||
"description",
|
||||
"examples",
|
||||
"example",
|
||||
"default",
|
||||
"enum",
|
||||
"pattern",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"format",
|
||||
"multipleOf",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
];
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (prop[field] !== undefined && prop[field] !== null) {
|
||||
cleaned[field] = prop[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.properties && typeof prop.properties === "object") {
|
||||
cleaned.properties = {};
|
||||
for (const [key, value] of Object.entries(prop.properties)) {
|
||||
const cleanedNested = cleanProperty(value);
|
||||
if (cleanedNested) {
|
||||
cleaned.properties[key] = cleanedNested;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(prop.required)) {
|
||||
cleaned.required = prop.required.filter((r: any) => typeof r === "string");
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.items) {
|
||||
cleaned.items = cleanProperty(prop.items);
|
||||
}
|
||||
|
||||
if (Array.isArray(prop.oneOf)) {
|
||||
cleaned.oneOf = prop.oneOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(prop.anyOf)) {
|
||||
cleaned.anyOf = prop.anyOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(prop.allOf)) {
|
||||
cleaned.allOf = prop.allOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning property:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan nama tool
|
||||
*/
|
||||
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, "");
|
||||
if (!name || typeof name !== "string") {
|
||||
return "unnamed_tool";
|
||||
}
|
||||
|
||||
try {
|
||||
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, "")
|
||||
|| "unnamed_tool";
|
||||
} catch (error) {
|
||||
console.error("Error cleaning tool name:", error);
|
||||
return "unnamed_tool";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
export async function getMcpTools(url: string, filterTag: string): Promise<McpTool[]> {
|
||||
try {
|
||||
|
||||
console.log(`Fetching OpenAPI spec from: ${url}`);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const openApiJson = await response.json();
|
||||
const tools = convertOpenApiToMcpTools(openApiJson, filterTag);
|
||||
|
||||
console.log(`✅ Successfully generated ${tools.length} MCP tools`);
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MCP tools:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// === CLI Mode ===
|
||||
if (import.meta.main) {
|
||||
const tools = await getMcpTools();
|
||||
await Bun.write("./tools.json", JSON.stringify(tools, null, 2));
|
||||
}
|
||||
|
||||
108
src/server/lib/mcp_tool_convert.txt
Normal file
108
src/server/lib/mcp_tool_convert.txt
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;
|
||||
}
|
||||
261
src/server/lib/seafile.ts
Normal file
261
src/server/lib/seafile.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
#!/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 uploadFileToFolder(config: Config, base64File: { name: string; data: string; }, folder: 'syarat-dokumen' | 'pengaduan'): 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", folder); // 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, '')}`
|
||||
}
|
||||
48
src/server/routes/configuration_desa_route.ts
Normal file
48
src/server/routes/configuration_desa_route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const ConfigurationDesaRoute = new Elysia({
|
||||
prefix: "configuration-desa",
|
||||
tags: ["configuration-desa"],
|
||||
})
|
||||
|
||||
.get("/list", async () => {
|
||||
const data = await prisma.configuration.findMany({
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Konfigurasi",
|
||||
description: `tool untuk mendapatkan list konfigurasi`,
|
||||
}
|
||||
})
|
||||
.post("/edit", async ({ body }) => {
|
||||
const { id, value } = body
|
||||
|
||||
await prisma.configuration.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
value,
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'konfigurasi sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "edit konfigurasi desa",
|
||||
description: `tool untuk edit konfigurasi desa`
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default ConfigurationDesaRoute
|
||||
@@ -96,7 +96,6 @@ const LayananRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Create Layanan KTP/KK",
|
||||
description: "Create a new service request for KTP or KK.",
|
||||
tags: ["mcp"],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -132,7 +131,6 @@ const LayananRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Cek Status KTP",
|
||||
description: "Retrieve the current status of a KTP/KK request by unique ID.",
|
||||
tags: ["mcp"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||
// import tools from "./../../../tools.json";
|
||||
|
||||
var tools = [] as any[];
|
||||
const OPENAPI_URL = process.env.BUN_PUBLIC_BASE_URL + "/docs/json";
|
||||
const FILTER_TAG = "mcp";
|
||||
|
||||
if (!process.env.BUN_PUBLIC_BASE_URL) {
|
||||
throw new Error("BUN_PUBLIC_BASE_URL environment variable is not set");
|
||||
}
|
||||
|
||||
// =====================
|
||||
// MCP Protocol Types
|
||||
@@ -113,16 +118,17 @@ async function handleMCPRequestAsync(
|
||||
const baseUrl =
|
||||
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
||||
const data = result.data.data;
|
||||
const isObject = typeof data === "object" && data !== null;
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
isObject
|
||||
? { type: "json", data: data }
|
||||
: { type: "text", text: JSON.stringify(data || result.data || result) },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -155,7 +161,7 @@ export const MCPRoute = new Elysia({
|
||||
})
|
||||
.post("/mcp", async ({ request, set }) => {
|
||||
if (!tools.length) {
|
||||
tools = await getMcpTools();
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Content-Type"] = "application/json";
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
@@ -189,7 +195,8 @@ export const MCPRoute = new Elysia({
|
||||
// Tools list (debug)
|
||||
.get("/mcp/tools", async ({ set }) => {
|
||||
if (!tools.length) {
|
||||
tools = await getMcpTools();
|
||||
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return {
|
||||
@@ -215,7 +222,7 @@ export const MCPRoute = new Elysia({
|
||||
})
|
||||
.get("/mcp/init", async ({ set }) => {
|
||||
|
||||
const _tools = await getMcpTools();
|
||||
const _tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
tools = _tools;
|
||||
return {
|
||||
success: true,
|
||||
|
||||
679
src/server/routes/pelayanan_surat_route.ts
Normal file
679
src/server/routes/pelayanan_surat_route.ts
Normal file
@@ -0,0 +1,679 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
import { createSurat } from "../lib/create-surat"
|
||||
import { getLastUpdated } from "../lib/get-last-updated"
|
||||
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
|
||||
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.Any(),
|
||||
dataText: t.Any(),
|
||||
}),
|
||||
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.Any(),
|
||||
dataText: t.Any(),
|
||||
}),
|
||||
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 ({ query }) => {
|
||||
const { phone } = query
|
||||
const data = await prisma.pelayananAjuan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
Warga: {
|
||||
phone
|
||||
}
|
||||
}
|
||||
})
|
||||
return data
|
||||
}, {
|
||||
query: t.Object({
|
||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "List Ajuan Pelayanan Surat by Phone",
|
||||
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
const data = await prisma.pelayananAjuan.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
noPengajuan: id
|
||||
},
|
||||
{
|
||||
id: id
|
||||
}
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true,
|
||||
dataText: true,
|
||||
syaratDokumen: true,
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
_count: {
|
||||
select: {
|
||||
Pengaduan: true,
|
||||
PelayananAjuan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const dataSurat = await prisma.suratPelayanan.findFirst({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idCategory: true,
|
||||
}
|
||||
})
|
||||
|
||||
const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
jenis: true,
|
||||
value: true,
|
||||
}
|
||||
})
|
||||
|
||||
const dataText = await prisma.dataTextPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
value: true,
|
||||
jenis: true,
|
||||
}
|
||||
})
|
||||
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
|
||||
name: string;
|
||||
desc: string;
|
||||
}[];
|
||||
|
||||
const dataSyaratFix = dataSyarat.map((item) => {
|
||||
const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
|
||||
return {
|
||||
id: item.id,
|
||||
jenis: desc,
|
||||
value: item.value,
|
||||
}
|
||||
})
|
||||
|
||||
const dataTextFix = dataText.map((item) => {
|
||||
const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
|
||||
return {
|
||||
id: item.id,
|
||||
jenis: item.jenis,
|
||||
value: item.value,
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistory = await prisma.historyPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.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.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}),
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
})
|
||||
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
phone: data?.Warga?.phone,
|
||||
pengaduan: data?.Warga?._count.Pengaduan,
|
||||
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||
}
|
||||
|
||||
const dataPengajuan = {
|
||||
id: data?.id,
|
||||
noPengajuan: data?.noPengajuan,
|
||||
category: data?.CategoryPelayanan.name,
|
||||
status: data?.status,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
idSurat: dataSurat?.id,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
pengajuan: dataPengajuan,
|
||||
history: dataHistoryFix,
|
||||
warga: warga,
|
||||
syaratDokumen: dataSyaratFix,
|
||||
dataText: dataTextFix,
|
||||
}
|
||||
return datafix
|
||||
}, {
|
||||
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 { kategoriId, wargaId, noTelepon, dataText, syaratDokumen } = body
|
||||
const noPengajuan = await generateNoPengajuanSurat()
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
const category = await prisma.categoryPelayanan.findUnique({
|
||||
where: {
|
||||
id: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPelayanan.findFirst({
|
||||
where: {
|
||||
name: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
return { success: false, message: 'kategori pelayanan surat tidak ditemukan' }
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: wargaId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findFirst({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: wargaId,
|
||||
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) {
|
||||
return { success: false, message: '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({
|
||||
kategoriId: t.String({
|
||||
minLength: 1,
|
||||
description: "ID atau nama kategori pelayanan surat yang dipilih. Jika berupa nama, sistem akan mencocokkan secara otomatis.",
|
||||
examples: ["skusaha"],
|
||||
error: "ID kategori harus diisi"
|
||||
}),
|
||||
|
||||
wargaId: t.String({
|
||||
minLength: 1,
|
||||
description: "ID warga atau nama warga. Jika ID tidak ditemukan, sistem akan mencari berdasarkan nama.",
|
||||
examples: ["Budi Santoso"],
|
||||
error: "ID warga harus diisi"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
minLength: 8,
|
||||
description: "Nomor HP warga yang akan dinormalisasi. Jika data warga tidak ditemukan berdasarkan idWarga, pencarian dilakukan via nomor ini.",
|
||||
examples: ["081234567890"],
|
||||
error: "Nomor telepon harus diisi"
|
||||
}),
|
||||
|
||||
dataText: t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
minLength: 1,
|
||||
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
||||
examples: ["nama", "alamat", "pekerjaan", "keperluan"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
minLength: 1,
|
||||
description: "Isi atau nilai dari jenis field terkait.",
|
||||
examples: ["Budi Santoso", "Jl. Mawar No. 10", "Karyawan Swasta"],
|
||||
error: "value harus diisi"
|
||||
}),
|
||||
}),
|
||||
{
|
||||
description: "Kumpulan data text dinamis sesuai kategori layanan.",
|
||||
examples: [
|
||||
[
|
||||
{ jenis: "jenis usaha", value: "usaha makanan" },
|
||||
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
|
||||
]
|
||||
],
|
||||
error: "dataText harus berupa array"
|
||||
}
|
||||
),
|
||||
|
||||
syaratDokumen: t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
minLength: 1,
|
||||
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
|
||||
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
minLength: 1,
|
||||
description: "Nama file atau identifier file dokumen yang diupload.",
|
||||
examples: ["ktp_budi.png", "kk_budi.png"],
|
||||
error: "value harus diisi"
|
||||
}),
|
||||
}),
|
||||
{
|
||||
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
|
||||
examples: [
|
||||
[
|
||||
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
|
||||
{ jenis: "ktp/kk", value: "kk_budi.png" },
|
||||
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
|
||||
]
|
||||
],
|
||||
error: "syaratDokumen harus berupa array"
|
||||
}
|
||||
),
|
||||
}),
|
||||
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, noSurat } = body
|
||||
let deskripsi = ""
|
||||
|
||||
|
||||
const pengajuan = await prisma.pelayananAjuan.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
status: status as StatusPengaduan,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idCategory: true,
|
||||
idWarga: true,
|
||||
}
|
||||
})
|
||||
|
||||
if (!pengajuan) {
|
||||
return { success: false, message: 'gagal update status pengajuan surat' }
|
||||
}
|
||||
|
||||
if (status === "diterima") {
|
||||
deskripsi = "Pengajuan surat diterima"
|
||||
} else if (status === "ditolak") {
|
||||
deskripsi = "Pengajuan surat ditolak dengan keterangan " + keterangan
|
||||
} else if (status === "selesai") {
|
||||
deskripsi = "Pengajuan surat disetujui"
|
||||
}
|
||||
|
||||
await prisma.historyPelayanan.create({
|
||||
data: {
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
deskripsi: deskripsi,
|
||||
status: status as StatusPengaduan,
|
||||
idUser,
|
||||
keteranganAlasan: keterangan,
|
||||
}
|
||||
})
|
||||
|
||||
if (status === "selesai") {
|
||||
await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
|
||||
}
|
||||
|
||||
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({ optional: true }),
|
||||
idUser: t.String({ optional: true }),
|
||||
noSurat: t.String({ optional: true }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Update Status Pengajuan Pelayanan Surat",
|
||||
description: `tool untuk update status pengajuan pelayanan surat`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.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: [
|
||||
{
|
||||
CategoryPelayanan: {
|
||||
name: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengajuan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
Warga: {
|
||||
phone: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if (status && status !== "semua") {
|
||||
where = {
|
||||
...where,
|
||||
status: status
|
||||
}
|
||||
}
|
||||
|
||||
const data = await prisma.pelayananAjuan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataFix = data.map((item) => {
|
||||
return {
|
||||
noPengajuan: item.noPengajuan,
|
||||
id: item.id,
|
||||
category: item.CategoryPelayanan.name,
|
||||
warga: item.Warga.name,
|
||||
status: item.status,
|
||||
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 Pengajuan Pelayanan Surat Warga",
|
||||
description: `tool untuk mendapatkan list pengajuan pelayanan surat warga`,
|
||||
}
|
||||
})
|
||||
.get("/count", async ({ query }) => {
|
||||
const counts = await prisma.pelayananAjuan.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.pelayananAjuan.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 Pengajuan Pelayanan Surat Warga",
|
||||
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
|
||||
}
|
||||
})
|
||||
|
||||
export default PelayananRoute
|
||||
@@ -1,7 +1,12 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
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 { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||
|
||||
const PengaduanRoute = new Elysia({
|
||||
prefix: "pengaduan",
|
||||
@@ -13,9 +18,18 @@ const PengaduanRoute = new Elysia({
|
||||
const data = await prisma.categoryPengaduan.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
return { data }
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Kategori Pengaduan",
|
||||
@@ -32,10 +46,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah dibuat`
|
||||
return { success: true, message: 'kategori pengaduan sudah dibuat' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name harus diisi" }),
|
||||
@@ -57,10 +68,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah diperbarui`
|
||||
return { success: true, message: 'kategori pengaduan sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -83,10 +91,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
kategori pengaduan sudah dihapus`
|
||||
return { success: true, message: 'kategori pengaduan sudah dihapus' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -101,49 +106,58 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
// --- PENGADUAN ---
|
||||
.post("/create", async ({ body }) => {
|
||||
const { title, detail, location, image, idCategory, idWarga, phone } = body
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||
let imageFix = namaGambar
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = idCategory
|
||||
let idWargaFix = idWarga
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
id: idCategory,
|
||||
}
|
||||
})
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
if (idCategoryFix) {
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
name: idCategory,
|
||||
id: idCategoryFix,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
where: {
|
||||
name: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
idCategoryFix = "lainnya"
|
||||
}
|
||||
|
||||
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: idWarga,
|
||||
id: wargaId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const cariWarga = await prisma.warga.findFirst({
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone,
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: idWarga,
|
||||
phone,
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
@@ -158,12 +172,12 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title,
|
||||
detail,
|
||||
title: judulPengaduan,
|
||||
detail: detailPengaduan,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
location,
|
||||
image,
|
||||
location: lokasi,
|
||||
image: imageFix,
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
@@ -172,7 +186,7 @@ const PengaduanRoute = new Elysia({
|
||||
})
|
||||
|
||||
if (!pengaduan.id) {
|
||||
throw new Error("gagal membuat pengaduan")
|
||||
return { success: false, message: 'gagal membuat pengaduan' }
|
||||
}
|
||||
|
||||
await prisma.historyPengaduan.create({
|
||||
@@ -182,23 +196,71 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
pengaduan sudah dibuat`
|
||||
return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
title: t.String({ minLength: 1, error: "title harus diisi" }),
|
||||
detail: t.String({ minLength: 1, error: "detail harus diisi" }),
|
||||
location: t.String({ minLength: 1, error: "location harus diisi" }),
|
||||
image: t.Any(),
|
||||
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" }),
|
||||
judulPengaduan: t.String({
|
||||
error: "Judul pengaduan harus diisi",
|
||||
examples: ["Sampah menumpuk di depan rumah"],
|
||||
description: "Judul singkat dari pengaduan warga"
|
||||
}),
|
||||
|
||||
detailPengaduan: t.String({
|
||||
error: "Deskripsi pengaduan harus diisi",
|
||||
examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"],
|
||||
description: "Penjelasan lebih detail mengenai pengaduan"
|
||||
}),
|
||||
|
||||
lokasi: t.String({
|
||||
error: "Lokasi pengaduan harus diisi",
|
||||
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
|
||||
description: "Alamat atau titik lokasi pengaduan"
|
||||
}),
|
||||
|
||||
namaGambar: t.String({
|
||||
optional: true,
|
||||
examples: ["sampah.jpg"],
|
||||
description: "Nama file gambar yang telah diupload (opsional)"
|
||||
}),
|
||||
|
||||
kategoriId: t.String({
|
||||
optional: true,
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
}),
|
||||
|
||||
wargaId: t.String({
|
||||
optional: true,
|
||||
examples: ["budiman"],
|
||||
description: "ID unik warga yang melapor (jika sudah terdaftar)"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Create Pengaduan Warga",
|
||||
description: `tool untuk membuat pengaduan warga`,
|
||||
summary: "Buat Pengaduan Warga",
|
||||
description: `
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
|
||||
|
||||
Alur proses:
|
||||
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
|
||||
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
|
||||
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
|
||||
2. Sistem memvalidasi data warga berdasarkan ID.
|
||||
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
|
||||
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
|
||||
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
|
||||
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
|
||||
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
|
||||
|
||||
Respon:
|
||||
- success: true jika pengaduan berhasil dibuat.
|
||||
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
@@ -217,7 +279,7 @@ const PengaduanRoute = new Elysia({
|
||||
})
|
||||
|
||||
if (!pengaduan) {
|
||||
throw new Error("gagal membuat pengaduan")
|
||||
return { success: false, message: 'gagal update status pengaduan' }
|
||||
}
|
||||
|
||||
if (status === "diterima") {
|
||||
@@ -239,10 +301,7 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return `
|
||||
${JSON.stringify(body)}
|
||||
|
||||
status pengaduan sudah diupdate`
|
||||
return { success: true, message: 'status pengaduan sudah diupdate' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
@@ -258,9 +317,16 @@ const PengaduanRoute = new Elysia({
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const data = await prisma.pengaduan.findUnique({
|
||||
|
||||
const data = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
id,
|
||||
OR: [
|
||||
{
|
||||
noPengaduan: id
|
||||
}, {
|
||||
id: id
|
||||
}
|
||||
]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -283,6 +349,13 @@ const PengaduanRoute = new Elysia({
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
_count: {
|
||||
select: {
|
||||
Pengaduan: true,
|
||||
PelayananAjuan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,39 +384,56 @@ const PengaduanRoute = new Elysia({
|
||||
id: item.id,
|
||||
deskripsi: item.deskripsi,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt,
|
||||
createdAt: item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}),
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
})
|
||||
|
||||
const datafix = {
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
phone: data?.Warga?.phone,
|
||||
pengaduan: data?.Warga?._count.Pengaduan,
|
||||
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||
}
|
||||
|
||||
const dataPengaduan = {
|
||||
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,
|
||||
category: data?.CategoryPengaduan.name,
|
||||
status: data?.status,
|
||||
keterangan: data?.keterangan,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
pengaduan: dataPengaduan,
|
||||
history: dataHistoryFix,
|
||||
warga: warga,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan`,
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const { take, page, search } = query
|
||||
const { take, page, search, phone } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
@@ -374,6 +464,11 @@ const PengaduanRoute = new Elysia({
|
||||
},
|
||||
}
|
||||
],
|
||||
AND: {
|
||||
Warga: {
|
||||
phone: phone
|
||||
}
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -408,10 +503,245 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
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",
|
||||
description: `tool untuk mendapatkan list pengaduan warga`,
|
||||
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
|
||||
|
||||
59
src/server/routes/surat_route.ts
Normal file
59
src/server/routes/surat_route.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const SuratRoute = new Elysia({
|
||||
prefix: "surat",
|
||||
tags: ["surat"],
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
const dataSurat = await prisma.suratPelayanan.findUnique({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noSurat: true,
|
||||
idCategory: true,
|
||||
createdAt: true,
|
||||
PelayananAjuan: {
|
||||
select: {
|
||||
DataTextPelayanan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataSetting = await prisma.configuration.findMany()
|
||||
|
||||
const toObject = (arr: any[]) =>
|
||||
dataSetting.reduce((acc: any, item: any) => {
|
||||
acc[item.id] = item.value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
surat: {
|
||||
id: dataSurat?.id,
|
||||
idCategory: dataSurat?.idCategory,
|
||||
noSurat: dataSurat?.noSurat,
|
||||
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
|
||||
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
},
|
||||
setting: toObject(dataSetting)
|
||||
}
|
||||
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Surat",
|
||||
description: `tool untuk mendapatkan detail surat`,
|
||||
}
|
||||
|
||||
})
|
||||
;
|
||||
|
||||
export default SuratRoute
|
||||
187
src/server/routes/test_pengaduan.ts
Normal file
187
src/server/routes/test_pengaduan.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import { prisma } from "../lib/prisma";
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan";
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone";
|
||||
|
||||
const TestPengaduanRoute = new Elysia({
|
||||
prefix: "online-pengaduan"
|
||||
})
|
||||
.get("/category", async () => {
|
||||
const data = await prisma.categoryPengaduan.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return { data }
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Kategori Pengaduan",
|
||||
description: `tool untuk mendapatkan list kategori pengaduan`,
|
||||
tags: ["test"]
|
||||
}
|
||||
})
|
||||
|
||||
.post("/create", async ({ body }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||
let imageFix = namaGambar
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
id: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
where: {
|
||||
name: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: wargaId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
idWargaFix = wargaCreate.id
|
||||
} else {
|
||||
idWargaFix = cariWarga.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title: judulPengaduan,
|
||||
detail: detailPengaduan,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
location: lokasi,
|
||||
image: imageFix,
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
}
|
||||
})
|
||||
|
||||
if (!pengaduan.id) {
|
||||
return { success: false, message: 'gagal membuat pengaduan' }
|
||||
}
|
||||
|
||||
await prisma.historyPengaduan.create({
|
||||
data: {
|
||||
idPengaduan: pengaduan.id,
|
||||
deskripsi: "Pengaduan dibuat",
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
judulPengaduan: t.String({
|
||||
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
|
||||
examples: ["Sampah menumpuk di depan rumah"],
|
||||
description: "Judul singkat dari pengaduan warga"
|
||||
}),
|
||||
|
||||
detailPengaduan: t.String({
|
||||
error: "Deskripsi pengaduan harus diisi dan minimal 10 karakter",
|
||||
examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"],
|
||||
description: "Penjelasan lebih detail mengenai pengaduan"
|
||||
}),
|
||||
|
||||
lokasi: t.String({
|
||||
error: "Lokasi pengaduan harus diisi",
|
||||
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
|
||||
description: "Alamat atau titik lokasi pengaduan"
|
||||
}),
|
||||
|
||||
namaGambar: t.String({
|
||||
optional: true,
|
||||
examples: ["sampah.jpg"],
|
||||
description: "Nama file gambar yang telah diupload (opsional)"
|
||||
}),
|
||||
|
||||
kategoriId: t.String({
|
||||
error: "ID kategori pengaduan harus diisi",
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
}),
|
||||
|
||||
wargaId: t.String({
|
||||
error: "ID warga harus diisi",
|
||||
examples: ["budiman"],
|
||||
description: "ID unik warga yang melapor (jika sudah terdaftar)"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Buat Pengaduan Warga",
|
||||
description: `
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
|
||||
|
||||
Alur proses:
|
||||
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
|
||||
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
|
||||
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
|
||||
2. Sistem memvalidasi data warga berdasarkan ID.
|
||||
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
|
||||
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
|
||||
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
|
||||
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
|
||||
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
|
||||
|
||||
Respon:
|
||||
- success: true jika pengaduan berhasil dibuat.
|
||||
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
|
||||
tags: ["test"]
|
||||
}
|
||||
})
|
||||
|
||||
export default TestPengaduanRoute
|
||||
|
||||
@@ -47,5 +47,140 @@ const UserRoute = new Elysia({
|
||||
description: "upsert user",
|
||||
}
|
||||
})
|
||||
.post("/update-password", async ({ body }) => {
|
||||
const { password, id } = body
|
||||
const update = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
password
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Password updated successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
password: t.String({ minLength: 1, error: "password is required" }),
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "update password",
|
||||
description: "update password user",
|
||||
}
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { name, phone, id, roleId } = body
|
||||
const update = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
phone,
|
||||
roleId
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User updated successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
phone: t.String({ minLength: 1, error: "phone is required" }),
|
||||
id: t.String({ minLength: 1, error: "id is required" }),
|
||||
roleId: t.String({ minLength: 1, error: "roleId is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "update",
|
||||
description: "update user",
|
||||
}
|
||||
})
|
||||
.post("/create", async ({ body }) => {
|
||||
const { name, phone, roleId, email, password } = body
|
||||
const create = await prisma.user.create({
|
||||
data: {
|
||||
name,
|
||||
phone,
|
||||
roleId,
|
||||
email,
|
||||
password
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User created successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
phone: t.String({ minLength: 1, error: "phone is required" }),
|
||||
roleId: t.String({ minLength: 1, error: "roleId is required" }),
|
||||
email: t.String({ minLength: 1, error: "email is required" }),
|
||||
password: t.String({ minLength: 1, error: "password is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "create",
|
||||
description: "create user",
|
||||
}
|
||||
})
|
||||
.get("/list", async (ctx) => {
|
||||
const { user } = ctx as any
|
||||
|
||||
const data = await prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
NOT: {
|
||||
id: user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "list",
|
||||
description: "list user",
|
||||
}
|
||||
})
|
||||
.get("/role", async () => {
|
||||
const data = await prisma.role.findMany()
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "role",
|
||||
description: "role user",
|
||||
}
|
||||
})
|
||||
.post("/delete", async ({ body }) => {
|
||||
const { id } = body
|
||||
const deleteData = await prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User deleted successfully",
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "delete",
|
||||
description: "delete user",
|
||||
}
|
||||
})
|
||||
|
||||
export default UserRoute
|
||||
141
src/server/routes/warga_route.ts
Normal file
141
src/server/routes/warga_route.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import _ from "lodash";
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const WargaRoute = new Elysia({
|
||||
prefix: "warga",
|
||||
tags: ["warga"],
|
||||
})
|
||||
|
||||
.get("/list", async ({ query }) => {
|
||||
const { search } = query
|
||||
|
||||
const data = await prisma.warga.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
},
|
||||
{
|
||||
phone: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Warga",
|
||||
description: `tool untuk mendapatkan list warga`,
|
||||
}
|
||||
})
|
||||
.post("/edit", async ({ body }) => {
|
||||
const { id, name, phone } = body
|
||||
|
||||
const nomorHP = normalizePhoneNumber({ phone })
|
||||
await prisma.warga.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
phone: nomorHP
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'data warga sudah diperbarui' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
name: t.String({ minLength: 1, error: "value harus diisi" }),
|
||||
phone: t.String({ minLength: 1 })
|
||||
}),
|
||||
detail: {
|
||||
summary: "edit konfigurasi desa",
|
||||
description: `tool untuk edit konfigurasi desa`
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const dataWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
|
||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
idWarga: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
noPengaduan: true,
|
||||
title: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
idWarga: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayanFix = dataPelayanan.map((v: any) => ({
|
||||
..._.omit(v, ["CategoryPelayanan"]),
|
||||
id: v.id,
|
||||
noPengaduan: v.noPengajuan,
|
||||
status: v.status,
|
||||
category: v.CategoryPelayanan.name
|
||||
}))
|
||||
|
||||
return {
|
||||
warga: dataWarga,
|
||||
pengaduan: dataPengaduan,
|
||||
pelayanan: dataPelayanFix
|
||||
}
|
||||
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Warga",
|
||||
description: `tool untuk mendapatkan detail warga`,
|
||||
}
|
||||
|
||||
})
|
||||
;
|
||||
|
||||
export default WargaRoute
|
||||
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\"}"
|
||||
99
x.sh
99
x.sh
@@ -1,2 +1,97 @@
|
||||
TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJob3N0Iiwic3ViIjoiYmlwIiwicGF5bG9hZCI6IntcIm5hbWVcIjpcImplbm5hLW1jcFwiLFwiZGVzY3JpcHRpb25cIjpcInVudHVrIGplbm5hIG1jcFwiLFwiZXhwaXJlZEF0XCI6XCIyMDQ4LTA2LTI4XCJ9IiwiZXhwIjoyNDc2OTE1MjAwLCJpYXQiOjE3NjE2NDA1NDN9.EY4P246r3GBHo3yJgG0c5hvgG7p1z2x0KNL2fWBMpk8
|
||||
curl http://localhost:3000/api/pengaduan/category
|
||||
# curl -X POST https://cld-dkr-makuro-seafile.wibudev.com/api2/auth-token/ \
|
||||
# -d "username=wibu@bip.com" \
|
||||
# -d "password=Production_123"
|
||||
|
||||
# {
|
||||
# "relay_id": "44e8f253849ad910dc142247227c8ece8ec0f971",
|
||||
# "relay_addr": "127.0.0.1",
|
||||
# "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 @@
|
||||
/**
|
||||
* 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 fs from "fs";
|
||||
|
||||
import { writeFileSync } from "fs"
|
||||
// 1️⃣ File yang mau diupload
|
||||
const filePath = "image.png";
|
||||
const apiUrl = "http://localhost:3000/api/pengaduan/upload-base64";
|
||||
|
||||
interface OpenAPI {
|
||||
info: { title?: string; description?: string; version?: string }
|
||||
paths: Record<string, any>
|
||||
}
|
||||
// 2️⃣ Baca file dan ubah ke base64
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const base64Data = fileBuffer.toString("base64");
|
||||
|
||||
interface McpManifest {
|
||||
schema_version: string
|
||||
name: string
|
||||
description: string
|
||||
version?: string
|
||||
endpoints: Record<string, string>
|
||||
capabilities: Record<string, any>
|
||||
contact?: { email?: string }
|
||||
}
|
||||
// 3️⃣ Buat payload JSON
|
||||
const payload = {
|
||||
data: base64Data,
|
||||
mimetype: "image/png"
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert OpenAPI JSON to MCP manifest format
|
||||
*/
|
||||
async function convertOpenApiToMcp(baseUrl: string): Promise<McpManifest> {
|
||||
const res = await fetch(`${baseUrl}/docs/json`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch Swagger JSON from ${baseUrl}/docs/json`)
|
||||
// 4️⃣ Kirim ke server pakai fetch
|
||||
async function uploadBase64() {
|
||||
try {
|
||||
const res = await fetch(apiUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const openapi: OpenAPI = await res.json()
|
||||
|
||||
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: {}
|
||||
if (!res.ok) {
|
||||
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
for (const [path, methods] of Object.entries(openapi.paths || {})) {
|
||||
for (const [method, def] of Object.entries<any>(methods)) {
|
||||
const tags = def.tags || ["default"]
|
||||
const tag = tags[0]
|
||||
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
|
||||
const result = await res.json();
|
||||
console.log("✅ Upload sukses:", result);
|
||||
} catch (err) {
|
||||
console.error("❌ Upload gagal:", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
uploadBase64();
|
||||
|
||||
108
xx.ts
108
xx.ts
@@ -1,65 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Elysia } from 'elysia'
|
||||
import jwt, { type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||
import bearer from '@elysiajs/bearer'
|
||||
import { prisma } from '../lib/prisma'
|
||||
|
||||
// =========================================================
|
||||
// JWT Secret Validation
|
||||
// =========================================================
|
||||
const secret = process.env.JWT_SECRET
|
||||
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||
|
||||
// =========================================================
|
||||
// Auth Middleware Plugin
|
||||
// =========================================================
|
||||
export default function apiAuth(app: Elysia) {
|
||||
if (!secret) throw new Error('JWT_SECRET environment variable is missing')
|
||||
return app
|
||||
// Register Bearer and JWT plugins
|
||||
.use(bearer()) // ✅ Extracts Bearer token automatically (case-insensitive)
|
||||
.use(
|
||||
jwt({
|
||||
name: 'jwt',
|
||||
secret,
|
||||
})
|
||||
)
|
||||
|
||||
// Derive user from JWT or cookie
|
||||
.derive(async ({ bearer, cookie, jwt }) => {
|
||||
// Normalize token type to string or undefined
|
||||
const token =
|
||||
(typeof bearer === 'string' ? bearer : undefined) ??
|
||||
(typeof cookie?.token?.value === 'string' ? cookie.token.value : undefined)
|
||||
|
||||
let user: Awaited<ReturnType<typeof prisma.user.findUnique>> | null = null
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
const decoded = (await jwt.verify(token)) as JWTPayloadSpec
|
||||
|
||||
if (decoded?.sub && typeof decoded.sub === 'string') {
|
||||
user = await prisma.user.findUnique({
|
||||
where: { id: decoded.sub },
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[SERVER][apiAuth] Invalid token:', (err as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
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' }
|
||||
}
|
||||
})
|
||||
{
|
||||
"response": [
|
||||
{
|
||||
"type": "json",
|
||||
"data": {
|
||||
"success": true,
|
||||
"status": 200,
|
||||
"method": "GET",
|
||||
"path": "/api/pengaduan/category",
|
||||
"data": {
|
||||
"data": [
|
||||
{
|
||||
"id": "infrastruktur",
|
||||
"name": "Infrastruktur"
|
||||
},
|
||||
{
|
||||
"id": "cmhslcvcy0000mg0810l7zx8x",
|
||||
"name": "keamanan"
|
||||
},
|
||||
{
|
||||
"id": "keamanan",
|
||||
"name": "Keamanan"
|
||||
},
|
||||
{
|
||||
"id": "kebersihan",
|
||||
"name": "Kebersihan"
|
||||
},
|
||||
{
|
||||
"id": "lainnya",
|
||||
"name": "Lainnya"
|
||||
},
|
||||
{
|
||||
"id": "pelayanan",
|
||||
"name": "Pelayanan"
|
||||
},
|
||||
{
|
||||
"id": "cmhsl5ijj0000mg08pru6kom4",
|
||||
"name": "sampah"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user