Compare commits

...

15 Commits

Author SHA1 Message Date
236d6cfc72 upd: api pengaduan
Deskripsi:
- update api tambah pengaduan
- update api pelayanan surat

No Issues
2025-11-19 17:39:27 +08:00
482227a502 Merge pull request 'amalia/19-nov-25' (#31) from amalia/19-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/31
2025-11-19 17:10:51 +08:00
fe52fb52c6 fix: dashboard admin
Deskripsi:
- fix error pada list pengajuan surat
- menghilangkan status dikerjakan pada list pengajuan surat

No Issues
2025-11-19 17:10:00 +08:00
99247b7a44 upd: dashboard admin
Deskripsi:
- update sk penghasilan
- update sk kelakuan baik
- update sk kelahiran

NO Issues
2025-11-19 16:58:19 +08:00
73e247c87f upd: dashboard admin
Deskripsi:
- sk tidak mampu
- sk yatim piatu

No Issues
2025-11-19 15:45:44 +08:00
4b914e1852 upd: dashboard admin
Deskripsi:
- update modal surat
- api surat
- dowload surat

No Issues
2025-11-19 15:26:58 +08:00
dfc35e88d5 Merge pull request 'upd:' (#30) from amalia/18-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/30
2025-11-18 17:38:05 +08:00
79660a766c upd:
dashboard admin

Deskripsi:
- list pelayanan surat
- detail pelayanan surat
- terima pelayanan surat
- menolak pelayanan surat
- menyelesaikan pelayanan surat
- tambah surat > databse

No Issues
2025-11-18 17:37:07 +08:00
083eb11bb0 Merge pull request 'amalia/17-nov-25' (#29) from amalia/17-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/29
2025-11-17 17:26:02 +08:00
3e09c934d4 fix : list pengaduan error 2025-11-17 17:25:14 +08:00
282b9678b3 upd: dashboard admin
Deskripsi:
- detail pengaduan
- menerima pengaduan
- mengerjakan pengaduan/
- menyelesaikan pengaduan
- menolak pengaduan

No Issues
2025-11-17 17:13:11 +08:00
ceed3e67c7 Merge pull request 'upd : dahboard admin' (#28) from amalia/17-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/28
2025-11-17 13:42:32 +08:00
04b5d26507 upd : dahboard admin
Deskripsi:
- detail pengaduan

No Issues
2025-11-17 13:41:34 +08:00
327434b42e Merge pull request 'upd: warga route' (#27) from amalia/14-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/27
2025-11-14 17:27:15 +08:00
c4e4aaffe7 upd: warga route 2025-11-14 17:26:42 +08:00
21 changed files with 1896 additions and 395 deletions

View File

@@ -22,6 +22,8 @@
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"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",
@@ -139,10 +141,16 @@
"@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=="],
@@ -175,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=="],
@@ -197,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=="],
@@ -231,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=="],
@@ -239,6 +251,8 @@
"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=="],
@@ -265,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=="],
@@ -323,6 +339,8 @@
"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=="],
@@ -379,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=="],
@@ -395,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=="],
@@ -423,6 +445,8 @@
"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=="],
@@ -487,6 +511,8 @@
"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=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
@@ -539,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=="],
@@ -571,7 +599,7 @@
"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=="],
@@ -581,6 +609,8 @@
"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=="],
@@ -619,6 +649,8 @@
"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=="],
"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=="],
@@ -631,10 +663,14 @@
"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.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=="],
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
@@ -687,6 +723,8 @@
"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.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=="],
@@ -711,6 +749,10 @@
"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=="],

View File

@@ -29,6 +29,8 @@
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"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",

View File

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

View 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>
</>
)
}

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

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

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

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

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

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

View File

@@ -17,6 +17,7 @@ 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"],
@@ -33,6 +34,8 @@ const Api = new Elysia({
.use(PengaduanRoute)
.use(PelayananRoute)
.use(ConfigurationDesaRoute)
.use(WargaRoute)
.use(SuratRoute)
.use(TestPengaduanRoute)
.use(apiAuth)
.use(ApiKeyRoute)

View File

@@ -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,7 +36,7 @@ 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",
@@ -45,7 +45,7 @@ export const categoryPelayananSurat = [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "keperluan"]
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",

View File

@@ -47,7 +47,7 @@ export const confDesa = [
{
id: "perbekelNIP",
name: "NIP",
value: ""
value: "1122334455"
},
{
id: "perbekelTTD",

View File

@@ -1,3 +1,5 @@
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
@@ -9,95 +11,172 @@ import {
Flex,
Grid,
Group,
List,
Modal,
Stack,
Table,
Text,
Textarea,
Title,
ThemeIcon,
Title
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCategory,
IconCheck,
IconFileCertificate,
IconInfoTriangle,
IconMapPin,
IconFileCheck,
IconMessageReport,
IconPhotoScan,
IconUser,
IconPhone,
IconUser
} from "@tabler/icons-react";
import { useState } from "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 DetailPelayananPage() {
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"}>
<DetailDataPelayanan />
<DetailDataHistori />
<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}>
<DetailUserPelayanan />
<DetailUserPengajuan data={data?.data?.warga} />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPelayanan() {
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"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} />
<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" onClick={close}>
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
<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}>
Batal
Tidak
</Button>
<Button variant="filled" color="green" onClick={close}>
Terima
<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"
@@ -114,117 +193,139 @@ function DetailDataPelayanan() {
<Flex align="center" justify="space-between">
<Group gap="xs">
<Title order={4} c="gray.2">
Pelayanan Surat
Pengajuan {data?.category}
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
#{data?.noPengajuan}
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={"yellow"}
color={
data?.status === "diterima"
? "green"
: data?.status === "ditolak"
? "red"
: data?.status === "selesai"
? "blue"
: data?.status === "dikerjakan"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
>
antrian
{data?.status}
</Badge>
</Flex>
<Divider my={0} />
<Grid>
<Grid.Col span={6}>
<Stack gap="md">
<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">Judul</Text>
<Text size="md">Data Pelengkap</Text>
</Group>
<Text size="md" c={"white"}>
Judul Pelayanan Surat
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} />
<Text size="md">Lokasi</Text>
</Group>
<Text size="md" c="white">
fwef
</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={6}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconCategory size={20} />
<Text size="md">Kategori</Text>
</Group>
<Text size="md" c="white">
fwef
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="https://mantine.dev/" target="_blank">
Lihat Gambar
</Anchor>
<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}>
<Stack gap="md">
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
<Text size="md">Detail</Text>
{
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>
<Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
Illum, corporis iusto. Suscipit veritatis quas, non nobis
fuga, laudantium accusantium tempora sint aliquid architecto
totam esse eum excepturi nostrum fugiat ut.
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
) : 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>
<Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
suscipit incidunt quos beatae modi, vel, id ullam quae
voluptas, deserunt quas placeat.
</Text>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
<Group justify="center" grow>
<Button
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
)
}
</Grid.Col>
</Grid>
</Stack>
@@ -233,23 +334,7 @@ function DetailDataPelayanan() {
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
function DetailDataHistori({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -265,7 +350,7 @@ function DetailDataHistori() {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
Histori Pengajuan Surat
</Title>
</Flex>
<Divider my={0} />
@@ -278,33 +363,25 @@ function DetailDataHistori() {
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
<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 DetailUserPelayanan() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
function DetailUserPengajuan({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -333,16 +410,16 @@ function DetailUserPelayanan() {
<Text size="md">Nama</Text>
</Group>
<Text size="md" c={"white"}>
Amalia Dwi Yustiani
{data?.name}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={20} />
<IconPhone size={20} />
<Text size="md">Telepon</Text>
</Group>
<Text size="md" c="white">
08123456789
{data?.phone}
</Text>
</Group>
<Group justify="space-between">
@@ -351,7 +428,7 @@ function DetailUserPelayanan() {
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
10
{data?.pengaduan}
</Text>
</Group>
<Group justify="space-between">
@@ -360,7 +437,7 @@ function DetailUserPelayanan() {
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
10
{data?.pelayanan}
</Text>
</Group>
</Stack>

View File

@@ -15,16 +15,15 @@ import {
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconClockHour3,
IconFileSad,
IconMapPin,
IconSearch,
IconUser
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
import { proxy } from "valtio";
import { proxy, subscribe } from "valtio";
const state = proxy({ reload: "" });
function reloadState() {
@@ -48,20 +47,24 @@ export default function PelayananSuratListPage() {
function TabListPelayananSurat({ status }: { status: string }) {
const navigate = useNavigate();
const dataCount = useSwr("/pelayanan-surat/count", () =>
apiFetch.api.pengaduan.count.get().then((res) => res.data),
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="all"
value="semua"
onClick={() => {
navigate("?status=semua");
}}
>
Semua ({dataCount?.data?.semua || 0})
Semua ({data?.semua || 0})
</Tabs.Tab>
<Tabs.Tab
value="antrian"
@@ -69,7 +72,7 @@ function TabListPelayananSurat({ status }: { status: string }) {
navigate("?status=antrian");
}}
>
Antrian ({dataCount?.data?.antrian || 0})
Antrian ({data?.antrian || 0})
</Tabs.Tab>
<Tabs.Tab
value="diterima"
@@ -77,15 +80,7 @@ function TabListPelayananSurat({ status }: { status: string }) {
navigate("?status=diterima");
}}
>
Diterima ({dataCount?.data?.diterima || 0})
</Tabs.Tab>
<Tabs.Tab
value="dikerjakan"
onClick={() => {
navigate("?status=dikerjakan");
}}
>
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
Diterima ({data?.diterima || 0})
</Tabs.Tab>
<Tabs.Tab
value="selesai"
@@ -93,7 +88,7 @@ function TabListPelayananSurat({ status }: { status: string }) {
navigate("?status=selesai");
}}
>
Selesai ({dataCount?.data?.selesai || 0})
Selesai ({data?.selesai || 0})
</Tabs.Tab>
<Tabs.Tab
value="ditolak"
@@ -101,7 +96,7 @@ function TabListPelayananSurat({ status }: { status: string }) {
navigate("?status=ditolak");
}}
>
Ditolak ({dataCount?.data?.ditolak || 0})
Ditolak ({data?.ditolak || 0})
</Tabs.Tab>
</Tabs.List>
</Tabs>
@@ -118,21 +113,29 @@ type StatusKey =
function ListPelayananSurat({ status }: { status: StatusKey }) {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
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)
@@ -147,19 +150,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
}}
>
<Text size="sm" c="dimmed">
Loading pengaduan...
Loading pelayanan surat...
</Text>
</Card>
);
const list = data?.data || [];
const list = data || [];
return (
<Stack gap="xl">
<Group grow>
<Input
value={value}
placeholder="Cari pengaduan..."
placeholder="Cari pengajuan..."
onChange={(event) => setValue(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
@@ -182,7 +185,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
list?.map((v: any) => (
Array.isArray(list) && list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -204,11 +207,11 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={3} c="gray.2">
{v.title}
{v.category}
</Title>
<Group>
<Title order={6} c="gray.5">
#{v.noPengaduan}
#{v.noPengajuan}
</Title>
<Text size="sm" c="dimmed">
{v.updatedAt}
@@ -227,7 +230,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
: v.status === "selesai"
? "blue"
: v.status === "dikerjakan"
? "purple"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
@@ -241,28 +244,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
<Group gap="xs">
<IconClockHour3 size={20} color="white" />
<Text size="md" c="white">
Tanggal Aduan
Tanggal Ajuan
</Text>
</Group>
<Text size="md">{v.createdAt}</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconMapPin size={20} color="white" />
<IconUser size={20} color="white" />
<Text size="md" c="white">
Lokasi
Warga
</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>
<Text size="md">{v.warga}</Text>
</Flex>
</Stack>
</Stack>

View File

@@ -1,3 +1,4 @@
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Anchor,
@@ -25,10 +26,13 @@ import {
IconInfoTriangle,
IconMapPin,
IconMessageReport,
IconPhone,
IconPhotoScan,
IconUser,
} from "@tabler/icons-react";
import { useState } from "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";
@@ -36,50 +40,95 @@ 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 />
<DetailDataHistori />
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
<Grid.Col span={4}>
<DetailUserPengaduan />
<DetailUserPengaduan data={data?.data?.warga} />
</Grid.Col>
</Grid>
</Container>
);
}
function DetailDataPengaduan() {
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);
async function handleLihatGambar() {
const res = await apiFetch.api.pengaduan.image.get({
query: {
fileName: "57d5ce89-7d18-4244-9f4c-ca21b70adb7e",
},
});
console.error("client", res);
// const blob = await res.data?.blob();
// setImageSrc(URL.createObjectURL(blob!));
// openModalImage();
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"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
@@ -89,25 +138,25 @@ function DetailDataPengaduan() {
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} />
<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" onClick={close}>
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
<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}>
Batal
Tidak
</Button>
<Button variant="filled" color="green" onClick={close}>
Terima
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
Ya
</Button>
</Group>
</>
@@ -115,11 +164,12 @@ function DetailDataPengaduan() {
</Stack>
</Modal>
{/* MODAL GAMBAR */}
<Modal
opened={openedModalImage}
onClose={closeModalImage}
title="Gambar Pengaduan"
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Image src={imageSrc!} />
@@ -143,17 +193,27 @@ function DetailDataPengaduan() {
Pengaduan
</Title>
<Title order={4} c="dimmed">
#PGf-2345-33
#{data?.noPengaduan}
</Title>
</Group>
<Badge
size="xl"
variant="light"
radius="sm"
color={"yellow"}
color={
data?.status === "diterima"
? "green"
: data?.status === "ditolak"
? "red"
: data?.status === "selesai"
? "blue"
: data?.status === "dikerjakan"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}
>
antrian
{data?.status}
</Badge>
</Flex>
<Divider my={0} />
@@ -166,7 +226,7 @@ function DetailDataPengaduan() {
<Text size="md">Judul</Text>
</Group>
<Text size="md" c={"white"}>
Judul Pengaduan
{_.upperFirst(data?.title)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
@@ -175,7 +235,7 @@ function DetailDataPengaduan() {
<Text size="md">Lokasi</Text>
</Group>
<Text size="md" c="white">
fwef
{_.upperFirst(data?.location)}
</Text>
</Flex>
</Stack>
@@ -188,7 +248,7 @@ function DetailDataPengaduan() {
<Text size="md">Kategori</Text>
</Group>
<Text size="md" c="white">
fwef
{_.upperFirst(data?.category)}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
@@ -196,7 +256,7 @@ function DetailDataPengaduan() {
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
<Anchor href="#" onClick={handleLihatGambar}>
<Anchor href="#" onClick={() => { }}>
Lihat Gambar
</Anchor>
</Flex>
@@ -210,47 +270,73 @@ function DetailDataPengaduan() {
<Text size="md">Detail</Text>
</Group>
<Text size="md" c="white">
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
Illum, corporis iusto. Suscipit veritatis quas, non nobis
fuga, laudantium accusantium tempora sint aliquid architecto
totam esse eum excepturi nostrum fugiat ut.
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
</Group>
<Text size="md" c={"white"}>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
suscipit incidunt quos beatae modi, vel, id ullam quae
voluptas, deserunt quas placeat.
{_.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}>
<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 === "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>
@@ -259,23 +345,7 @@ function DetailDataPengaduan() {
);
}
function DetailDataHistori() {
const elements = [
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
];
const rows = elements.map((element) => (
<Table.Tr key={element.name}>
<Table.Td>{element.position}</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td>{element.symbol}</Table.Td>
<Table.Td>{element.mass}</Table.Td>
</Table.Tr>
));
function DetailDataHistori({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -304,33 +374,25 @@ function DetailDataHistori() {
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>{rows}</Table.Tbody>
<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() {
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
query: {
status,
search: value,
take: "",
page: "",
},
}),
);
useShallowEffect(() => {
mutate();
}, [status, value]);
const list = data?.data || [];
function DetailUserPengaduan({ data }: { data: any }) {
return (
<Card
radius="md"
@@ -359,16 +421,16 @@ function DetailUserPengaduan() {
<Text size="md">Nama</Text>
</Group>
<Text size="md" c={"white"}>
Amalia Dwi Yustiani
{data?.name}
</Text>
</Group>
<Group justify="space-between">
<Group gap="xs">
<IconMapPin size={20} />
<IconPhone size={20} />
<Text size="md">Telepon</Text>
</Group>
<Text size="md" c="white">
08123456789
{data?.phone}
</Text>
</Group>
<Group justify="space-between">
@@ -377,7 +439,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pengaduan</Text>
</Group>
<Text size="md" c="white">
10
{data?.pengaduan}
</Text>
</Group>
<Group justify="space-between">
@@ -386,7 +448,7 @@ function DetailUserPengaduan() {
<Text size="md">Jumlah Pelayanan Surat</Text>
</Group>
<Text size="md" c="white">
10
{data?.pelayanan}
</Text>
</Group>
</Stack>

View File

@@ -24,7 +24,7 @@ import {
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
import { proxy } from "valtio";
import { proxy, subscribe } from "valtio";
const state = proxy({ reload: "" });
function reloadState() {
@@ -48,20 +48,24 @@ export default function PengaduanListPage() {
function TabListPengaduan({ status }: { status: string }) {
const navigate = useNavigate();
const dataCount = useSwr("/pengaduan/count", () =>
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="all"
value="semua"
onClick={() => {
navigate("?status=semua");
}}
>
Semua ({dataCount?.data?.semua || 0})
Semua ({data?.semua || 0})
</Tabs.Tab>
<Tabs.Tab
value="antrian"
@@ -69,7 +73,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=antrian");
}}
>
Antrian ({dataCount?.data?.antrian || 0})
Antrian ({data?.antrian || 0})
</Tabs.Tab>
<Tabs.Tab
value="diterima"
@@ -77,7 +81,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=diterima");
}}
>
Diterima ({dataCount?.data?.diterima || 0})
Diterima ({data?.diterima || 0})
</Tabs.Tab>
<Tabs.Tab
value="dikerjakan"
@@ -85,7 +89,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=dikerjakan");
}}
>
Dikerjakan ({dataCount?.data?.dikerjakan || 0})
Dikerjakan ({data?.dikerjakan || 0})
</Tabs.Tab>
<Tabs.Tab
value="selesai"
@@ -93,7 +97,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=selesai");
}}
>
Selesai ({dataCount?.data?.selesai || 0})
Selesai ({data?.selesai || 0})
</Tabs.Tab>
<Tabs.Tab
value="ditolak"
@@ -101,7 +105,7 @@ function TabListPengaduan({ status }: { status: string }) {
navigate("?status=ditolak");
}}
>
Ditolak ({dataCount?.data?.ditolak || 0})
Ditolak ({data?.ditolak || 0})
</Tabs.Tab>
</Tabs.List>
</Tabs>
@@ -120,21 +124,28 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [value, setValue] = useState("");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.pengaduan.list.get({
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
@@ -152,7 +163,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Card>
);
const list = data?.data || [];
const list = data || [];
return (
<Stack gap="xl">
@@ -186,7 +197,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
list?.map((v: any) => (
Array.isArray(list) && list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -229,7 +240,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
: v.status === "selesai"
? "blue"
: v.status === "dikerjakan"
? "purple"
? "gray"
: "yellow"
}
style={{ textTransform: "none" }}

View 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' }
}
}

View File

@@ -1,5 +1,7 @@
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"
@@ -102,28 +104,190 @@ const PelayananRoute = new Elysia({
// --- PELAYANAN SURAT ---
.get("/", async () => {
.get("/", async ({ query }) => {
const { phone } = query
const data = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "asc"
},
where: {
isActive: true
isActive: true,
Warga: {
phone
}
}
})
return data
}, {
query: t.Object({
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
}),
detail: {
summary: "List Ajuan Pelayanan Surat",
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.findUnique({
const data = await prisma.pelayananAjuan.findFirst({
where: {
id,
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,
}
}
}
},
}
})
return data
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" }),
@@ -135,25 +299,25 @@ const PelayananRoute = new Elysia({
}
})
.post("/create", async ({ body }) => {
const { idCategory, idWarga, phone, dataText, syaratDokumen } = body
const { kategoriId, wargaId, noTelepon, dataText, syaratDokumen } = body
const noPengajuan = await generateNoPengajuanSurat()
let idCategoryFix = idCategory
let idWargaFix = idWarga
let idCategoryFix = kategoriId
let idWargaFix = wargaId
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: idCategory,
id: kategoriId,
}
})
if (!category) {
const cariCategory = await prisma.categoryPelayanan.findFirst({
where: {
name: idCategory,
name: kategoriId,
}
})
if (!cariCategory) {
throw new Error("kategori pelayanan surat tidak ditemukan")
return { success: false, message: 'kategori pelayanan surat tidak ditemukan' }
} else {
idCategoryFix = cariCategory.id
}
@@ -162,12 +326,12 @@ const PelayananRoute = new Elysia({
const warga = await prisma.warga.findUnique({
where: {
id: idWarga,
id: wargaId,
}
})
if (!warga) {
const nomorHP = normalizePhoneNumber({ phone })
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const cariWarga = await prisma.warga.findFirst({
where: {
phone: nomorHP,
@@ -177,7 +341,7 @@ const PelayananRoute = new Elysia({
if (!cariWarga) {
const wargaCreate = await prisma.warga.create({
data: {
name: idWarga,
name: wargaId,
phone: nomorHP,
},
select: {
@@ -203,7 +367,7 @@ const PelayananRoute = new Elysia({
})
if (!pengaduan.id) {
throw new Error("gagal membuat pengajuan surat")
return { success: false, message: 'gagal membuat pengajuan surat' }
}
let dataInsertSyaratDokumen = []
@@ -246,17 +410,81 @@ const PelayananRoute = new Elysia({
return { success: true, message: 'pengajuan surat sudah dibuat' }
}, {
body: t.Object({
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
dataText: t.Array(t.Object({
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
value: t.String({ minLength: 1, error: "value harus diisi" }),
})),
syaratDokumen: t.Array(t.Object({
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
value: t.String({ minLength: 1, error: "value harus diisi" }),
})),
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",
@@ -265,7 +493,9 @@ const PelayananRoute = new Elysia({
}
})
.post("/update-status", async ({ body }) => {
const { id, status, keterangan, idUser } = body
const { id, status, keterangan, idUser, noSurat } = body
let deskripsi = ""
const pengajuan = await prisma.pelayananAjuan.update({
where: {
@@ -273,28 +503,48 @@ const PelayananRoute = new Elysia({
},
data: {
status: status as StatusPengaduan,
},
select: {
id: true,
idCategory: true,
idWarga: true,
}
})
if (!pengajuan) {
throw new Error("gagal membuat 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: "Pengajuan surat diperbarui",
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({ minLength: 1, error: "keterangan harus diisi" }),
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
keterangan: t.String({ optional: true }),
idUser: t.String({ optional: true }),
noSurat: t.String({ optional: true }),
}),
detail: {
summary: "Update Status Pengajuan Pelayanan Surat",
@@ -302,5 +552,128 @@ const PelayananRoute = new Elysia({
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

View File

@@ -28,8 +28,8 @@ const PengaduanRoute = new Elysia({
}
})
return {data}
return { data }
}, {
detail: {
summary: "List Kategori Pengaduan",
@@ -111,27 +111,34 @@ const PengaduanRoute = new Elysia({
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({
if (idCategoryFix) {
const category = await prisma.categoryPengaduan.findUnique({
where: {
name: kategoriId,
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: wargaId,
@@ -193,21 +200,18 @@ const PengaduanRoute = new Elysia({
}, {
body: t.Object({
judulPengaduan: t.String({
minLength: 3,
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
error: "Judul pengaduan harus diisi",
examples: ["Sampah menumpuk di depan rumah"],
description: "Judul singkat dari pengaduan warga"
}),
detailPengaduan: t.String({
minLength: 5,
error: "Deskripsi pengaduan harus diisi dan minimal 10 karakter",
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({
minLength: 5,
error: "Lokasi pengaduan harus diisi",
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
description: "Alamat atau titik lokasi pengaduan"
@@ -220,21 +224,18 @@ const PengaduanRoute = new Elysia({
}),
kategoriId: t.String({
minLength: 1,
error: "ID kategori pengaduan harus diisi",
optional: true,
examples: ["kebersihan"],
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
}),
wargaId: t.String({
minLength: 1,
error: "ID warga harus diisi",
optional: true,
examples: ["budiman"],
description: "ID unik warga yang melapor (jika sudah terdaftar)"
}),
noTelepon: t.String({
minLength: 1,
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
@@ -278,7 +279,7 @@ Respon:
})
if (!pengaduan) {
throw new Error("gagal membuat pengaduan")
return { success: false, message: 'gagal update status pengaduan' }
}
if (status === "diterima") {
@@ -316,12 +317,14 @@ Respon:
})
.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
}
]
},
@@ -346,6 +349,13 @@ Respon:
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
}
}
@@ -374,27 +384,44 @@ Respon:
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

View 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