Compare commits

...

140 Commits

Author SHA1 Message Date
d6abc163fb Merge pull request 'upd: tambah satuan' (#115) from amalia/06-feb-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/115
2026-02-06 17:46:25 +08:00
9c08980bf1 upd: tambah satuan
Deskripsi:
- satuan luas tempat usaha
- satuan pendapatan perbulan
- pada tambah, edit, detail surat

No Issues
2026-02-06 14:39:58 +08:00
a2af3fbe36 Merge pull request 'upd: form tambah surat' (#114) from amalia/21-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/114
2026-01-21 11:55:14 +08:00
ec8722ffba upd: form tambah surat
Deskripsi:
- fix select jenis surat pada saat selesai input

No Issue
2026-01-21 09:02:25 +08:00
b0752dac8d Merge pull request 'amalia/20-jan-26' (#113) from amalia/20-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/113
2026-01-20 17:28:34 +08:00
a8d3a3a9ff fix: pelayanan surat
Deskripsi:
- mandatory pada form tambah pelayanan surat
- mandatory pada form update pelayanan surat

No Issues
2026-01-20 17:15:16 +08:00
f86703e7d1 rev: button cancel
Deskripsi:
- tambah button clear pada form file tambah pengajuan surat
- tambah button clear pada form file update data pengajuan surat

NO Issues
2026-01-20 10:48:37 +08:00
d8bb33cc93 Merge pull request 'amalia/15-jan-26' (#112) from amalia/15-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/112
2026-01-15 14:33:16 +08:00
5807e98069 fix: tambah pengajuan surat pengantar skck
Deskripsi:
- link nya

No Issues
2026-01-15 14:32:28 +08:00
59b4f1d73f fix: surat keterangan kelahiran
Deskripsi:
- update value tanggal_lahir_ayah

No Issues
2026-01-15 14:19:05 +08:00
8bd552ac22 fix: pelayanan surat
Deskripsi:
- link surat

NO Issues
2026-01-15 14:09:32 +08:00
5d48d06513 Merge pull request 'fix : error surat' (#111) from amalia/15-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/111
2026-01-15 11:54:25 +08:00
3da163ea1d fix : error surat 2026-01-15 11:53:10 +08:00
57e4f34eb6 Merge pull request 'amalia/14-jan-26' (#110) from amalia/14-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/110
2026-01-14 17:43:11 +08:00
3348cbe8e3 fix: pelayanan surat
Deskripsi:
- pelayanan surat

No Issues
2026-01-14 17:41:27 +08:00
727984a076 fix: update data pengajuan surat
Deskripsi:
- loading saat melakukan pencarian
- disable select dan input date saat status selesai

No Issues
2026-01-14 15:58:51 +08:00
a7a0ad7e37 Merge pull request 'amalia/13-jan-26' (#109) from amalia/13-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/109
2026-01-13 17:18:01 +08:00
9fed41cbe8 fix: testing surat 2026-01-13 17:10:59 +08:00
fc387fe8e6 fix: menu setting
deskiripsi:
- navigate
- list length

No Issues
2026-01-13 15:38:29 +08:00
80df579499 qc: nomer 2 dan 4
Deskripsi:
- button back
- breadcrumb
- active menu

No Issues
2026-01-13 15:10:08 +08:00
5bbbc15c27 Merge pull request 'fix: qc' (#108) from amalia/12-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/108
2026-01-12 17:47:04 +08:00
82765f6ef0 fix: qc
Deskripsi:
- breadcumbs
- back
- active menu

nb: blm selesai

No Issues
2026-01-12 17:43:04 +08:00
e8b5720118 Merge pull request 'amalia/09-jan-26' (#107) from amalia/09-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/107
2026-01-09 17:23:26 +08:00
01334ec573 upd: login
Deskripsi:
- update design login page

No Issues
2026-01-09 16:43:19 +08:00
98ad9b0d72 upd: loading saat melakukan aksi pada detail pengaduan
- mencegah 2x klik

NO Issues
2026-01-09 15:53:41 +08:00
c0471f47f3 upd: detail warga
Deskripsi:
- pagination pada list pengaduan dan list pengajuan surat
- search pada list pengaduan dan list pengajuan surat

No Issues
2026-01-09 15:46:30 +08:00
3d641d2035 Merge pull request 'upd: hapus console log' (#106) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/106
2026-01-08 17:38:19 +08:00
694115dbfb upd: hapus console log 2026-01-08 17:37:37 +08:00
7de5078868 Merge pull request 'upd: console log server2' (#105) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/105
2026-01-08 16:25:03 +08:00
7a3faa5719 upd: console log server2 2026-01-08 16:24:13 +08:00
ea5072d9ab Merge pull request 'upd: console log server' (#104) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/104
2026-01-08 16:03:03 +08:00
e8bb4f5a41 upd: console log server 2026-01-08 16:02:01 +08:00
d63bf024d3 Merge pull request 'upd: console log send wa' (#103) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/103
2026-01-08 15:50:37 +08:00
46f7dbf7bb upd: console log send wa 2026-01-08 15:49:51 +08:00
1adea29990 Merge pull request 'amalia/07-jan-26' (#102) from amalia/07-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/102
2026-01-07 17:11:50 +08:00
2a5b6e7b7c fix: validasi form tambah pengajuan surat 2026-01-07 17:10:28 +08:00
2117612337 upd: pengajuan surat
Deskripsi:
- form edit data pelengkap

No Issues
2026-01-07 15:23:34 +08:00
8f33ec2ffa pelayanan surat
deskripsi:
- update validasi form tambah pengajuan layanan surat
- update validasi form update pengajuan layanan surat

No Issues
2026-01-07 12:02:27 +08:00
411f61ec15 Merge pull request 'amalia/06-jan-26' (#101) from amalia/06-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/101
2026-01-06 17:45:44 +08:00
476319945e upd: validasi form create dan update pengajuan surat
Deskripsi:
- deselect false pada input select form tambah dan update pengajuan surat

No Issues
2026-01-06 17:44:16 +08:00
8480cec6ae upd: notif wa pengajian surat
Deskripsi:
- upload surat ke seafile
- update struktur db
- notif wa kirim link download surat
- api download surat

No Issues;
2026-01-06 17:00:08 +08:00
4ca5e4c4f3 Merge pull request 'upd: pengajuan surat' (#100) from amalia/05-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/100
2026-01-05 17:25:27 +08:00
75758bcbe6 upd: pengajuan surat
Deskripsi:
- send wa penolakan + lik update
- send wa diterima
- upload ke seafile
- blm selesai ngirim link surat ke wa

No Issues
2026-01-05 17:24:53 +08:00
2d336ea467 Merge pull request 'amalia/02-jan-26' (#99) from amalia/02-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/99
2026-01-02 17:32:32 +08:00
112e931bad upd: form update pengajuan surat
Deskripsi:
- tampilan saat ada status ditolak

NO Issues
2026-01-02 17:31:56 +08:00
487395bdb3 upd: notif warga
Deskripsi:
- tolak pengaduan
- terima pengaduan
- kerjakan pengaduan
- pengaduan selesai

NO Issues
2026-01-02 14:52:00 +08:00
3944e1ee82 Merge pull request 'upd: seeder' (#98) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/98
2025-12-29 11:33:00 +08:00
a9b34547f0 upd: seeder 2025-12-29 11:30:50 +08:00
211aac3d5f Merge pull request 'upd dskripsi kategori surat' (#97) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/97
2025-12-29 11:19:09 +08:00
73a2a4367c upd dskripsi kategori surat 2025-12-29 11:18:01 +08:00
a01f394e43 Merge pull request 'upd: link update pengajuan surat pada api' (#96) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/96
2025-12-29 10:56:30 +08:00
7dde0a4eb9 upd: link update pengajuan surat pada api 2025-12-29 10:55:23 +08:00
6debbf8c64 Merge pull request 'upd : update pengajuan surat' (#95) from amalia/24-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/95
2025-12-24 17:30:14 +08:00
c074e2bc0a upd : update pengajuan surat
deskripsi:
- update type form pada tambah dan update pengajuan surat
- tampilan klo tidak ada data yg dicari pada update pengajuan surat
- update seeder category pengajuan surat

No Issues
2025-12-24 17:29:25 +08:00
bab832b87f Merge pull request 'amalia/23-des-25' (#94) from amalia/23-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/94
2025-12-23 17:51:17 +08:00
8bafb88086 upd: template surat 2025-12-23 17:50:01 +08:00
777f2c04f1 upd: update kategori pelayanan surat
Deskripsi:
- update data seeder kategori pelayanan
- view more pada riwayat pengajuan surat dan pengaduan
- sort data pada api detail riwayat pengajuan surat dan pengaduan

No Issues
2025-12-23 14:33:38 +08:00
a81f6c4255 upd : setting kategori pelayanan surat
Deskripsi:
- disable tambah kategori pengajuan surat
- disable edit kategori pengajuan surat
- update json permission

No Issues
2025-12-23 11:42:30 +08:00
0f1b0196e7 Merge pull request 'amalia/22-des-25' (#93) from amalia/22-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/93
2025-12-22 17:39:31 +08:00
da86f5f10a upd: api kategori pengajuan surat
Deskripsi:
- menambahkan link tambah disetiap kategori
- perbaikan list kategori karena berubah struktur

NO Issues
2025-12-22 17:38:45 +08:00
91a3dfdb5d upd: update pelayanan surat
Deskripsi:
- pengaplikasian api
- modal konfirmasi update pelayanan surat
- modal konfirmasi create pelayanan surat

NO Issues
2025-12-22 14:37:42 +08:00
3904527c2a Merge pull request 'upd: update data pelayanan surat' (#92) from amalia/19-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/92
2025-12-19 17:28:49 +08:00
ff0413be5a upd: update data pelayanan surat
Deskripsi
- form pencarian
- detail data pengajuan
- api
- belom selesai submit

NO Issues
2025-12-19 17:27:39 +08:00
fda2b0977a Merge pull request 'upd: tambah surat' (#91) from amalia/18-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/91
2025-12-18 17:43:22 +08:00
55fbf4836d upd: tambah surat
deskripsi:
- layout form
- seeder category pelayanan
- api tambah surat

No Issues
2025-12-18 17:42:25 +08:00
c897063eb5 Merge pull request 'upd: form surat' (#90) from amalia/17-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/90
2025-12-17 17:40:40 +08:00
84161db7f2 upd: form surat
Deskripsi:
- tambah form surat
- update api
- fungsi

No Issues
2025-12-17 17:39:48 +08:00
a1766538b2 Merge pull request 'upd: form surat' (#89) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/89
2025-12-16 17:39:35 +08:00
7c6e4ac9eb upd: form surat
Deskripsi:
- api detail categori list
- form awal buat surat

No Issues
2025-12-16 17:38:13 +08:00
d8cf7833a9 Merge pull request 'upd: update api update pelayanan surat' (#88) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/88
2025-12-16 14:04:31 +08:00
a13e51a724 upd: update api update pelayanan surat 2025-12-16 14:03:15 +08:00
c585d2481d Merge pull request 'amalia/16-des-25' (#87) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/87
2025-12-16 13:51:17 +08:00
18b541116a upd: api update 2025-12-16 13:50:47 +08:00
e2d523d535 upd: update api update pelayanan surat 2025-12-16 12:30:56 +08:00
18d3b40700 Merge pull request 'upd: lower case' (#86) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/86
2025-12-16 11:51:36 +08:00
719ba0186b upd: lower case 2025-12-16 11:50:51 +08:00
baf00b1ba8 Merge pull request 'upd: console' (#85) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/85
2025-12-16 11:29:05 +08:00
026f74cc44 upd: console 2025-12-16 11:28:18 +08:00
dfc5c9144f Merge pull request 'upd: console detail data' (#84) from amalia/16-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/84
2025-12-16 11:23:57 +08:00
2badded9c3 upd: console detail data 2025-12-16 11:22:47 +08:00
f91e2dd87b Merge pull request 'upd: update data pengajuan surat' (#83) from amalia/15-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/83
2025-12-15 15:46:09 +08:00
6fb6ab9750 upd: update data pengajuan surat 2025-12-15 15:45:29 +08:00
5524f72712 Merge pull request 'upd: update edit pengajuan surat' (#82) from amalia/15-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/82
2025-12-15 14:23:24 +08:00
11a78d7371 upd: update edit pengajuan surat 2025-12-15 14:22:37 +08:00
93de9ebe9a Merge pull request 'upd: api update' (#81) from amalia/15-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/81
2025-12-15 12:18:48 +08:00
3baba059ab upd: api update 2025-12-15 12:17:59 +08:00
12eb71b96d Merge pull request 'upd: api update pelayanan pengajuan surat' (#80) from amalia/15-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/80
2025-12-15 11:32:55 +08:00
dcd072034c upd: api update pelayanan pengajuan surat 2025-12-15 11:32:02 +08:00
4cef5148ad Merge pull request 'upd: update pengajuan surat' (#79) from amalia/12-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/79
2025-12-12 17:33:08 +08:00
ee27813da7 upd: update pengajuan surat 2025-12-12 17:32:22 +08:00
7a6ea5b13d Merge pull request 'upd: api update' (#78) from amalia/12-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/78
2025-12-12 15:14:12 +08:00
29f6ecfd23 upd: api update 2025-12-12 15:13:32 +08:00
8d28e7ae6a Merge pull request 'upd: api update pelayanan surat' (#77) from amalia/12-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/77
2025-12-12 15:09:02 +08:00
d6882d4b3a upd: api update pelayanan surat 2025-12-12 15:08:09 +08:00
b7f0f4da48 Merge pull request 'upd: api jenna ai' (#76) from amalia/12-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/76
2025-12-12 14:43:42 +08:00
286c989bcf upd: api jenna ai
deskripsi:
- update pelayanan surat

No Issues:
2025-12-12 14:41:20 +08:00
031e408640 Merge pull request 'upd: api jenna ai' (#75) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/75
2025-12-11 17:06:29 +08:00
c797d1fc46 upd: api jenna ai
Deskripsi:
- detail post pengaduan
- detail post pengajuan surat

No Issues
2025-12-11 17:05:53 +08:00
6f6905a414 Merge pull request 'amalia/11-des-25' (#74) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/74
2025-12-11 14:19:34 +08:00
91e5f6a77e upd: console data 2025-12-11 14:18:43 +08:00
3f567b57b2 upd: detail api
Deskripsi
- detail pengaduan by nomer pengaduan
- detail pengajuan surat by nomer pengajuan

No Issues
2025-12-11 14:16:31 +08:00
c98cfd21ce Merge pull request 'upd: list pengaduan dan list pelayanan surat api jenna ai' (#73) from amalia/11-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/73
2025-12-11 12:05:02 +08:00
fdf7b0a13f upd: list pengaduan dan list pelayanan surat api jenna ai 2025-12-11 12:04:16 +08:00
d76a702d2d Merge pull request 'upd: api pelayanan' (#72) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/72
2025-12-10 17:25:04 +08:00
6bc6a9d357 upd: api pelayanan 2025-12-10 17:24:12 +08:00
dee32b8cfd Merge pull request 'upd: api pelayanan surat' (#71) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/71
2025-12-10 16:31:18 +08:00
ff0b0273bf upd: api pelayanan surat 2025-12-10 16:30:51 +08:00
f8dcffa9c5 Merge pull request 'amalia/10-des-25' (#70) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/70
2025-12-10 11:45:12 +08:00
20e3056e04 upd: list pengaduan 2025-12-10 11:44:22 +08:00
84c9f405d6 upd: api jenna 2025-12-10 11:40:35 +08:00
22597c0159 Merge pull request 'upd: api jenna' (#69) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/69
2025-12-10 11:34:01 +08:00
0f9af404e1 upd: api jenna 2025-12-10 11:33:05 +08:00
676edaa22b Merge pull request 'upd: api jenna' (#68) from amalia/10-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/68
2025-12-10 11:10:25 +08:00
7f6f495eaa upd: api jenna 2025-12-10 11:09:00 +08:00
b5af41b07d Merge pull request 'amalia/09-des-25' (#67) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/67
2025-12-09 17:17:31 +08:00
6428f5084e upd: api jenna ai 2025-12-09 17:16:24 +08:00
bfc292ec6c upd: api jenna ai 2025-12-09 17:15:08 +08:00
3b71976863 Merge pull request 'upd: api' (#66) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/66
2025-12-09 16:18:48 +08:00
5680466c98 upd: api 2025-12-09 16:17:39 +08:00
270f3687a3 Merge pull request 'upd: jenna ai mcp' (#65) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/65
2025-12-09 15:24:40 +08:00
f5cc45937c upd: jenna ai mcp 2025-12-09 15:23:21 +08:00
5b4164b151 Merge pull request 'upd: api pelayanan jenna ai' (#64) from amalia/09-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/64
2025-12-09 14:15:20 +08:00
225c58b346 upd: api pelayanan jenna ai 2025-12-09 14:14:04 +08:00
b8b3aed86e Merge pull request 'upd: api jenna ai' (#63) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/63
2025-12-08 16:31:00 +08:00
fc530399dd upd: api jenna ai 2025-12-08 16:28:37 +08:00
281e34ea69 Merge pull request 'upd' (#62) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/62
2025-12-08 14:51:18 +08:00
f928fc504f upd 2025-12-08 14:50:29 +08:00
4fb98d0480 Merge pull request 'upd: api' (#61) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/61
2025-12-08 14:32:54 +08:00
bfb33e2105 upd: api 2025-12-08 14:32:05 +08:00
2579714000 Merge pull request 'upd' (#60) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/60
2025-12-08 14:12:36 +08:00
d69189cf7d upd
:  api tambah pengaduan
2025-12-08 14:10:34 +08:00
20e24a03aa Merge pull request 'amalia/08-des-25' (#59) from amalia/08-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/59
2025-12-08 11:50:41 +08:00
c256f4b729 upd: api jenna ai
Deskripsi:
- create pengaduan pake nama dan nomer hp dari header

No Issues
2025-12-08 11:49:24 +08:00
9430ad3728 upd: update data pengaduan dari wawrga 2025-12-08 11:37:16 +08:00
c6c3ba95f8 upd: update data pengaduan by jenna AI 2025-12-08 11:04:03 +08:00
bipproduction
3c58230c3a tambahan untuk handle true user 2025-12-08 10:16:27 +08:00
d22b4b973f Merge pull request 'upd: api jenna ai' (#58) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/58
2025-12-02 17:27:35 +08:00
9b7a61e134 Merge pull request 'upd: api jenna ai' (#57) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/57
2025-12-02 14:40:07 +08:00
7c669f3494 Merge pull request 'fix: dashboard admin' (#56) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/56
2025-12-02 12:01:42 +08:00
b9984c6337 Merge pull request 'amalia/02-des-25' (#55) from amalia/02-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/55
2025-12-02 11:51:54 +08:00
62 changed files with 8546 additions and 3289 deletions

146
bak/ModalSurat.tsx.txt Normal file
View File

@@ -0,0 +1,146 @@
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 SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
import SKKelahiran from "./surat/SKKelahiran";
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
import SKKematian from "./surat/SKKematian";
import SKPenghasilan from "./surat/SKPenghasilan";
import SKTempatUsaha from "./surat/SKTempatUsaha";
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(`${data?.data?.surat?.nameCategory}.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} />
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
<SKDomisiliOrganisasi data={data.data} />
) : data.data.surat.idCategory == "skbedabiodata" ? (
<SKBedaBiodataDiri data={data.data} />
) : data.data.surat.idCategory == "sktempatusaha" ? (
<SKTempatUsaha data={data.data} />
) : data.data.surat.idCategory == "skbelumkawin" ? (
<SKBelumKawin data={data.data} />
) : data.data.surat.idCategory == "skkematian" ? (
<SKKematian data={data.data} />
) : (
<></>
)
) : (
<></>
)}
</div>
</Modal>
</>
);
}

310
bak/listPermission.json.txt Normal file
View File

@@ -0,0 +1,310 @@
{
"menus": [
{
"key": "dashboard",
"label": "Dashboard",
"default": true,
"children": [
{
"key": "dashboard.view",
"label": "Melihat Dashboard",
"default": true
}
]
},
{
"key": "pengaduan",
"label": "Pengaduan",
"default": true,
"children": [
{
"key": "pengaduan.view",
"label": "Melihat List & Detail",
"default": true
},
{
"key": "pengaduan.antrian",
"label": "Detail pengaduan dengan status antrian",
"default": true,
"children": [
{
"key": "pengaduan.antrian.tolak",
"label": "Menolak pengaduan",
"default": true
},
{
"key": "pengaduan.antrian.terima",
"label": "Menerima pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.diterima",
"label": "Detail pengaduan dengan status diterima",
"default": true,
"children": [
{
"key": "pengaduan.diterima.dikerjakan",
"label": "Menegerjakan pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.dikerjakan",
"label": "Detail pengaduan dengan status dikerjakan",
"default": true,
"children": [
{
"key": "pengaduan.dikerjakan.selesai",
"label": "Menyelesaikan pengaduan",
"default": true
}
]
}
]
},
{
"key": "pelayanan",
"label": "Pelayanan",
"default": true,
"children": [
{
"key": "pelayanan.view",
"label": "Melihat List & Detail",
"default": true
},
{
"key": "pelayanan.antrian",
"label": "Detail pelayanan dengan status antrian",
"default": true,
"children": [
{
"key": "pelayanan.antrian.tolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.antrian.terima",
"label": "Menerima pelayanan",
"default": true
}
]
},
{
"key": "pelayanan.diterima",
"label": "Detail pelayanan dengan status diterima",
"default": true,
"children": [
{
"key": "pelayanan.diterima.tolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.diterima.setujui",
"label": "Menyetujui pelayanan",
"default": true
}
]
}
]
},
{
"key": "warga",
"label": "Warga",
"default": true,
"children": [
{
"key": "warga.view",
"label": "Melihat List & Detail",
"default": true
}
]
},
{
"key": "setting",
"label": "Setting",
"default": true,
"children": [
{
"key": "setting.profile",
"label": "Profile",
"default": true,
"children": [
{
"key": "setting.profile.view",
"label": "View",
"default": true
},
{
"key": "setting.profile.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.profile.password",
"label": "Ubah Password",
"default": true
}
]
},
{
"key": "setting.user",
"label": "User",
"default": true,
"children": [
{
"key": "setting.user.view",
"label": "View List",
"default": true
},
{
"key": "setting.user.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.user.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.user.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.user_role",
"label": "User Role",
"default": true,
"children": [
{
"key": "setting.user_role.view",
"label": "View List",
"default": true
},
{
"key": "setting.user_role.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.user_role.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.user_role.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.kategori_pengaduan",
"label": "Kategori Pengaduan",
"default": true,
"children": [
{
"key": "setting.kategori_pengaduan.view",
"label": "View List",
"default": true
},
{
"key": "setting.kategori_pengaduan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pengaduan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pengaduan.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.kategori_pelayanan",
"label": "Kategori Pelayanan Surat",
"default": true,
"children": [
{
"key": "setting.kategori_pelayanan.view",
"label": "View List",
"default": true
},
{
"key": "setting.kategori_pelayanan.detail",
"label": "View Detail",
"default": true
},
{
"key": "setting.kategori_pelayanan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pelayanan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pelayanan.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.desa",
"label": "Desa",
"default": true,
"children": [
{
"key": "setting.desa.view",
"label": "View List",
"default": true
},
{
"key": "setting.desa.edit",
"label": "Edit",
"default": true
}
]
}
]
},
{
"key": "api_key",
"label": "API Key",
"default": true,
"children": [
{
"key": "api_key.view",
"label": "View List",
"default": true
}
]
},
{
"key": "credential",
"label": "Credential",
"default": true,
"children": [
{
"key": "credential.view",
"label": "View List",
"default": true
}
]
}
]
}

View File

@@ -22,6 +22,7 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",

View File

@@ -28,6 +28,7 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",

View File

@@ -110,7 +110,8 @@ model CategoryPelayanan {
id String @id @default(cuid())
name String
syaratDokumen Json[]
dataText String[]
dataText String[] @default([])
dataPelengkap Json[] @default([])
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -186,6 +187,7 @@ model SuratPelayanan {
Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String
noSurat String
file String?
dateExpired DateTime? @db.Date
status Int @default(0)
isActive Boolean @default(true)

View File

@@ -10,10 +10,12 @@ import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import UpdateDataSurat from "./pages/darmasaba/update_data_surat";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
import Surat from "./pages/darmasaba/surat";
import Home from "./pages/Home";
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
@@ -68,6 +70,10 @@ export default function AppRoutes() {
path="/darmasaba/surat-keterangan-domisili-organisasi"
element={<FormSuratKeteranganDomisiliOrganisasi />}
/>
<Route
path="/darmasaba/update-data-surat"
element={<UpdateDataSurat />}
/>
<Route
path="/darmasaba/surat-keterangan-belum-kawin"
element={<FormSuratKeteranganBelumKawin />}
@@ -84,6 +90,7 @@ export default function AppRoutes() {
path="/darmasaba/surat-keterangan-kelakuan-baik"
element={<FormSuratKeteranganKelakuanBaik />}
/>
<Route path="/darmasaba/surat" element={<Surat />} />
</Route>
<Route path="/" element={<Home />} />

View File

@@ -10,10 +10,12 @@ const clientRoutes = {
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
"/darmasaba/update-data-surat": "/darmasaba/update-data-surat",
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
"/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik",
"/darmasaba/surat": "/darmasaba/surat",
"/": "/",
"/scr": "/scr",
"/scr/dashboard": "/scr/dashboard",

View File

@@ -0,0 +1,44 @@
import { ActionIcon, Anchor, Breadcrumbs, Card, Group } from "@mantine/core";
import { IconChevronLeft } from "@tabler/icons-react";
import { useNavigate } from "react-router-dom";
export default function BreadCrumbs({ dataLink, back, linkBack }: { dataLink: { title: string, link: string, active: boolean }[], back?: boolean, linkBack?: string }) {
const navigate = useNavigate();
return (
<Card
radius="md"
p="sm"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Group>
{
back &&
<ActionIcon variant="outline" aria-label="Settings" radius={"lg"} onClick={() => window.history.back()}>
<IconChevronLeft size={20} stroke={1.5} />
</ActionIcon>
}
<Breadcrumbs>
{
dataLink.map((item, index) => (
<Anchor
c={item.active ? "gray.0" : "gray.5"}
onClick={() => item.active || item.link == "#" ? null : navigate(item.link)}
key={index}
>
{item.title}
</Anchor>
))
}
</Breadcrumbs>
</Group>
</Card>
)
}

View File

@@ -1,98 +1,101 @@
import apiFetch from "@/lib/apiFetch";
import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconFileCertificate, IconMessageReport, IconUsers } from "@tabler/icons-react";
import {
IconFileCertificate,
IconMessageReport,
IconUsers,
} from "@tabler/icons-react";
import useSWR from "swr";
export default function DashboardCountData() {
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.dashboard.count.get()
);
const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.dashboard.count.get(),
);
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
mutate();
}, []);
return (
<Grid>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconMessageReport size={28} />}
label="Pengaduan Hari Ini"
value={String(data?.data?.pengaduan?.today)}
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
color={"gray"}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconFileCertificate size={28} />}
label="Pengajuan Surat Hari Ini"
value={String(data?.data?.pelayanan?.today)}
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
color="gray"
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconUsers size={28} />}
label="Warga"
value={String(data?.data?.warga)}
color="blue"
/>
</Grid.Col>
</Grid>
);
return (
<Grid>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconMessageReport size={28} />}
label="Pengaduan Hari Ini"
value={String(data?.data?.pengaduan?.today)}
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
color={"gray"}
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconFileCertificate size={28} />}
label="Pengajuan Surat Hari Ini"
value={String(data?.data?.pelayanan?.today)}
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
color="gray"
/>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard
icon={<IconUsers size={28} />}
label="Warga"
value={String(data?.data?.warga)}
color="blue"
/>
</Grid.Col>
</Grid>
);
}
function MetricCard({
icon,
label,
value,
change,
color,
icon,
label,
value,
change,
color,
}: {
icon: React.ReactNode;
label: string;
value: string;
change?: string;
color: string;
icon: React.ReactNode;
label: string;
value: string;
change?: string;
color: string;
}) {
return (
<Card
radius="lg"
p="md"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
borderColor: "rgba(100,100,100,0.2)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<Stack gap={6}>
<Group gap={6}>
{icon}
<Text size="sm" c="dimmed">
{label}
</Text>
</Group>
<Flex align="center" justify="space-between">
<Text fw={600} size="xl" c="gray.0">
{value}
</Text>
{change && (
<Text size="sm" c={color}>
{change}
</Text>
)}
</Flex>
</Stack>
</Card>
);
}
return (
<Card
radius="lg"
p="md"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
borderColor: "rgba(100,100,100,0.2)",
transition: "transform 0.15s ease, box-shadow 0.15s ease",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
}
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
>
<Stack gap={6}>
<Group gap={6}>
{icon}
<Text size="sm" c="dimmed">
{label}
</Text>
</Group>
<Flex align="center" justify="space-between">
<Text fw={600} size="xl" c="gray.0">
{value}
</Text>
{change && (
<Text size="sm" c={color}>
{change}
</Text>
)}
</Flex>
</Stack>
</Card>
);
}

View File

@@ -7,77 +7,71 @@ import { useEffect, useState } from "react";
import useSWR from "swr";
export default function DashboardGrafik() {
const [options, setOptions] = useState<EChartsOption>({});
const { data, mutate, isLoading } = useSWR(
"grafik-dashboard",
async () => {
return apiFetch.api.dashboard.grafik.get().then(res => res.data);
}
);
const [options, setOptions] = useState<EChartsOption>({});
const { data, mutate, isLoading } = useSWR("grafik-dashboard", async () => {
return apiFetch.api.dashboard.grafik.get().then((res) => res.data);
});
const loadData = () => {
if (!data) return;
const option: EChartsOption = {
darkMode: true,
animation: true,
legend: {
textStyle: { color: "#fff" }
},
tooltip: {},
dataset: {
dimensions: data.dimensions,
source: data.source
},
xAxis: {
type: "category",
axisLabel: { color: "#fff" }
},
yAxis: {
type: "value",
minInterval: 1,
axisLabel: { color: "#fff" }
},
color: ["#1abc9c", "#10816aff"],
series: [
{ type: "bar" },
{ type: "bar" }
]
};
const loadData = () => {
if (!data) return;
const option: EChartsOption = {
darkMode: true,
animation: true,
legend: {
textStyle: { color: "#fff" },
},
tooltip: {},
dataset: {
dimensions: data.dimensions,
source: data.source,
},
xAxis: {
type: "category",
axisLabel: { color: "#fff" },
},
yAxis: {
type: "value",
minInterval: 1,
axisLabel: { color: "#fff" },
},
color: ["#1abc9c", "#10816aff"],
series: [{ type: "bar" }, { type: "bar" }],
};
setOptions(option);
};
setOptions(option);
};
useEffect(() => {
if (data) loadData();
}, [data]);
useEffect(() => {
if (data) loadData();
}, [data]);
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={4} c="gray.0">
Grafik Pengaduan dan Pelayanan Surat
</Title>
<Text size="sm">7 Hari Terakhir</Text>
</Flex>
<IconChartBar size={20} color="gray" />
</Flex>
<Divider my="xs" />
<Stack gap="sm">
<EChartsReact style={{ height: 400 }} option={options} />
</Stack>
</Stack>
</Card>
)
}
return (
<Card
radius="lg"
p="xl"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={4} c="gray.0">
Grafik Pengaduan dan Pelayanan Surat
</Title>
<Text size="sm">7 Hari Terakhir</Text>
</Flex>
<IconChartBar size={20} color="gray" />
</Flex>
<Divider my="xs" />
<Stack gap="sm">
<EChartsReact style={{ height: 400 }} option={options} />
</Stack>
</Stack>
</Card>
);
}

View File

@@ -1,133 +1,210 @@
import apiFetch from "@/lib/apiFetch";
import { Badge, Button, Card, Flex, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
import {
Badge,
Button,
Card,
Flex,
Group,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { useNavigate } from "react-router-dom";
import useSWR from "swr";
export default function DashboardLastData() {
const navigate = useNavigate();
const { data, mutate, isLoading } = useSWR("last-update", async () => {
const res = await apiFetch.api.dashboard["last-update"].get();
return res.data
});
const navigate = useNavigate();
const { data, mutate, isLoading } = useSWR("last-update", async () => {
const res = await apiFetch.api.dashboard["last-update"].get();
return res.data;
});
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
mutate();
}, []);
return (
<Flex justify="flex-start" gap="md">
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex
align="center"
pb={"sm"}
justify="space-between"
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
>
<Title order={4} c="gray.0">
Last update pengaduan
</Title>
<Button
variant="subtle"
size="xs"
radius="md"
onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}
>
View All
</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{data &&
Array.isArray(data.pengaduan) &&
data.pengaduan.length > 0 ? (
data.pengaduan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengaduan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pengaduan"
/>
))
) : (
<Text c="dimmed" ta={"center"}>
Tidak ada data
</Text>
)}
</Stack>
</Stack>
</Card>
return (
<Flex justify="flex-start" gap="md">
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Title order={4} c="gray.0">
Last update pengaduan
</Title>
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}>View All</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{
data && Array.isArray(data.pengaduan) && data.pengaduan.length > 0 ? data.pengaduan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengaduan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pengaduan"
/>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text>
}
</Stack>
</Stack>
</Card>
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Title order={4} c="gray.0">
Last update pelayanan surat
</Title>
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)}>View All</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{
data && Array.isArray(data.pelayanan) && data.pelayanan.length > 0 ? data.pelayanan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengajuan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pelayanan"
/>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text>
}
</Stack>
</Stack>
</Card>
</Flex>
);
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex
align="center"
pb={"sm"}
justify="space-between"
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
>
<Title order={4} c="gray.0">
Last update pelayanan surat
</Title>
<Button
variant="subtle"
size="xs"
radius="md"
onClick={() =>
navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)
}
>
View All
</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{data &&
Array.isArray(data.pelayanan) &&
data.pelayanan.length > 0 ? (
data.pelayanan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengajuan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pelayanan"
/>
))
) : (
<Text c="dimmed" ta={"center"}>
Tidak ada data
</Text>
)}
</Stack>
</Stack>
</Card>
</Flex>
);
}
function PengaduanSection({ id, nomer, judul, status, updated, kategori }: { id: string, nomer: string, judul: string, status: string, updated: string, kategori: 'pengaduan' | 'pelayanan' }) {
const navigate = useNavigate();
function PengaduanSection({
id,
nomer,
judul,
status,
updated,
kategori,
}: {
id: string;
nomer: string;
judul: string;
status: string;
updated: string;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate();
return (
<Stack
gap="xs"
onClick={() => navigate(kategori == "pelayanan" ? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}` : `/scr/dashboard/pengaduan/detail?id=${id}`)}
return (
<Stack
gap="xs"
onClick={() =>
navigate(
kategori == "pelayanan"
? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}`
: `/scr/dashboard/pengaduan/detail?id=${id}`,
)
}
>
<Flex
align="center"
pb={"sm"}
justify="space-between"
gap="md"
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
>
<Flex align="center" pb={"sm"} justify="space-between" gap="md" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}>
<Flex direction={"column"}>
<Text size="md" c="gray.2" lineClamp={1}>
{judul}
</Text>
<Group>
<Text size="sm" c="dimmed">
#{nomer} {updated}
</Text>
</Group>
</Flex>
<Tooltip label={status}>
<Badge size="xs" circle color={
status === "diterima"
? "green"
: status === "ditolak"
? "red"
: status === "selesai"
? "blue"
: status === "dikerjakan"
? "gray"
: "yellow"
} />
</Tooltip>
</Flex>
</Stack>
)
}
<Flex direction={"column"}>
<Text size="md" c="gray.2" lineClamp={1}>
{judul}
</Text>
<Group>
<Text size="sm" c="dimmed">
#{nomer} {updated}
</Text>
</Group>
</Flex>
<Tooltip label={status}>
<Badge
size="xs"
circle
color={
status === "diterima"
? "green"
: status === "ditolak"
? "red"
: status === "selesai"
? "blue"
: status === "dikerjakan"
? "gray"
: "yellow"
}
/>
</Tooltip>
</Flex>
</Stack>
);
}

View File

@@ -12,7 +12,7 @@ import {
Stack,
Table,
Title,
Tooltip
Tooltip,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react";
@@ -23,11 +23,15 @@ import useSWR from "swr";
import ModalFile from "./ModalFile";
import notification from "./notificationGlobal";
export default function DesaSetting({ permissions }: { permissions: JsonValue[] }) {
export default function DesaSetting({
permissions,
}: {
permissions: JsonValue[];
}) {
const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [img, setImg] = useState<any>()
const [img, setImg] = useState<any>();
const [openedPreview, setOpenedPreview] = useState(false);
const [viewImg, setViewImg] = useState("");
const { data, mutate, isLoading } = useSWR("/", () =>
@@ -51,13 +55,19 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
let finalData = { ...dataEdit }; // ← buffer data terbaru
if (dataEdit.name === "TTD") {
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({ file: dataEdit.value, folder: "lainnya" });
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img, folder: "lainnya" });
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({
file: dataEdit.value,
folder: "lainnya",
});
const resImg = await apiFetch.api.pengaduan.upload.post({
file: img,
folder: "lainnya",
});
if (resImg.status === 200) {
finalData = {
...finalData,
value: resImg.data?.filename || ""
value: resImg.data?.filename || "",
};
setDataEdit(finalData); // update state
@@ -70,7 +80,6 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
}
}
const res = await apiFetch.api["configuration-desa"].edit.post(finalData);
if (res.status === 200) {
@@ -100,8 +109,11 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
}
}
function chooseEdit({ data }: { data: { id: string; value: string; name: string }; }) {
function chooseEdit({
data,
}: {
data: { id: string; value: string; name: string };
}) {
setDataEdit(data);
open();
}
@@ -133,31 +145,27 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{
dataEdit.name == "TTD"
?
(
<Input.Wrapper label={dataEdit.name}>
<FileInput
clearable
placeholder="Upload TTD"
accept="image/*"
onChange={(e) => { setImg(e) }}
/>
</Input.Wrapper>
)
:
(
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</Input.Wrapper>
)
}
{dataEdit.name == "TTD" ? (
<Input.Wrapper label={dataEdit.name}>
<FileInput
clearable
placeholder="Upload TTD"
accept="image/*"
onChange={(e) => {
setImg(e);
}}
/>
</Input.Wrapper>
) : (
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</Input.Wrapper>
)}
<Group justify="center" grow>
<Button variant="light" onClick={close}>
@@ -199,25 +207,37 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
{list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>
{
v.name == "TTD"
?
v.value ?
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
Lihat
</Anchor>
:
"-"
:
v.value
}
{v.name == "TTD" ? (
v.value ? (
<Anchor
href="#"
onClick={() => {
setViewImg(v.value);
setOpenedPreview(true);
}}
underline="always"
>
Lihat
</Anchor>
) : (
"-"
)
) : (
v.value
)}
</Table.Td>
<Table.Td>
<Tooltip label={permissions.includes("setting.desa.edit") ? "Edit Setting" : "Edit Setting - Anda tidak memiliki akses"}>
<Tooltip
label={
permissions.includes("setting.desa.edit")
? "Edit Setting"
: "Edit Setting - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"

View File

@@ -0,0 +1,38 @@
import { Center, Loader, Overlay, Stack, Text } from "@mantine/core";
type FullScreenLoadingProps = {
visible: boolean;
text?: string;
};
export default function FullScreenLoading({
visible,
text = "Memproses data...",
}: FullScreenLoadingProps) {
if (!visible) return null;
return (
<Overlay fixed blur={6} backgroundOpacity={0.3} zIndex={10000}>
<Center h="100%">
<Stack align="center" justify="center">
<Loader size="lg" />
<Text size="sm" c="dimmed">
{text}
</Text>
</Stack>
</Center>
</Overlay>
);
}
const overlayStyle: React.CSSProperties = {
position: "fixed",
inset: 0,
zIndex: 9999,
backdropFilter: "blur(6px)",
backgroundColor: "rgba(255, 255, 255, 0.6)",
};
const contentStyle: React.CSSProperties = {
flexDirection: "column",
};

View File

@@ -4,26 +4,27 @@ import {
Button,
Divider,
Flex,
Grid,
Group,
Input,
List,
Modal,
Stack,
Table,
TagsInput,
Text,
Title,
Tooltip,
Tooltip
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
import { IconEye, IconTrash } from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library";
import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function KategoriPelayananSurat({ permissions }: { permissions: JsonValue[] }) {
export default function KategoriPelayananSurat({
permissions,
}: {
permissions: JsonValue[];
}) {
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [openedDetail, { open: openDetail, close: closeDetail }] =
@@ -40,25 +41,24 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
const [dataChoose, setDataChoose] = useState({
id: "",
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
const [dataTambah, setDataTambah] = useState({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const cleanedDataText = dataTambah.dataText
.map((v) => v.trim())
const cleanedDataText = dataTambah.dataPelengkap
.map((v) => v.name.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataTambah.syaratDokumen
.map((item) => ({
@@ -79,8 +79,8 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
closeTambah();
setDataTambah({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
notification({
title: "Success",
@@ -109,8 +109,8 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
async function handleEdit() {
try {
setBtnLoading(true);
const cleanedDataText = dataChoose.dataText
.map((v) => v.trim())
const cleanedDataText = dataChoose.dataPelengkap
.map((v) => v.name.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataChoose.syaratDokumen
.map((item) => ({
@@ -188,7 +188,10 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
function handleAddSyarat() {
setDataChoose({
...dataChoose,
syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }],
syaratDokumen: [
...dataChoose.syaratDokumen,
{ key: "", name: "", desc: "" },
],
});
}
@@ -201,7 +204,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
function handleEditSyarat(
index: number,
data: { name: string; desc: string },
data: { key: string; name: string; desc: string },
) {
setDataChoose({
...dataChoose,
@@ -214,7 +217,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
return (
<>
{/* Modal Edit */}
<Modal
{/* <Modal
opened={opened}
onClose={close}
title={"Edit"}
@@ -230,15 +233,85 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataChoose.dataText}
onChange={(value) =>
setDataChoose({ ...dataChoose, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
Data Pelengkap
</Text>
<Tooltip label="Tambah Data Pelengkap">
<ActionIcon
variant="light"
size="sm"
color="blue"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={handleAddSyarat}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
{dataChoose?.dataPelengkap?.map((v: any, i: number) => (
<Grid
key={i}
style={{
borderBottom: "1px solid gray",
paddingBottom: "10px",
}}
>
<Grid.Col
span={1}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Tooltip label="Delete Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
handleDeleteSyarat(i);
}}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Label">
<Input
value={v.name}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: e.target.value,
desc: v.desc,
})
}
/>
</Input.Wrapper>
</Grid.Col>
<Grid.Col span={6}>
<Input.Wrapper label="Deskripsi">
<Input
value={v.desc}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: v.name,
desc: e.target.value,
})
}
/>
</Input.Wrapper>
</Grid.Col>
</Grid>
))}
</Flex>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
@@ -287,11 +360,12 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Nama">
<Input.Wrapper label="Label">
<Input
value={v.name}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: e.target.value,
desc: v.desc,
})
@@ -305,6 +379,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
value={v.desc}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: v.name,
desc: e.target.value,
})
@@ -325,10 +400,10 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
</Button>
</Group>
</Stack>
</Modal>
</Modal> */}
{/* Modal Tambah */}
<Modal
{/* <Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
@@ -344,15 +419,6 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataTambah.dataText}
onChange={(value) =>
setDataTambah({ ...dataTambah, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
@@ -369,7 +435,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
...dataTambah,
syaratDokumen: [
...dataTambah.syaratDokumen,
{ name: "", desc: "" },
{ key: "", name: "", desc: "" },
],
});
}}
@@ -462,7 +528,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
</Button>
</Group>
</Stack>
</Modal>
</Modal> */}
{/* Modal Delete */}
<Modal
@@ -521,8 +587,8 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
Data Pelengkap
</Text>
<List>
{dataChoose?.dataText?.map((v: any) => (
<List.Item key={v.id}>{v}</List.Item>
{dataChoose?.dataPelengkap?.map((v: any) => (
<List.Item key={v.id}>{v.name}</List.Item>
))}
</List>
</Flex>
@@ -535,19 +601,17 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
<Title order={4} c="gray.2">
Kategori Pelayanan Surat
</Title>
{
permissions.includes("setting.kategori_pelayanan.tambah") && (
<Tooltip label="Tambah Kategori Pelayanan Surat">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)
}
{/* {permissions.includes("setting.kategori_pelayanan.tambah") && (
<Tooltip label="Tambah Kategori Pelayanan Surat">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)} */}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
@@ -559,7 +623,7 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list?.map((v: any) => (
{list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td>
<Table.Td>
@@ -578,7 +642,15 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
<IconEye size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pelayanan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
{/* <Tooltip
label={
permissions.includes(
"setting.kategori_pelayanan.edit",
)
? "Edit Kategori"
: "Edit Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
@@ -587,12 +659,24 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
setDataChoose(v);
open();
}}
disabled={!permissions.includes("setting.kategori_pelayanan.edit")}
disabled={
!permissions.includes(
"setting.kategori_pelayanan.edit",
)
}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pelayanan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
</Tooltip> */}
<Tooltip
label={
permissions.includes(
"setting.kategori_pelayanan.delete",
)
? "Hapus Kategori"
: "Hapus Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
@@ -602,7 +686,11 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes("setting.kategori_pelayanan.delete")}
disabled={
!permissions.includes(
"setting.kategori_pelayanan.delete",
)
}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -20,7 +20,11 @@ import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function KategoriPengaduan({ permissions }: { permissions: JsonValue[] }) {
export default function KategoriPengaduan({
permissions,
}: {
permissions: JsonValue[];
}) {
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [btnDisable, setBtnDisable] = useState(true);
@@ -294,19 +298,17 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
<Title order={4} c="gray.2">
Kategori Pengaduan
</Title>
{
permissions.includes("setting.kategori_pengaduan.tambah") && (
<Tooltip label="Tambah Kategori Pengaduan">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)
}
{permissions.includes("setting.kategori_pengaduan.tambah") && (
<Tooltip label="Tambah Kategori Pengaduan">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
@@ -323,18 +325,38 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
<Table.Td>{v.name}</Table.Td>
<Table.Td>
<Group>
<Tooltip label={permissions.includes("setting.kategori_pengaduan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}>
<Tooltip
label={
permissions.includes(
"setting.kategori_pengaduan.edit",
)
? "Edit Kategori"
: "Edit Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes("setting.kategori_pengaduan.edit") || v.id == "lainnya"}
disabled={
!permissions.includes(
"setting.kategori_pengaduan.edit",
) || v.id == "lainnya"
}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pengaduan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}>
<Tooltip
label={
permissions.includes(
"setting.kategori_pengaduan.delete",
)
? "Hapus Kategori"
: "Hapus Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
@@ -344,7 +366,11 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes("setting.kategori_pengaduan.delete") || v.id == "lainnya"}
disabled={
!permissions.includes(
"setting.kategori_pengaduan.delete",
) || v.id == "lainnya"
}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -3,92 +3,100 @@ import { Flex, Image, Loader, Modal } from "@mantine/core";
import { useEffect, useState } from "react";
import notification from "./notificationGlobal";
export default function ModalFile({ open, onClose, folder, fileName }: { open: boolean, onClose: () => void, folder: string, fileName: string }) {
const [viewFile, setViewFile] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [typeFile, setTypeFile] = useState<string>("");
const [error, setError] = useState<boolean>(false);
export default function ModalFile({
open,
onClose,
folder,
fileName,
}: {
open: boolean;
onClose: () => void;
folder: string;
fileName: string;
}) {
const [viewFile, setViewFile] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [typeFile, setTypeFile] = useState<string>("");
const [error, setError] = useState<boolean>(false);
useEffect(() => {
if (open && fileName) {
loadImage();
useEffect(() => {
if (open && fileName) {
loadImage();
}
}, [open, fileName]);
const loadImage = async () => {
try {
setViewFile("");
setLoading(true);
// detect type of file
const { ext, type } = detectFileType(fileName);
setTypeFile(type || "");
// load file
const urlApi =
"/api/pengaduan/image?folder=" + folder + "&fileName=" + fileName;
const res = await fetch(urlApi);
if (!res.ok) {
setError(true);
return notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
}
}, [open, fileName]);
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewFile(url);
} catch (err) {
setError(true);
notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
} finally {
setLoading(false);
}
};
const loadImage = async () => {
try {
setViewFile("");
setLoading(true);
useEffect(() => {
if (error) {
onClose();
}
}, [error]);
// detect type of file
const { ext, type } = detectFileType(fileName);
setTypeFile(type || "");
// load file
const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName;
const res = await fetch(urlApi);
if (!res.ok) {
setError(true);
return notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewFile(url);
} catch (err) {
setError(true);
notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (error) {
onClose();
}
}, [error]);
return (
<Modal
opened={open}
onClose={onClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="xl"
withCloseButton
removeScrollProps={{ allowPinchZoom: true }}
title="File"
>
{loading && (
<Flex justify="center" align="center" h={200}>
<Loader />
</Flex>
)}
{viewFile && (
<>
{typeFile == "pdf" ? (
<embed src={viewFile} type="application/pdf" width="100%" height="950" />
) : (
<Image
radius="md"
h={300}
fit="contain"
src={viewFile}
/>
)}
</>
)}
</Modal>
);
return (
<Modal
opened={open}
onClose={onClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="xl"
withCloseButton
removeScrollProps={{ allowPinchZoom: true }}
title="File"
>
{loading && (
<Flex justify="center" align="center" h={200}>
<Loader />
</Flex>
)}
{viewFile && (
<>
{typeFile == "pdf" ? (
<embed
src={viewFile}
type="application/pdf"
width="100%"
height="950"
/>
) : (
<Image radius="md" h={300} fit="contain" src={viewFile} />
)}
</>
)}
</Modal>
);
}

View File

@@ -1,10 +1,9 @@
import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core";
import { Flex, Modal, Progress, Stack, Text } 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 { useRef, useState } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
@@ -18,122 +17,190 @@ 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();
}, []);
export default function ModalSurat({
open,
onClose,
surat,
}: {
open: boolean;
onClose: (val: { success: boolean, data: string }) => void;
surat: string;
}) {
const A4Style = {
width: "210mm",
height: "297mm",
padding: "20mm",
background: "#fff",
color: "#000",
fontSize: "14px",
fontFamily: "Times New Roman",
};
const [uploading, setUploading] = useState<{ text: "Menyiapkan" | "Mengupload" | "Selesai" | "Gagal", value: number }>({ text: "Menyiapkan", value: 10 })
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
query: {
id: surat,
},
}),
);
const downloadPDF = async () => {
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
useShallowEffect(() => {
mutate();
}, []);
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const uploadPdf = async () => {
try {
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
setUploading({ text: "Mengupload", value: 75 });
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
};
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
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>
// ⬇️ ambil sebagai Blob
const pdfBlob = pdf.output("blob");
<Flex gap={8}>
<ActionIcon size={32} variant="default">
<IconDownload size={20} onClick={downloadPDF} />
</ActionIcon>
const pdfFile = new File(
[pdfBlob],
`${data?.data?.surat?.nameCategory}.pdf`,
{
type: "application/pdf",
lastModified: Date.now(),
}
);
<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} />
: data.data.surat.idCategory == "skdomisiliorganisasi"
? <SKDomisiliOrganisasi data={data.data} />
: data.data.surat.idCategory == "skbedabiodata"
? <SKBedaBiodataDiri data={data.data} />
: data.data.surat.idCategory == "sktempatusaha"
? <SKTempatUsaha data={data.data} />
: data.data.surat.idCategory == "skbelumkawin"
? <SKBelumKawin data={data.data} />
: data.data.surat.idCategory == "skkematian"
? <SKKematian data={data.data} />
: <></>
: <></>
}
</div>
</Modal>
</>
)
}
const resImg = await apiFetch.api.pengaduan.upload.post({
file: pdfFile,
folder: "surat",
});
const resUpdate = await apiFetch.api.surat.update.post({
id: surat,
filename: resImg.data?.filename!,
});
if (resUpdate?.data?.success) {
setUploading({ text: "Selesai", value: 100 });
setTimeout(() => {
onClose({ success: true, data: resUpdate.data?.link });
}, 1000)
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose({ success: false, data: "" });
}, 1000)
}
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose({ success: false, data: "" });
}, 1000)
}
} catch (error) {
console.error("Error uploading PDF:", error);
}
}
useShallowEffect(() => {
if (open) {
setTimeout(() => {
uploadPdf();
}, 3000);
}
}, [surat, open]);
return (
<>
<Modal
opened={open}
onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
closeOnClickOutside={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} align="center">
<Loader color="blue" size="xs" />
<Text size="sm">{uploading.text}</Text>
</Flex> */}
</Flex>
<Stack
align="stretch"
justify="center"
gap="xs"
>
<Text size="sm" ta="center">{uploading.text} - Harap menunggu sampai selesai</Text>
<Progress radius="md" value={uploading.value} animated size="lg" />
</Stack>
</>
}
>
<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} />
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
<SKDomisiliOrganisasi data={data.data} />
) : data.data.surat.idCategory == "skbedabiodata" ? (
<SKBedaBiodataDiri data={data.data} />
) : data.data.surat.idCategory == "sktempatusaha" ? (
<SKTempatUsaha data={data.data} />
) : data.data.surat.idCategory == "skbelumkawin" ? (
<SKBelumKawin data={data.data} />
) : data.data.surat.idCategory == "skkematian" ? (
<SKKematian data={data.data} />
) : (
<></>
)
) : (
<></>
)}
</div>
</Modal>
</>
);
}

View File

@@ -0,0 +1,45 @@
import { Button, Center, Group, Stack, Text, Title } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
export function DataNotFound({
onRetry,
backTo,
}: {
onRetry?: () => void;
backTo?: () => void;
}) {
return (
<Center mih={320}>
<Stack align="center" gap="sm">
<IconSearch size={64} opacity={0.5} />
<Title order={4}>Data Pengajuan Tidak Ditemukan</Title>
<Text size="sm" c="dimmed" ta="center" maw={380}>
Kami tidak dapat menemukan data pengajuan dengan nomor pengajuan yg
diinputkan. Silakan periksa kembali data Anda.
</Text>
<Group mt="md">
{/* {onRetry && (
<Button
variant="light"
leftSection={<IconSearch size={16} />}
onClick={onRetry}
>
Cari Ulang
</Button>
)} */}
<Button
variant="outline"
// leftSection={<IconArrowLeft size={16} />}
onClick={backTo}
>
Cari Kembali
</Button>
</Group>
</Stack>
</Center>
);
}

View File

@@ -3,65 +3,66 @@ import { Anchor, Flex, Stack, Text } from "@mantine/core";
import { useState } from "react";
interface Node {
label: string;
children: any;
actions: string[];
label: string;
children: any;
actions: string[];
}
function RenderNode({ node }: { node: Node }) {
const sub = Object.values(node.children || {});
const sub = Object.values(node.children || {});
return (
<Stack pl="md" gap={6}>
{/* Title */}
<Text size="sm">- {node.label}</Text>
return (
<Stack pl="md" gap={6}>
{/* Title */}
<Text size="sm">- {node.label}</Text>
{/* Children */}
{sub.map((child: any, i) => (
<RenderNode key={i} node={child} />
))}
</Stack>
);
{/* Children */}
{sub.map((child: any, i) => (
<RenderNode key={i} node={child} />
))}
</Stack>
);
}
function RenderNode2({ node }: { node: Node }) {
const sub = Object.values(node.children || {});
const sub = Object.values(node.children || {});
return (
<Flex direction={"row"} wrap={'wrap'} gap={6}>
{/* Title */}
<Text size="sm">{node.label},</Text>
return (
<Flex direction={"row"} wrap={"wrap"} gap={6}>
{/* Title */}
<Text size="sm">{node.label},</Text>
{/* Children */}
{sub.map((child: any, i) => (
<RenderNode2 key={i} node={child} />
))}
</Flex>
);
{/* Children */}
{sub.map((child: any, i) => (
<RenderNode2 key={i} node={child} />
))}
</Flex>
);
}
export default function PermissionRole({ permissions }: { permissions: string[] }) {
const [showAll, setShowAll] = useState(false);
if (!permissions?.length) return <Text c="dimmed">-</Text>;
export default function PermissionRole({
permissions,
}: {
permissions: string[];
}) {
const [showAll, setShowAll] = useState(false);
if (!permissions?.length) return <Text c="dimmed">-</Text>;
const groups = groupPermissions(permissions);
const rootNodes = Object.values(groups);
const groups = groupPermissions(permissions);
const rootNodes = Object.values(groups);
return (
<Stack gap="sm">
{
showAll ?
rootNodes.map((node: any, idx) => (
<RenderNode key={idx} node={node} />
))
:
rootNodes.slice(0, 2).map((node: any, idx) => (
<RenderNode2 key={idx} node={node} />
))
}
<Anchor size="xs" onClick={() => setShowAll(!showAll)} >
{showAll ? "View less" : "View more"}
</Anchor>
</Stack>
);
return (
<Stack gap="sm">
{showAll
? rootNodes.map((node: any, idx) => (
<RenderNode key={idx} node={node} />
))
: rootNodes
.slice(0, 2)
.map((node: any, idx) => <RenderNode2 key={idx} node={node} />)}
<Anchor size="xs" onClick={() => setShowAll(!showAll)}>
{showAll ? "View less" : "View more"}
</Anchor>
</Stack>
);
}

View File

@@ -1,177 +1,183 @@
import permissionConfig from "@/lib/listPermission.json";
import { ActionIcon, Checkbox, Collapse, Group, Stack, Text } from "@mantine/core";
import {
ActionIcon,
Checkbox,
Collapse,
Group,
Stack,
Text,
} from "@mantine/core";
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
import { useState } from "react";
interface Node {
label: string;
key: string;
children?: Node[];
label: string;
key: string;
children?: Node[];
}
export default function PermissionTree({
selected,
onChange,
selected,
onChange,
}: {
selected: string[];
onChange: (val: string[]) => void;
selected: string[];
onChange: (val: string[]) => void;
}) {
// Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
// Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
function toggleNode(label: string) {
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
}
function toggleNode(label: string) {
setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
}
function getAllChildKeys(node: Node): string[] {
let result: string[] = [];
if (node.children) {
node.children.forEach((c) => {
result.push(c.key);
result = [...result, ...getAllChildKeys(c)];
});
function getAllChildKeys(node: Node): string[] {
let result: string[] = [];
if (node.children) {
node.children.forEach((c) => {
result.push(c.key);
result = [...result, ...getAllChildKeys(c)];
});
}
return result;
}
// Dapatkan parentKey, jika ada
function getParentKey(key: string) {
const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
// Update parent ke atas secara rekursif
function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const allChildKeys = findAllChildKeysFromKey(parentKey);
const selectedChild = allChildKeys.filter((c) => next.includes(c));
if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
next = next.filter((x) => x !== parentKey);
} else if (selectedChild.length === allChildKeys.length) {
// Semua child check → parent check
if (!next.includes(parentKey)) {
next.push(parentKey);
}
return result;
}
// Dapatkan parentKey, jika ada
function getParentKey(key: string) {
const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
// Update parent ke atas secara rekursif
function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const allChildKeys = findAllChildKeysFromKey(parentKey);
const selectedChild = allChildKeys.filter((c) => next.includes(c));
if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
next = next.filter((x) => x !== parentKey);
} else if (selectedChild.length === allChildKeys.length) {
// Semua child check → parent check
if (!next.includes(parentKey)) {
next.push(parentKey);
}
} else {
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
next.push(parentKey);
}
} else {
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
next.push(parentKey);
}
}
// Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey));
}
// Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey));
}
// dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
// dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = [];
function traverse(nodes: Node[]) {
nodes.forEach((n) => {
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
list.push(n.key);
}
if (n.children) traverse(n.children);
});
}
function traverse(nodes: Node[]) {
nodes.forEach((n) => {
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
list.push(n.key);
}
if (n.children) traverse(n.children);
});
}
traverse(permissionConfig.menus);
return list;
}
traverse(permissionConfig.menus);
return list;
}
const RenderMenu = ({ menu }: { menu: Node }) => {
const hasChild = menu.children && menu.children.length > 0;
const open = openNodes[menu.label] ?? false;
const childKeys = getAllChildKeys(menu);
const isChecked = selected.includes(menu.key);
const isIndeterminate =
!isChecked &&
selected.some(
(x) =>
typeof x === "string" &&
x.startsWith(menu.key + ".")
);
function handleCheck() {
let next = [...selected];
if (childKeys.length > 0) {
// klik parent
if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<Stack gap={4}>
<Group gap="xs">
{menu.children && menu.children.length > 0 ? (
<ActionIcon
variant="subtle"
onClick={() => toggleNode(menu.label)}
>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon>
) : (
<div style={{ width: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack>
const RenderMenu = ({ menu }: { menu: Node }) => {
const hasChild = menu.children && menu.children.length > 0;
const open = openNodes[menu.label] ?? false;
const childKeys = getAllChildKeys(menu);
const isChecked = selected.includes(menu.key);
const isIndeterminate =
!isChecked &&
selected.some(
(x) => typeof x === "string" && x.startsWith(menu.key + "."),
);
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus.filter((menu: Node) => !menu.key.startsWith("api") && !menu.key.startsWith("credential")).map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
function handleCheck() {
let next = [...selected];
if (childKeys.length > 0) {
// klik parent
if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<Stack gap={4}>
<Group gap="xs">
{menu.children && menu.children.length > 0 ? (
<ActionIcon variant="subtle" onClick={() => toggleNode(menu.label)}>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon>
) : (
<div style={{ width: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack>
);
);
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus
.filter(
(menu: Node) =>
!menu.key.startsWith("api") && !menu.key.startsWith("credential"),
)
.map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
</Stack>
);
}

View File

@@ -13,7 +13,11 @@ import type { JsonValue } from "generated/prisma/runtime/library";
import { useEffect, useState } from "react";
import notification from "./notificationGlobal";
export default function ProfileUser({ permissions }: { permissions: JsonValue[] }) {
export default function ProfileUser({
permissions,
}: {
permissions: JsonValue[];
}) {
const [opened, setOpened] = useState(false);
const [openedPassword, setOpenedPassword] = useState(false);
const [pwdBaru, setPwdBaru] = useState("");
@@ -127,21 +131,17 @@ export default function ProfileUser({ permissions }: { permissions: JsonValue[]
Profile Pengguna
</Title>
<Group gap="md">
{
permissions.includes("setting.profile.edit") && (
<Button variant="light" onClick={() => setOpened(true)}>
Edit
</Button>
)
}
{permissions.includes("setting.profile.edit") && (
<Button variant="light" onClick={() => setOpened(true)}>
Edit
</Button>
)}
{
permissions.includes("setting.profile.password") && (
<Button variant="light" onClick={() => setOpenedPassword(true)}>
Ubah Password
</Button>
)
}
{permissions.includes("setting.profile.password") && (
<Button variant="light" onClick={() => setOpenedPassword(true)}>
Ubah Password
</Button>
)}
</Group>
</Flex>
<Divider my={0} />

View File

@@ -0,0 +1,49 @@
import { Badge, Button, Card, Center, Stack, Text, Title } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
type SuccessPengajuanProps = {
noPengajuan: string;
onClose?: () => void;
category?: "create" | "update";
};
export default function SuccessPengajuan({
noPengajuan,
onClose,
category,
}: SuccessPengajuanProps) {
return (
<Center h="100vh">
<Card shadow="md" radius="md" p="xl" withBorder maw={520} w="100%">
<Stack align="center" gap="md">
<IconCheck size={56} color="green" />
<Title order={3} ta="center">
{category == "create"
? "Pengajuan Berhasil Dibuat"
: "Pengajuan Berhasil Diupdate"}
</Title>
<Text ta="center" size="sm" c="dimmed">
{category == "create"
? "Pengajuan layanan surat sudah dibuat dengan nomor:"
: "Pengajuan layanan surat sudah diupdate dengan nomor:"}
</Text>
<Badge size="xl" variant="light" color="green">
{noPengajuan}
</Badge>
<Text ta="center" size="sm">
Nomor ini akan digunakan untuk mengakses dan memantau status
pengajuan surat Anda.
</Text>
<Button fullWidth mt="md" onClick={onClose}>
Selesai
</Button>
</Stack>
</Card>
</Center>
);
}

View File

@@ -1,17 +1,17 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Text,
Title,
Tooltip
ActionIcon,
Button,
Divider,
Flex,
Group,
Input,
Modal,
Stack,
Table,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
@@ -24,404 +24,449 @@ import PermissionRole from "./PermissionRole";
import PermissionTree from "./PermissionTree";
interface MenuNode {
key: string;
label: string;
default: boolean;
children?: MenuNode[];
key: string;
label: string;
default: boolean;
children?: MenuNode[];
}
export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [dataDelete, setDataDelete] = useState("");
const {
data: dataRole,
mutate: mutateRole,
isLoading: isLoadingRole,
} = useSWR("user-role", () => apiFetch.api.user.role.get());
const [openedTambah, { open: openTambah, close: closeTambah }] =
useDisclosure(false);
const { data, mutate, isLoading } = useSWR("role-list", () =>
apiFetch.api.user.role.get(),
);
const list = data?.data || [];
const listRole = dataRole?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
permissions: [],
});
const [dataTambah, setDataTambah] = useState({
name: "",
permissions: [],
});
const [error, setError] = useState({
name: false,
permissions: false,
});
export default function UserRoleSetting({
permissions,
}: {
permissions: JsonValue[];
}) {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false);
const [dataDelete, setDataDelete] = useState("");
const {
data: dataRole,
mutate: mutateRole,
isLoading: isLoadingRole,
} = useSWR("user-role", () => apiFetch.api.user.role.get());
const [openedTambah, { open: openTambah, close: closeTambah }] =
useDisclosure(false);
const { data, mutate, isLoading } = useSWR("role-list", () =>
apiFetch.api.user.role.get(),
);
const list = data?.data || [];
const listRole = dataRole?.data || [];
const [dataEdit, setDataEdit] = useState({
id: "",
name: "",
permissions: [],
});
const [dataTambah, setDataTambah] = useState({
name: "",
permissions: [],
});
const [error, setError] = useState({
name: false,
permissions: false,
});
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
mutate();
}, []);
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-create"].post(dataTambah as any);
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah({
name: "",
permissions: [],
});
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to create role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-delete"].post({ id: dataDelete });
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your role have been deleted",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
function chooseEdit({ data }: { data: { id: string; name: string; permissions: []; }; }) {
setDataEdit({
id: data.id, name: data.name, permissions: data.permissions ? data.permissions : []
});
open();
}
function onValidation({ kat, value, aksi, }: { kat: "name" | "permission"; value: string | null; aksi: "edit" | "tambah"; }) {
if (value == null || value.length < 1) {
setBtnDisable(true);
setError({ ...error, [kat]: true });
async function handleCreate() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-create"].post(
dataTambah as any,
);
if (res.status === 200) {
mutate();
closeTambah();
setDataTambah({
name: "",
permissions: [],
});
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
setBtnDisable(false);
setError({ ...error, [kat]: false });
notification({
title: "Error",
message: "Failed to create role",
type: "error",
});
}
if (aksi === "edit") {
setDataEdit({ ...dataEdit, [kat]: value });
} else {
setDataTambah({ ...dataTambah, [kat]: value });
}
}
function buildOrderList(menus: MenuNode[]): string[] {
const list: string[] = [];
const traverse = (nodes: MenuNode[]) => {
nodes.forEach((node) => {
list.push(node.key);
if (node.children) traverse(node.children);
});
};
traverse(menus);
return list;
}
function sortByJsonOrder(arrayData: string[]): string[] {
const orderList = buildOrderList(listMenu.menus);
return arrayData.sort((a, b) => {
return orderList.indexOf(a) - orderList.indexOf(b);
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create role",
type: "error",
});
}
} finally {
setBtnLoading(false);
}
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
}
}, [dataEdit.id]);
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper label="Nama Role">
<Input
value={dataEdit.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "edit",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({ ...dataEdit, permissions: sortByJsonOrder(permissions) as never[] });
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button
variant="filled"
onClick={handleEdit}
disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-delete"].post({
id: dataDelete,
});
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your role have been deleted",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper
label="Nama Role"
description=""
error={error.name ? "Field is required" : ""}
>
<Input
value={dataTambah.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({ ...dataTambah, permissions: sortByJsonOrder(permissions) as never[] });
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button
variant="filled"
onClick={handleCreate}
disabled={
btnDisable ||
dataTambah.name.length < 1 ||
dataTambah.permissions.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
function chooseEdit({
data,
}: {
data: { id: string; name: string; permissions: [] };
}) {
setDataEdit({
id: data.id,
name: data.name,
permissions: data.permissions ? data.permissions : [],
});
open();
}
{/* Modal Delete */}
<Modal
opened={openedDelete}
onClose={closeDelete}
title={"Delete"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Text size="md" color="gray.6">
Apakah anda yakin ingin menghapus role ini?
</Text>
<Group justify="center" grow>
<Button variant="light" onClick={closeDelete}>
Batal
</Button>
<Button
variant="filled"
color="red"
onClick={handleDelete}
loading={btnLoading}
>
Hapus
</Button>
</Group>
</Stack>
</Modal>
function onValidation({
kat,
value,
aksi,
}: {
kat: "name" | "permission";
value: string | null;
aksi: "edit" | "tambah";
}) {
if (value == null || value.length < 1) {
setBtnDisable(true);
setError({ ...error, [kat]: true });
} else {
setBtnDisable(false);
setError({ ...error, [kat]: false });
}
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Daftar Role
</Title>
{
permissions.includes('setting.user_role.tambah') && (
<Tooltip label="Tambah Role">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
if (aksi === "edit") {
setDataEdit({ ...dataEdit, [kat]: value });
} else {
setDataTambah({ ...dataTambah, [kat]: value });
}
}
function buildOrderList(menus: MenuNode[]): string[] {
const list: string[] = [];
const traverse = (nodes: MenuNode[]) => {
nodes.forEach((node) => {
list.push(node.key);
if (node.children) traverse(node.children);
});
};
traverse(menus);
return list;
}
function sortByJsonOrder(arrayData: string[]): string[] {
const orderList = buildOrderList(listMenu.menus);
return arrayData.sort((a, b) => {
return orderList.indexOf(a) - orderList.indexOf(b);
});
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper label="Nama Role">
<Input
value={dataEdit.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "edit",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({
...dataEdit,
permissions: sortByJsonOrder(permissions) as never[],
});
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button
variant="filled"
onClick={handleEdit}
disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper
label="Nama Role"
description=""
error={error.name ? "Field is required" : ""}
>
<Input
value={dataTambah.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({
...dataTambah,
permissions: sortByJsonOrder(permissions) as never[],
});
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button
variant="filled"
onClick={handleCreate}
disabled={
btnDisable ||
dataTambah.name.length < 1 ||
dataTambah.permissions.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Delete */}
<Modal
opened={openedDelete}
onClose={closeDelete}
title={"Delete"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Text size="md" color="gray.6">
Apakah anda yakin ingin menghapus role ini?
</Text>
<Group justify="center" grow>
<Button variant="light" onClick={closeDelete}>
Batal
</Button>
<Button
variant="filled"
color="red"
onClick={handleDelete}
loading={btnLoading}
>
Hapus
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Daftar Role
</Title>
{permissions.includes("setting.user_role.tambah") && (
<Tooltip label="Tambah Role">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Role</Table.Th>
<Table.Th>Permission</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list.length > 0 ? (
list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td w={"150"}>{v.name}</Table.Td>
<Table.Td>
<PermissionRole permissions={v.permissions} />
</Table.Td>
<Table.Td w={"100"}>
<Group>
<Tooltip
label={
permissions.includes("setting.user_role.edit")
? "Edit Role"
: "Edit Role - Anda tidak memiliki akses"
}
>
Tambah
</Button>
</Tooltip>
)
}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Role</Table.Th>
<Table.Th>Permission</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list.length > 0 ? (
list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td w={"150"}>{v.name}</Table.Td>
<Table.Td>
<PermissionRole permissions={v.permissions} />
</Table.Td>
<Table.Td w={"100"}>
<Group>
<Tooltip label={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}>
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes('setting.user_role.edit') || v.id == "developer"}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label={permissions.includes('setting.user_role.delete') ? "Delete Role" : "Delete Role - Anda tidak memiliki akses"}>
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes('setting.user_role.delete') || v.id == "developer"}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={5} align="center">
Data Role Tidak Ditemukan
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={
!permissions.includes("setting.user_role.edit") ||
v.id == "developer"
}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip
label={
permissions.includes("setting.user_role.delete")
? "Delete Role"
: "Delete Role - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
setDataDelete(v.id);
openDelete();
}}
disabled={
!permissions.includes(
"setting.user_role.delete",
) || v.id == "developer"
}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Group>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={5} align="center">
Data Role Tidak Ditemukan
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
}

View File

@@ -21,7 +21,11 @@ import { useState } from "react";
import useSWR from "swr";
import notification from "./notificationGlobal";
export default function UserSetting({ permissions }: { permissions: JsonValue[] }) {
export default function UserSetting({
permissions,
}: {
permissions: JsonValue[];
}) {
const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false);
@@ -437,20 +441,17 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
<Title order={4} c="gray.2">
Daftar User
</Title>
{
permissions.includes('setting.user.tambah') && (
<Tooltip label="Tambah User">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)
}
{permissions.includes("setting.user.tambah") && (
<Tooltip label="Tambah User">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
@@ -474,18 +475,33 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
<Table.Td>{v.nameRole}</Table.Td>
<Table.Td>
<Group>
<Tooltip label={permissions.includes('setting.user.edit') ? "Edit User" : "Edit User - Anda tidak memiliki akses"}>
<Tooltip
label={
permissions.includes("setting.user.edit")
? "Edit User"
: "Edit User - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes('setting.user.edit') || v.roleId == "developer"}
disabled={
!permissions.includes("setting.user.edit") ||
v.roleId == "developer"
}
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
<Tooltip label={permissions.includes('setting.user.delete') ? "Delete User" : "Delete User - Anda tidak memiliki akses"}>
<Tooltip
label={
permissions.includes("setting.user.delete")
? "Delete User"
: "Delete User - Anda tidak memiliki akses"
}
>
<ActionIcon
variant="light"
size="sm"
@@ -495,7 +511,10 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
setDataDelete(v.id);
openDelete();
}}
disabled={!permissions.includes('setting.user.delete') || v.roleId == "developer"}
disabled={
!permissions.includes("setting.user.delete") ||
v.roleId == "developer"
}
>
<IconTrash size={20} />
</ActionIcon>

View File

@@ -3,157 +3,242 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKBedaBiodataDiri({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>();
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>();
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
return (
<div style={{ lineHeight: "1.25" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "15px" }}>
<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" }}>
<b><u>SURAT KETERANGAN BEDA BIODATA DIRI</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 + " " + 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>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "15px" }}>
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang bersangkutan:
<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>Tempat/Tanggal 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>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>, meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa dokumen, sebagai berikut:
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>1. Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td><td style={{ width: "10px" }}>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>3. Nama Orang Tua</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama orang tua")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena <b>kesalahan penulisan/pencatatan administratif</b>, namun yang bersangkutan adalah <b>orang yang sama</b>.
<br />
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "15px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "0px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
return (
<div style={{ lineHeight: "1.25" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "15px" }}>
<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" }}>
<b>
<u>SURAT KETERANGAN BEDA BIODATA DIRI</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 + " " + 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>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "15px" }}>
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang
bersangkutan:
<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>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>,
meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa
dokumen, sebagai berikut:
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>1. {getValue("data_dokumen")}</td>
<td style={{ width: "10px" }}></td>
<td>{/* {getValue("nama")} */}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("dokumen_a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("dokumen_b")}</td>
</tr>
</tbody>
</table>
</div>
{/* <div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tempat_lahir")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("dokumen_a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>3. Nama Orang Tua</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama orang tua")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
</tr>
</tbody>
</table>
</div> */}
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena{" "}
<b>kesalahan penulisan/pencatatan administratif</b>, namun yang
bersangkutan adalah <b>orang yang sama</b>.
<br />
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk
dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "15px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "0px",
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,108 +3,166 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKBelumKawin({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>();
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>();
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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 BELUM KAWIN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan} {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
<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>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Tempat/Tanggal 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>
</div>
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan dari yang bersangkutan dan data administrasi kependudukan yang ada di Desa {data.setting.desaNama},
yang bersangkutan benar sampai saat ini belum pernah menikah, baik secara adat, agama, maupun hukum negara.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan 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: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br /><br /><br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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 BELUM KAWIN</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan}{" "}
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
<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>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("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>
</div>
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan dari yang bersangkutan dan data administrasi
kependudukan yang ada di Desa {data.setting.desaNama}, yang bersangkutan
benar sampai saat ini belum pernah menikah, baik secara adat, agama,
maupun hukum negara.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan 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: "space-between",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
<br />
Pemohon
<br />
<br />
<br />
<br />
<br />
<br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,121 +3,163 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKDomisiliOrganisasi({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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 DOMISILI ORGANISASI</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>Alamat Kantor</td>
<td>:</td>
<td>{data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama Organisasi</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Jenis Organisasi</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Nomor Telepon</td><td>:</td><td>{getValue("negara")}</td></tr>
<tr><td>Nama Pimpinan</td><td>:</td><td>{getValue("agama")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}.
Dan sampai saat ini masih aktif melakukan kegiatan sesuai dengan bidangnya.<br />
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan 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 />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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 DOMISILI ORGANISASI</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>Alamat Kantor</td>
<td>:</td>
<td>{data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama Organisasi</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama_organisasi")}</td>
</tr>
<tr>
<td>Jenis Organisasi</td>
<td>:</td>
<td>{getValue("jenis_organisasi")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat_organisasi")}</td>
</tr>
<tr>
<td>Nomor Telepon</td>
<td>:</td>
<td>{getValue("no_telepon")}</td>
</tr>
<tr>
<td>Nama Pimpinan</td>
<td>:</td>
<td>{getValue("nama_pimpinan")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan{" "}
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
Kabupaten {data.setting.desaKabupaten}. Dan sampai saat ini masih aktif
melakukan kegiatan sesuai dengan bidangnya.
<br />
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan 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 />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,142 +3,244 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKKelahiran({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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")}</td>
</tr>
<tr>
<td>Tempat Kelahiran</td>
<td>:</td>
<td>{getValue("tempat_lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis_kelamin")}</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_lahir_ibu")}, ${getValue("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_lahir_ayah")}, ${getValue("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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,143 +3,153 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKKelakuanBaik({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u><br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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_lahir")}, ${getValue("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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u>
<br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,118 +3,201 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKKematian({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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 KEMATIAN</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>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Hubungan dengan almarhum/almarhumah</td><td>:</td><td>{getValue("hubungan dengan almarhum")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Melaporkan 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>NIK</td><td>:</td><td>{getValue("nik")}</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>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Telah meninggal dunia pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Tanggal Kematian</td><td style={{ width: "10px" }}>:</td><td>{getValue("tanggal kematian")}</td></tr>
<tr><td>Waktu Kematian</td><td>:</td><td>{getValue("waktu kematian")}</td></tr>
<tr><td>Tempat Kematian</td><td>:</td><td>{getValue("tempat kematian")}</td></tr>
<tr><td>Penyebab Kematian</td><td>:</td><td>{getValue("penyebab kematian")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br /> <br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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 KEMATIAN</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>{getValue("nama_pelapor")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik_pelapor")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan_pelapor")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat_pelapor")}</td>
</tr>
<tr>
<td>Hubungan dengan almarhum/almarhumah</td>
<td>:</td>
<td>{getValue("hubungan_pelapor")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Melaporkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama_almarhum")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik_almarhum")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis_kelamin_almarhum")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{`${getValue("tempat_lahir_almarhum")}, ${getValue("tanggal_lahir_almarhum")}`}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama_almarhum")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat_almarhum")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Telah meninggal dunia pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Tanggal Kematian</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tanggal_kematian")}</td>
</tr>
<tr>
<td>Waktu Kematian</td>
<td>:</td>
<td>{getValue("waktu_kematian")}</td>
</tr>
<tr>
<td>Tempat Kematian</td>
<td>:</td>
<td>{getValue("tempat_kematian")}</td>
</tr>
<tr>
<td>Penyebab Kematian</td>
<td>:</td>
<td>{getValue("penyebab_kematian")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan sebagaimana mestinya.
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "40px",
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
<br />
Pemohon
<br />
<br />
<br />
<br /> <br />
<u>{getValue("nama_pelapor")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,141 +3,177 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKPenghasilan({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
useEffect(() => {
loadImage();
}, [data]);
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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
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_lahir")}, ${getValue("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")} per bulan</td>
</tr>
</tbody>
</table>
</div>
{/* KEPERLUAN */}
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan")}</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 />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -3,119 +3,139 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKTempatUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
useEffect(() => {
loadImage();
}, [data]);
return (
<div style={{ lineHeight: "1.5" }}>
{/* TITLE */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b><br />
Nomor: {data.surat.noSurat}
</div>
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya:
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
<Row label="Alamat" value={data.setting.desaAlamat} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama")} />
<Row label="Tempat/Tanggal Lahir" value={getValue("tempat tanggal lahir")} />
<Row label="Alamat Pemilik Usaha" value={getValue("alamat")} />
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
<br />
<div>Benar yang bersangkutan memiliki tempat usaha dengan keterangan seperti berikut:</div>
<div>
<Row label="Nama Usaha" value={getValue("nama usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status tempat usaha")} />
<Row label="Luas Tempat Usaha" value={getValue("luas tempat usaha")} />
<Row label="Jumlah Karyawan" value={getValue("jumlah karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan <b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
</p>
<div>
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
<Row label="Pada tanggal" value={data.surat.createdAt} />
</div>
<br /><br />
{/* TANDA TANGAN */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
return (
<div style={{ lineHeight: "1.5" }}>
{/* TITLE */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b>
<br />
Nomor: {data.surat.noSurat}
</div>
);
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya:
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
<Row label="Alamat" value={data.setting.desaAlamat} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama_pemilik")} />
<Row
label="Tempat/Tanggal Lahir"
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
/>
<Row
label="Alamat Pemilik Usaha"
value={getValue("alamat_pemilik")}
/>
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
<br />
<div>
Benar yang bersangkutan memiliki tempat usaha dengan keterangan
seperti berikut:
</div>
<div>
<Row label="Nama Usaha" value={getValue("nama_usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang_usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat_usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status_tempat")} />
<Row label="Luas Tempat Usaha" value={getValue("luas_usaha") + " m2"} />
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan{" "}
<b>{getValue("tujuan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat
dipergunakan sebagaimana mestinya.
</p>
<div>
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
<Row label="Pada tanggal" value={data.surat.createdAt} />
</div>
<br />
<br />
{/* TANDA TANGAN */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u>
<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>
);
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

@@ -3,110 +3,118 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKTidakMampu({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
useEffect(() => {
loadImage();
}, [data]);
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>
<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>
<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.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
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>
<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>
<Row label="NIK" value={getValue("nik")} />
<Row label="Nama" value={getValue("nama")} />
<Row
label="Tempat Tgl Lahir"
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
/>
<Row label="Alamat" value={getValue("alamat")} />
</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")}.</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.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u>
<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>
);
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

@@ -3,147 +3,217 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
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: "15px 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: "10px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useEffect(() => {
loadImage();
}, [data]);
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: "15px 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_lahir")}, ${getValue("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: "10px",
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -4,191 +4,211 @@ import { useState } from "react";
import notification from "../notificationGlobal";
export default function SKYatim({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) =>
_.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi =
"/api/pengaduan/image?folder=lainnya&fileName=" +
data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useShallowEffect(() => {
loadImage();
}, [data]);
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 />
{/* TTD */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
useShallowEffect(() => {
loadImage();
}, [data]);
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>NIK</td>
<td>: {getValue("nik")}</td>
</tr>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {getValue("nama")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>
: {`${getValue("tempat_lahir")}, ${getValue("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>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 />
{/* TTD */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -15,6 +15,7 @@ import LayananRoute from "./server/routes/layanan_route";
import { MCPRoute } from "./server/routes/mcp_route";
import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route";
import SendWaRoute from "./server/routes/send_wa_route";
import SuratRoute from "./server/routes/surat_route";
import TestPengaduanRoute from "./server/routes/test_pengaduan";
import UserRoute from "./server/routes/user_route";
@@ -45,7 +46,8 @@ const Api = new Elysia({
.use(CredentialRoute)
.use(UserRoute)
.use(LayananRoute)
.use(AduanRoute);
.use(AduanRoute)
.use(SendWaRoute);
const app = new Elysia()
.use(Api)

View File

@@ -1,109 +1,384 @@
import { enumAgama, enumJenisKelamin, enumStatusHidup, enumStatusPerkawinan, enumStatusTempatUsaha } from "./valueEnum";
export const categoryPelayananSurat = [
{
id: "skbedabiodata",
name: "Surat Keterangan Beda Biodata Diri",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "dokumen yang beda", desc: "Fotokopi dokumen bersangkutan yang terdapat perbedaan biodata diri, misalnya: Sertifikat Tanah, Ijazah, Polis Asuransi, dan lainnya." }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing",
required: true, satuan: null
},
{
key: "ktp_kk",
name: "KTP / KK",
desc: "Fotokopi KTP atau Kartu Keluarga",
required: true, satuan: null
},
{
key: "dokumen_beda",
name: "Dokumen Pendukung",
desc: "Fotokopi dokumen yang terdapat perbedaan biodata (ijazah, sertifikat, dll)",
required: true, satuan: null
}
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "dokumen", "tertulis pada dokumen a", "tertulis pada dokumen b"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
{
key: "dokumen",
name: "Nama Dokumen",
desc: "Jenis dokumen yang mengalami perbedaan biodata",
type: "text",
required: true, satuan: null
},
{
key: "data_dokumen",
name: "Data Dokumen",
desc: "Data dokumen yg berbeda",
type: "text",
required: true, satuan: null
},
{ key: "dokumen_a", name: "Data pada Dokumen A", desc: "Data biodata pada dokumen pertama", type: "text", required: true, satuan: null },
{ key: "dokumen_b", name: "Data pada Dokumen B", desc: "Data biodata pada dokumen kedua", type: "text", required: true, satuan: null }
]
},
{
id: "skbelumkawin",
name: "Surat Keterangan Belum Kawin",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau Kartu Keluarga", required: true, satuan: null },
{ key: "akta_cerai", name: "Akta Cerai", desc: "Fotokopi akta cerai (jika berstatus janda/duda)", required: false, satuan: null }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "agama", "pekerjaan"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{
key: "agama",
name: "Agama",
desc: "Agama pemohon",
type: "enum",
options: enumAgama,
required: true, satuan: null
},
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null }
]
},
{
id: "skdomisiliorganisasi",
name: "Surat Keterangan Domisili Organisasi",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
{ name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "skt_organisasi", name: "SKT Organisasi", desc: "Fotokopi SKT Organisasi", required: true, satuan: null },
{ key: "susunan_pengurus", name: "Susunan Pengurus", desc: "Susunan pengurus organisasi", required: true, satuan: null }
],
dataText: ["nama organisasi", "jenis organisasi", "alamat organisasi/sekretariat", "no telepon", "nama pimpinan", "keperluan"]
dataText: [],
dataPelengkap: [
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text", required: true, satuan: null },
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis organisasi", type: "text", required: true, satuan: null },
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat", type: "text", required: true, satuan: null },
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text", required: true, satuan: null },
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan", type: "text", required: true, satuan: null },
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text", required: true, satuan: null }
]
},
{
id: "skkelahiran",
name: "Surat Keterangan Kelahiran",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "surat lahir", desc: "Fotokopi Surat Keterangan Lahir dari Bidan/Dokter (jika ada)" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "surat_lahir", name: "Surat Keterangan Lahir", desc: "Surat keterangan lahir dari bidan/dokter (jika ada)", required: false, satuan: null }
],
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"]
dataText: [],
dataPelengkap: [
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date", required: true, satuan: null },
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran", type: "text", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin Anak",
desc: "Jenis kelamin anak",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran", type: "number", required: true, satuan: null },
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number", required: true, satuan: null },
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text", required: true, satuan: null },
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu", type: "date", required: true, satuan: null },
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu", type: "text", required: true, satuan: null },
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu", type: "text", required: true, satuan: null },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text", required: true, satuan: null },
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number", required: true, satuan: null },
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah", type: "date", required: true, satuan: null },
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah", type: "text", required: true, satuan: null },
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah", type: "text", required: true, satuan: null },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan anak", type: "text", required: true, satuan: null },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null }
]
},
{
id: "skkelakuanbaik",
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "agama", "alamat", "pekerjaan", "polsek"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{
key: "agama",
name: "Agama",
desc: "Agama",
type: "enum",
options: enumAgama,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan", type: "text", required: true, satuan: null }
]
},
{
id: "skkematian",
name: "Surat Keterangan Kematian",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null },
{ key: "surat_kematian", name: "Surat Keterangan Kematian", desc: "Surat keterangan kematian dari rumah sakit/dokter (jika ada)", required: false, satuan: null }
],
dataText: ["nik pelapor", "nama pelapor", "pekerjaan pelapor", "alamat pelapor", "hubungan pelapor dengan almarhum", "nama almarhum", "nik almarhum", "tempat tanggal lahir almarhum", "alamat almarhum", "agama almarhum", "tanggal kematian", "waktu kematian", "tempat kematian", "penyebab kematian"]
dataText: [],
dataPelengkap: [
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "NIK pelapor", type: "number", required: true, satuan: null },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text", required: true, satuan: null },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null },
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan almarhum", type: "text", required: true, satuan: null },
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama almarhum", type: "text", required: true, satuan: null },
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "NIK almarhum", type: "number", required: true, satuan: null },
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir", desc: "Tempat lahir almarhum", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir", desc: "Tanggal lahir almarhum", type: "date", required: true, satuan: null },
{ key: "alamat_almarhum", name: "Alamat", desc: "Alamat terakhir", type: "text", required: true, satuan: null },
{
key: "agama_almarhum",
name: "Agama Almarhum",
desc: "Agama almarhum",
type: "enum",
options: enumAgama,
required: true, satuan: null
},
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date", required: true, satuan: null },
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text", required: true, satuan: null },
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text", required: true, satuan: null },
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text", required: true, satuan: null }
]
},
{
id: "skpenghasilan",
name: "Surat Keterangan Penghasilan",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_ortu_kk", name: "KTP Orang Tua / KK", desc: "Fotokopi KTP orang tua/KK", required: true, satuan: null },
{ key: "surat_pernyataan", name: "Surat Pernyataan Penghasilan", desc: "Surat pernyataan penghasilan bermaterai", required: true, satuan: null }
],
dataText: ["nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "penghasilan", "alasan permohonan"]
dataText: [],
dataPelengkap: [
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number", required: true, satuan: "/Bulan" },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text", required: true, satuan: null }
]
},
{
id: "sktempatusaha",
name: "Surat Keterangan Tempat Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null },
{ key: "dokumen_lahan", name: "Dokumen Lahan", desc: "SPPT/Sertifikat/surat sewa tempat usaha", required: true, satuan: null }
],
dataText: ["nik", "nama pemilik", "tempat tanggal lahir", "alamat pemilik", "nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "NIK pemilik", type: "number", required: true, satuan: null },
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik", type: "text", required: true, satuan: null },
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text", required: true, satuan: null },
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang usaha", type: "text", required: true, satuan: null },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null },
{
key: "status_tempat",
name: "Status Tempat Usaha",
desc: "Status kepemilikan tempat usaha",
type: "enum",
options: enumStatusTempatUsaha,
required: true, satuan: null
},
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number", required: true, satuan: "m²" },
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah karyawan", type: "number", required: true, satuan: null },
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text", required: true, satuan: null }
]
},
{
id: "sktidakmampu",
name: "Surat Keterangan Tidak Mampu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "alamat", "alasan permohonan"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "NIK pemohon", type: "number", required: true, satuan: null },
{ key: "nama Lengkap", name: "Nama", desc: "Nama pemohon", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "alamat", name: "Alamat", desc: "Alamat pemohon", type: "text", required: true, satuan: null },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan permohonan", type: "text", required: true, satuan: null }
]
},
{
id: "skusaha",
name: "Surat Keterangan Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null }
],
dataText: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"]
dataText: [],
dataPelengkap: [
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemilik usaha", type: "text", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemilik usaha",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan", type: "text", required: true, satuan: null },
{
key: "agama",
name: "Agama",
desc: "Agama",
type: "enum",
options: enumAgama,
required: true, satuan: null
},
{
key: "status_perkawinan",
name: "Status Perkawinan",
desc: "Status perkawinan",
type: "enum",
options: enumStatusPerkawinan,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha", type: "text", required: true, satuan: null },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null }
]
},
{
id: "skyatimpiatu",
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "nama ayah", "status ayah", "nama ibu", "status ibu"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "NIK anak", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama", desc: "Nama anak", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin anak",
type: "enum",
options: enumJenisKelamin,
required: true, satuan: null
},
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text", required: false, satuan: null },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah", type: "text", required: true, satuan: null },
{
key: "status_ayah",
name: "Status Ayah",
desc: "Status ayah",
type: "enum",
options: enumStatusHidup,
required: true, satuan: null
},
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu", type: "text", required: true, satuan: null },
{
key: "status_ibu",
name: "Status Ibu",
desc: "Status ibu",
type: "enum",
options: enumStatusHidup,
required: true, satuan: null
}
]
}
];

View File

@@ -246,16 +246,6 @@
"label": "View Detail",
"default": true
},
{
"key": "setting.kategori_pelayanan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pelayanan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pelayanan.delete",
"label": "Delete",

31
src/lib/valueEnum.ts Normal file
View File

@@ -0,0 +1,31 @@
export const enumJenisKelamin = [
{ label: "Laki-laki", value: "Laki-laki" },
{ label: "Perempuan", value: "Perempuan" }
];
export const enumAgama = [
{ label: "Islam", value: "Islam" },
{ label: "Kristen", value: "Kristen" },
{ label: "Katolik", value: "Katolik" },
{ label: "Hindu", value: "Hindu" },
{ label: "Buddha", value: "Buddha" },
{ label: "Konghucu", value: "Konghucu" }
];
export const enumStatusHidup = [
{ label: "Hidup", value: "Hidup" },
{ label: "Meninggal", value: "Meninggal" }
];
export const enumStatusPerkawinan = [
{ label: "Belum Kawin", value: "Belum Kawin" },
{ label: "Kawin", value: "Kawin" },
{ label: "Cerai Hidup", value: "Cerai Hidup" },
{ label: "Cerai Mati", value: "Cerai Mati" }
];
export const enumStatusTempatUsaha = [
{ label: "Milik Sendiri", value: "Milik Sendiri" },
{ label: "Sewa", value: "Sewa" },
{ label: "Pinjam", value: "Pinjam" }
];

View File

@@ -1,12 +1,12 @@
import clientRoutes from "@/clientRoutes";
import {
Button,
Container,
Group,
Center,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
TextInput
} from "@mantine/core";
import { useState } from "react";
import apiFetch from "../lib/apiFetch";
@@ -28,16 +28,19 @@ export default function Login() {
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
break;
case "credential":
window.location.href = clientRoutes["/scr/dashboard/credential/credential"];
window.location.href =
clientRoutes["/scr/dashboard/credential/credential"];
break;
case "setting":
window.location.href = clientRoutes["/scr/dashboard/setting/detail-setting"];
window.location.href =
clientRoutes["/scr/dashboard/setting/detail-setting"];
break;
case "api_key":
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
break;
case "pelayanan":
window.location.href = clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
window.location.href =
clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
break;
default:
window.location.href = clientRoutes["/scr/dashboard"];
@@ -70,25 +73,73 @@ export default function Login() {
};
return (
<Container>
<Stack>
<Text>Login</Text>
<TextInput
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Group justify="right">
<Button onClick={handleSubmit} disabled={loading}>
<Center
h="100vh"
style={{
background:
"radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
}}
>
<Paper
radius="lg"
p="xl"
w={420}
style={{
background: "rgba(20, 20, 20, 0.75)",
backdropFilter: "blur(12px)",
border: "1px solid rgba(255, 255, 255, 0.08)",
boxShadow: "0 20px 60px rgba(0,0,0,0.6)",
}}
>
<Stack>
<Text
size="xl"
fw={700}
ta="center"
c="white"
>
Welcome Back
</Text>
<Text
size="sm"
ta="center"
c="dimmed"
>
Sign in to continue to your dashboard
</Text>
<TextInput
label="Email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
fullWidth
mt="md"
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
style={{
transition: "all 0.2s ease",
}}
onClick={handleSubmit}
disabled={loading}
>
Login
</Button>
</Group>
</Stack>
</Container>
</Stack>
</Paper>
</Center>
);
}

View File

@@ -0,0 +1,707 @@
import FullScreenLoading from "@/components/FullScreenLoading";
import notification from "@/components/notificationGlobal";
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
import apiFetch from "@/lib/apiFetch";
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
import {
ActionIcon,
Badge,
Box,
Button,
Card,
Container,
Divider,
FileInput,
Flex,
Grid,
Group,
Modal,
Select,
Stack,
Text,
TextInput,
Tooltip,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconBuildingCommunity,
IconCategory,
IconFiles,
IconInfoCircle,
IconNotes,
IconPhone,
IconUpload
} from "@tabler/icons-react";
import dayjs from "dayjs";
import "dayjs/locale/id";
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSWR from "swr";
type DataItem = {
key: string;
value: string;
required: boolean;
};
type FormSurat = {
kategoriId: string;
nama: string;
phone: string;
dataPelengkap: DataItem[];
syaratDokumen: DataItem[];
};
type ErrorState = Record<string, string | null>;
export default function FormSurat() {
const [errors, setErrors] = useState<ErrorState>({});
const [opened, { open, close }] = useDisclosure(false);
const [noPengajuan, setNoPengajuan] = useState("");
const [submitLoading, setSubmitLoading] = useState(false);
const navigate = useNavigate();
const { search } = useLocation();
const query = new URLSearchParams(search);
const jenisSurat = query.get("jenis");
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
apiFetch.api.pelayanan.category.get(),
);
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
const [dataSurat, setDataSurat] = useState<any>({});
const [formSurat, setFormSurat] = useState<FormSurat>({
nama: "",
phone: "",
kategoriId: "",
dataPelengkap: [],
syaratDokumen: [],
});
const listCategory = data?.data || [];
function onResetAll() {
setNoPengajuan("");
setSubmitLoading(false);
setFormSurat({
nama: "",
phone: "",
kategoriId: "",
dataPelengkap: [],
syaratDokumen: [],
});
}
function onGetJenisSurat() {
try {
if (!jenisSurat || jenisSurat == "null") {
setJenisSuratFix({ name: "", id: "" });
} else {
const namaJenis = fromSlug(jenisSurat);
const data = listCategory.find((item: any) => item.name.toUpperCase() == namaJenis.toUpperCase());
if (!data) return;
setJenisSuratFix(data);
}
} catch (error) {
console.error(error);
}
}
async function getDetailJenisSurat() {
try {
const get: any = await apiFetch.api.pelayanan.category.detail.get({
query: {
id: jenisSuratFix.id,
},
});
setDataSurat(get.data);
setFormSurat({
kategoriId: jenisSuratFix.id,
nama: "",
phone: "",
dataPelengkap: (get.data?.dataPelengkap || []).map(
(item: { key: string, required: boolean }) => ({
key: item.key,
value: "",
required: item.required
}),
),
syaratDokumen: (get.data?.syaratDokumen || []).map(
(item: { key: string, required: boolean }) => ({
key: item.key,
value: "",
required: item.required
}),
),
});
} catch (error) {
console.error(error);
}
}
useShallowEffect(() => {
mutate();
}, []);
useShallowEffect(() => {
if (listCategory.length > 0) {
onGetJenisSurat();
}
}, [jenisSurat, listCategory]);
useShallowEffect(() => {
if (jenisSuratFix.id != "") {
getDetailJenisSurat();
}
}, [jenisSuratFix.id]);
function onChecking() {
const hasError = Object.values(errors).some((v) => v);
if (hasError) {
return notification({
title: "Gagal",
message: "Masih ada form yang belum valid",
type: "error",
});
}
const isFormKosong = Object.values(formSurat).some((value) => {
if (Array.isArray(value)) {
return value.some(
(item) =>
(typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
);
}
return typeof value === "string" && value.trim() === "";
});
if (isFormKosong) {
return notification({
title: "Gagal",
message: "Silahkan lengkapi form surat",
type: "error",
});
}
open();
}
async function onSubmit() {
try {
setSubmitLoading(true);
// 🔥 CLONE state SEKALI
let finalFormSurat = structuredClone(formSurat);
// 2⃣ Upload satu per satu
for (const itemUpload of finalFormSurat.syaratDokumen) {
const updImg = await apiFetch.api.pengaduan.upload.post({
file: itemUpload.value,
folder: "syarat-dokumen",
});
if (updImg.status === 200) {
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
finalFormSurat.syaratDokumen = updateArrayByKey(
finalFormSurat.syaratDokumen,
itemUpload.key,
updImg.data?.filename || "",
);
}
}
// 3⃣ SET STATE SEKALI (optional, untuk UI)
setFormSurat(finalFormSurat);
// 4⃣ SUBMIT KE API
const res = await apiFetch.api.pelayanan.create.post(finalFormSurat);
if (res.status === 200) {
setNoPengajuan(res.data?.noPengajuan || "");
} else {
notification({
title: "Gagal",
message:
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
type: "error",
});
}
} catch (error) {
notification({
title: "Gagal",
message: "Server Error",
type: "error",
});
} finally {
setSubmitLoading(false);
}
}
function updateArrayByKey(
list: DataItem[],
targetKey: string,
value: string,
): DataItem[] {
return list.map((item) =>
item.key === targetKey ? { ...item, value } : item,
);
}
function validateField(key: string, value: any) {
const stringValue = String(value ?? "").trim();
// wajib diisi
if (!stringValue) {
return "Field wajib diisi";
}
// 🔥 semua key yang mengandung "nik"
if (key.toLowerCase().includes("nik")) {
if (!/^\d+$/.test(stringValue)) {
return "NIK harus berupa angka";
}
if (stringValue.length !== 16) {
return "NIK harus 16 digit";
}
}
return null;
}
function validationForm({
key,
value,
}: {
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
value: any;
}) {
if (key === "dataPelengkap" || key === "syaratDokumen") {
if (value.required == true) {
const errorMsg = validateField(value.key, value.value);
setErrors((prev) => ({
...prev,
[value.key]: errorMsg,
}));
}
setFormSurat((prev) => ({
...prev,
[key]: updateArrayByKey(prev[key], value.key, value.value),
}));
} else {
const keyFix = key == "nama" ? "nama_kontak" : key;
const errorMsg = validateField(keyFix, value);
setErrors((prev) => ({
...prev,
[keyFix]: errorMsg,
}));
setFormSurat({
...formSurat,
[key]: value,
});
}
}
return (
<Container size="md" w={"100%"} pb={"lg"}>
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
<Text>Apakah anda yakin ingin mengirim pengajuan surat ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Tidak
</Button>
<Button
variant="filled"
color="green"
onClick={() => {
onSubmit();
close();
}}
>
Ya
</Button>
</Group>
</Stack>
</Modal>
<FullScreenLoading visible={submitLoading} />
{noPengajuan != "" ? (
<SuccessPengajuan
noPengajuan={noPengajuan}
onClose={() => {
onResetAll();
navigate("/darmasaba/surat");
}}
category="create"
/>
) : (
<Box>
<Stack gap="lg">
<Group justify="space-between" align="center">
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Layanan Pengajuan Surat Administrasi
</Text>
<Text size="sm" c="dimmed">
Formulir resmi untuk mengajukan berbagai jenis surat
administrasi desa/kelurahan secara online.
</Text>
</div>
</Group>
<Group>
<Badge radius="sm">Form Length: 4 Sections</Badge>
</Group>
</Group>
<Stack gap="lg">
<FormSection
title="Jenis Surat Pengajuan"
icon={<IconCategory size={16} />}
>
<Grid>
<Grid.Col span={12}>
<Select
allowDeselect={false}
label={
<FieldLabel
label="Jenis Surat"
hint="Jenis surat yang ingin diajukan"
/>
}
placeholder="Pilih jenis surat"
data={listCategory.map((item: any) => ({
value: item.name,
label: item.name,
}))}
value={jenisSuratFix.name == "" ? null : jenisSuratFix.name}
onChange={(value) => {
const slug = toSlug(String(value));
navigate("/darmasaba/surat?jenis=" + slug);
}}
/>
</Grid.Col>
</Grid>
</FormSection>
{/* Kontak Section */}
<FormSection
title="Kontak"
icon={<IconPhone size={16} />}
description="Informasi kontak yg dapat dihubungi"
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={<FieldLabel label="Nama" hint="Nama kontak" required />}
placeholder="Budi Setiawan"
value={formSurat.nama}
error={errors.nama_kontak}
onChange={(e) =>
validationForm({ key: "nama", value: e.target.value })
}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
required
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
value={formSurat.phone}
error={errors.phone}
type="number"
onChange={(e) =>
validationForm({ key: "phone", value: e.target.value })
}
/>
</Grid.Col>
</Grid>
</FormSection>
{jenisSuratFix.id != "" &&
dataSurat &&
dataSurat.dataPelengkap && (
<>
<FormSection
title="Data Yang Diperlukan"
description="Data yang diperlukan untuk mengajukan surat"
icon={<IconNotes size={16} />}
>
<Grid>
{dataSurat.dataPelengkap.map(
(item: any, index: number) => (
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
allowDeselect={false}
label={
<FieldLabel
label={item.name}
hint={item.desc}
required={item.required}
/>
}
data={item.options ?? []}
placeholder={item.name}
onChange={(e) => {
validationForm({
key: "dataPelengkap",
value: { key: item.key, value: e, required: item.required },
});
}}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value
}
/>
) : item.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={
<FieldLabel
label={item.name}
hint={item.desc}
required={item.required}
/>
}
placeholder={item.name}
onChange={(e) => {
const formatted = e
? dayjs(e)
.locale("id")
.format("DD MMMM YYYY")
: "";
validationForm({
key: "dataPelengkap",
value: {
key: item.key,
value: formatted,
required: item.required,
},
});
}}
/>
) : (
<TextInput
error={errors[item.key]}
type={item.type}
label={
<FieldLabel
label={item.name}
hint={item.desc}
required={item.required}
/>
}
placeholder={item.name}
onChange={(e) =>
validationForm({
key: "dataPelengkap",
value: {
key: item.key,
value: e.target.value,
required: item.required,
},
})
}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value
}
rightSection={
item.satuan != null &&
<Text mr={"lg"}>{item.satuan}</Text>
}
/>
)}
</Grid.Col>
),
)}
</Grid>
</FormSection>
<FormSection
title="Syarat Dokumen"
description="Syarat dokumen yang diperlukan"
icon={<IconFiles size={16} />}
>
<Grid>
{dataSurat.syaratDokumen.map(
(item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FileInputWrapper
label={item.desc}
placeholder={"Upload file "}
accept="image/*,application/pdf"
onChange={(file) =>
validationForm({
key: "syaratDokumen",
value: { key: item.key, value: file },
})
}
name={item.name}
required={item.required}
/>
</Grid.Col>
),
)}
</Grid>
</FormSection>
{/* Actions */}
<Group justify="right" mt="md">
{/* <Button variant="default" onClick={() => { }}>
Reset
</Button> */}
<Button onClick={onChecking}>Kirim</Button>
</Group>
</>
)}
</Stack>
</Stack>
</Box>
)}
</Container>
);
}
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
return (
<Group justify="apart" gap="xs" align="center">
<Group gap={4} align="center">
<Text fw={600}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
function FormSection({
title,
icon,
children,
description,
}: {
title: string;
icon?: React.ReactNode;
children: React.ReactNode;
description?: string;
}) {
return (
<Card radius="md" shadow="sm" withBorder>
<Group justify="apart" align="center" mb="xs">
<Group align="center" gap="xs">
{icon}
<Text fw={700}>{title}</Text>
</Group>
{description && <Badge variant="light">{description}</Badge>}
</Group>
<Divider mb="sm" />
<Stack gap="sm">{children}</Stack>
</Card>
);
}
function FileInputWrapper({
label,
placeholder,
accept,
onChange,
preview,
name,
description,
required = false,
}: {
label: string;
placeholder?: string;
accept?: string;
onChange: (file: File | null) => void;
preview?: string | null;
name: string;
description?: string;
required?: boolean;
}) {
return (
<Stack gap="xs">
<Flex direction={"column"}>
<Group justify="apart" align="center">
<Text fw={500}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group>
{description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
{description}
</Text>
)}
</Flex>
<FileInput
accept={accept}
placeholder={placeholder}
onChange={(f) => onChange(f)}
leftSection={<IconUpload />}
aria-label={label}
name={name}
clearable={true}
/>
{preview ? (
<div>
<Text size="xs" color="dimmed">
Preview:
</Text>
{/* If preview is an image it will show; pdf preview might not render as image */}
{/* Use <object> or <img> depending on file type — keep simple here */}
<div style={{ marginTop: 6 }}>
<img
src={preview}
alt={`${label} preview`}
style={{ maxWidth: "200px", borderRadius: 4 }}
/>
</div>
</div>
) : null}
</Stack>
);
}

View File

@@ -0,0 +1,837 @@
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile";
import { DataNotFound } from "@/components/NotFoundPengajuanSurat";
import notification from "@/components/notificationGlobal";
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Alert,
Anchor,
Badge,
Box,
Button,
Card,
Container,
Divider,
FileInput,
Flex,
Grid,
Group,
Modal,
Select,
Stack,
Text,
TextInput,
Tooltip,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconBuildingCommunity,
IconFiles,
IconInfoCircle,
IconNotes,
IconUpload,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import "dayjs/locale/id";
import _ from "lodash";
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
type DataItem = {
key: string;
value: string;
};
type UpdateDataItem = {
id: string;
key: string;
value: any;
required: boolean;
};
type FormSurat = {
kategoriId: string;
nama: string;
phone: string;
dataPelengkap: DataItem[];
syaratDokumen: DataItem[];
};
type FormUpdateSurat = {
dataPelengkap: UpdateDataItem[];
syaratDokumen: UpdateDataItem[];
};
type DataPengajuan = {
id: string;
noPengajuan: string;
category: string;
status: "antrian" | "diproses" | "selesai" | "ditolak";
createdAt: Date;
updatedAt: Date;
idSurat: string | undefined;
alasan: string | undefined | null;
};
export default function UpdateDataSurat() {
const navigate = useNavigate();
const { search } = useLocation();
const query = new URLSearchParams(search);
const noPengajuan = query.get("pengajuan");
const [found, setFound] = useState(true);
return (
<Container size="md" w={"100%"}>
<Box>
<Stack gap="lg">
{found && (
<Group justify="space-between" align="center" mt={"lg"}>
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Update Data Pengajuan Surat Administrasi
</Text>
<Text size="sm" c="dimmed">
Formulir ini digunakan untuk memperbarui data pengajuan
surat administrasi yang telah diajukan sebelumnya.
</Text>
</div>
</Group>
</Group>
)}
<Stack gap="lg" mb="lg">
{!noPengajuan ? (
<SearchData />
) : found ? (
<DataUpdate
noPengajuan={noPengajuan}
onValidate={(e) => {
setFound(e);
}}
/>
) : (
<DataNotFound
backTo={() => navigate("/darmasaba/update-data-surat")}
/>
)}
</Stack>
</Stack>
</Box>
</Container>
);
}
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
return (
<Group justify="apart" gap="xs" align="center">
<Group gap={4} align="center">
<Text fw={600}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
function FormSection({
title,
icon,
children,
description,
info,
}: {
title?: string;
icon?: React.ReactNode;
children: React.ReactNode;
description?: string;
info?: string;
}) {
return (
<Card radius="md" shadow="sm" withBorder>
<Box mb="xs">
<Group justify="apart" align="center">
<Group align="center" gap="xs">
{icon}
{title && <Text fw={700}>{title}</Text>}
</Group>
{description && <Badge variant="light">{description}</Badge>}
</Group>
{info && (
<Text size="sm" c="dimmed">
{info}
</Text>
)}
</Box>
{title && <Divider mb="sm" />}
<Stack gap="sm">{children}</Stack>
</Card>
);
}
function FileInputWrapper({
label,
placeholder,
accept,
onChange,
preview,
name,
description,
linkView,
disabled,
required = false,
}: {
label: string;
placeholder?: string;
accept?: string;
linkView?: string;
onChange: (file: File | null) => void;
preview?: string | null;
name: string;
description?: string;
disabled?: boolean;
required?: boolean;
}) {
const [viewImg, setViewImg] = useState("");
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
}
}, [viewImg]);
return (
<>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
onClose={() => {
setOpenedPreviewFile(false);
}}
folder="syarat-dokumen"
fileName={viewImg}
/>
<Stack gap="xs">
<Flex direction={"column"}>
<Group justify="apart" align="center">
<Text fw={500}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group>
{description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
{description}
</Text>
)}
{linkView && (
<Anchor onClick={() => setViewImg(linkView)} size="sm">
Lihat dokumen sebelumnya
</Anchor>
)}
</Flex>
<FileInput
accept={accept}
placeholder={placeholder}
onChange={(f) => onChange(f)}
leftSection={<IconUpload />}
aria-label={label}
name={name}
disabled={disabled}
clearable={true}
/>
{preview ? (
<div>
<Text size="xs" color="dimmed">
Preview:
</Text>
<div style={{ marginTop: 6 }}>
<img
src={preview}
alt={`${label} preview`}
style={{ maxWidth: "200px", borderRadius: 4 }}
/>
</div>
</div>
) : null}
</Stack>
</>
);
}
function SearchData() {
const [submitLoading, setSubmitLoading] = useState(false);
const [searchPengajuan, setSearchPengajuan] = useState("");
const [searchPengajuanPhone, setSearchPengajuanPhone] = useState("");
const navigate = useNavigate();
async function handleSearch() {
try {
setSubmitLoading(true);
if (searchPengajuan == "" || searchPengajuanPhone == "") {
notification({
title: "Peringatan",
message: "Silakan isi nomor pengajuan atau nomor telephone",
type: "warning",
});
return;
}
const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({
phone: searchPengajuanPhone,
noPengajuan: searchPengajuan,
});
if (response.status === 200) {
if (response.data?.success) {
navigate(
`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`,
);
} else {
notification({
title: "Peringatan",
message: response.data?.message || "Data pengajuan tidak valid",
type: "warning",
});
}
} else {
notification({
title: "Error",
message: "Pengajuan tidak ditemukan atau gagal memuat data",
type: "error",
});
}
} catch (error) {
console.error("Error searching:", error);
notification({
title: "Error",
message: "Gagal mencari data pengajuan",
type: "error",
});
} finally {
setSubmitLoading(false);
}
}
return (
<>
<FullScreenLoading visible={submitLoading} text="Mencari Data" />
<FormSection
title="Cari Pengajuan Surat"
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Pengajuan"
hint="Nomor pengajuan surat"
/>
}
placeholder="PS-2025-000123"
onChange={(e) => {
setSearchPengajuan(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
type="number"
onChange={(e) => {
setSearchPengajuanPhone(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={12}>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => {
handleSearch();
}}
loading={submitLoading}
>
Cari Pengajuan
</Button>
</Grid.Col>
</Grid>
</FormSection>
</>
);
}
function DataUpdate({
noPengajuan,
onValidate,
}: {
noPengajuan: string;
onValidate: (e: boolean) => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string | null>>({});
const [sukses, setSukses] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
const [status, setStatus] = useState("");
const [loadingFetchData, setLoadingFetchData] = useState(false);
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
dataPelengkap: [],
syaratDokumen: [],
});
async function fetchData() {
try {
setLoadingFetchData(true);
const res = await apiFetch.api.pelayanan["detail-data"].post({
nomerPengajuan: noPengajuan,
});
if (res.data && res.data.success === true) {
onValidate(true);
setDataPelengkap(res.data.dataPelengkap || []);
setDataSyaratDokumen(res.data.syaratDokumen || []);
setDataPengajuan(res.data.pengajuan || {});
setStatus(
res.data.pengajuan && "status" in res.data.pengajuan
? res.data.pengajuan.status
: "",
);
} else {
// notification({
// title: "Error",
// message: res.data?.message || "Gagal memuat data",
// type: "error",
// });
onValidate(false);
setDataPelengkap([]);
setDataSyaratDokumen([]);
setDataPengajuan({});
}
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setLoadingFetchData(false);
}
}
useShallowEffect(() => {
fetchData();
}, []);
function validateField(key: string, value: any) {
const stringValue = String(value ?? "").trim();
// wajib diisi
if (!stringValue) {
return "Field wajib diisi";
}
// 🔥 semua key yg mengandung "nik"
if (key.toLowerCase().includes("nik")) {
if (!/^\d+$/.test(stringValue)) {
return "NIK harus berupa angka";
}
if (stringValue.length !== 16) {
return "NIK harus 16 digit";
}
}
return null;
}
function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
const index = array.findIndex((v) => v.id === item.id);
// insert
if (index === -1) {
return [...array, item];
}
// ✏️ update
return array.map((v, i) => (i === index ? { ...v, ...item } : v));
}
function validationForm({
kategori,
value,
}: {
kategori: "dataPelengkap" | "syaratDokumen";
value: UpdateDataItem;
}) {
if (kategori == "syaratDokumen" && value.value == null) {
setFormSurat((prev) => ({
...prev,
syaratDokumen: prev.syaratDokumen.filter(
(item) => item.id !== value.id
),
}));
} else {
if (value.required == true) {
const errorMsg = validateField(value.key, value.value);
setErrors((prev) => ({
...prev,
[value.id]: errorMsg,
}));
}
setFormSurat((prev) => ({
...prev,
[kategori]: upsertById(prev[kategori], {
id: value.id,
key: value.key,
value: value.value,
required: value.required,
}),
}));
}
}
function updateArrayByKey(
list: UpdateDataItem[],
id: string,
value: any,
): UpdateDataItem[] {
return list.map((item) => (item.id === id ? { ...item, value } : item));
}
function onChecking() {
const hasError = Object.values(errors).some((v) => v);
if (hasError) {
return notification({
title: "Gagal",
message: "Masih ada data yang belum valid",
type: "error",
});
}
if (
formSurat.dataPelengkap.length == 0 &&
formSurat.syaratDokumen.length == 0
)
return notification({
title: "Peringatan",
message: "Tidak ada data yang diupdate",
type: "warning",
});
const isFormKosong = Object.values(formSurat).some(
(value: UpdateDataItem[] | string) => {
if (Array.isArray(value)) {
return value.some(
(item) =>
(typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
);
}
if (typeof value === "string") {
return value.trim() === "";
}
return false;
},
);
if (isFormKosong) {
return notification({
title: "Gagal",
message: "Silahkan lengkapi form surat",
type: "error",
});
} else {
open();
}
}
async function onSubmit() {
try {
setSubmitLoading(true);
// 🔥 CLONE state SEKALI
let finalFormSurat = structuredClone(formSurat);
// 2⃣ Upload satu per satu
for (const itemUpload of finalFormSurat.syaratDokumen) {
const updImg = await apiFetch.api.pengaduan.upload.post({
file: itemUpload.value,
folder: "syarat-dokumen",
});
if (updImg.status === 200) {
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
finalFormSurat.syaratDokumen = updateArrayByKey(
finalFormSurat.syaratDokumen,
itemUpload.id,
updImg.data?.filename || "",
);
}
}
// 3⃣ SET STATE SEKALI (optional, untuk UI)
setFormSurat(finalFormSurat);
// 4⃣ SUBMIT KE API
const res = await apiFetch.api.pelayanan.update.post({
id: dataPengajuan && "id" in dataPengajuan ? dataPengajuan.id : "",
dataPelengkap: finalFormSurat.dataPelengkap,
syaratDokumen: finalFormSurat.syaratDokumen,
});
if (res.status === 200) {
setSukses(true);
} else {
notification({
title: "Gagal",
message:
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
type: "error",
});
}
} catch (error) {
notification({
title: "Gagal",
message: "Server Error",
type: "error",
});
} finally {
setSubmitLoading(false);
}
}
return (
<>
<FullScreenLoading visible={submitLoading || loadingFetchData} />
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
<Text>Apakah anda yakin ingin mengupdate pengajuan surat ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Tidak
</Button>
<Button
variant="filled"
color="green"
onClick={() => {
onSubmit();
close();
}}
>
Ya
</Button>
</Group>
</Stack>
</Modal>
{sukses ? (
<SuccessPengajuan
noPengajuan={noPengajuan}
onClose={() => {
navigate("/darmasaba/update-data-surat");
}}
category="update"
/>
) : (
<>
{status != "ditolak" && status != "antrian" && (
<Alert
variant="light"
color="yellow"
radius="lg"
title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
{status == "ditolak" && (
<Alert
variant="light"
color="yellow"
radius="lg"
title={`Data pengajuan surat ini ditolak, karena ${dataPengajuan && 'alasan' in dataPengajuan && dataPengajuan.alasan
? dataPengajuan.alasan
: "alasan tidak tersedia"
}. Silahkan perbaiki data pengajuan surat ini.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
<FormSection
title="Data Yang Diperlukan"
description="Data yang diperlukan"
icon={<IconNotes size={16} />}
>
<Grid>
{dataPelengkap.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
disabled={status != "ditolak" && status != "antrian"}
allowDeselect={false}
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
data={item.options ?? []}
placeholder={item.name}
onChange={(e) => {
validationForm({
kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: e, required: item.required },
});
}}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value ||
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
/>
) : item.type == "date" ? (
<DateInput
disabled={status != "ditolak" && status != "antrian"}
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
placeholder={item.name}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
validationForm({
kategori: "dataPelengkap",
value: {
id: item.id,
key: item.key,
value: formatted,
required: item.required
},
});
}}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key === item.key,
)?.value
? parseTanggalID(
formSurat.dataPelengkap.find(
(n: any) => n.key === item.key,
)?.value,
)
: parseTanggalID(item.value)
}
/>
) : (
<TextInput
error={errors[item.id]}
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
placeholder={item.name}
type={item.type}
onChange={(e) =>
validationForm({
kategori: "dataPelengkap",
value: {
id: item.id,
key: item.key,
value: e.target.value,
required: item.required,
},
})
}
value={
formSurat.dataPelengkap.find((n) => n.id === item.id)
?.value ??
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
disabled={status != "ditolak" && status != "antrian"}
rightSection={
item.satuan != null &&
<Text mr={"lg"}>{item.satuan}</Text>
}
/>
)}
</Grid.Col>
))}
</Grid>
</FormSection>
<FormSection
title="Syarat Dokumen hjh"
description="Syarat dokumen yang diperlukan"
icon={<IconFiles size={16} />}
>
<Grid>
{dataSyaratDokumen.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FileInputWrapper
required={item.required}
label={item.desc}
placeholder={"Upload file terbaru untuk mengupdate"}
accept="image/*,application/pdf"
linkView={item.value}
onChange={(file) =>
validationForm({
kategori: "syaratDokumen",
value: { id: item.id, key: item.key, value: file, required: item.required },
})
}
name={item.name}
disabled={status != "ditolak" && status != "antrian"}
/>
</Grid.Col>
))}
</Grid>
</FormSection>
<Group justify="right" mt="md">
<Button
onClick={() => {
onChecking();
}}
disabled={status != "ditolak" && status != "antrian"}
>
Kirim
</Button>
</Group>
</>
)}
</>
);
}

View File

@@ -9,7 +9,7 @@ import {
Progress,
Stack,
Text,
Title
Title,
} from "@mantine/core";
export default function Dashboard() {

View File

@@ -73,7 +73,7 @@ export default function DashboardLayout() {
<AppShell
padding="lg"
navbar={{
width: 260,
width: 300,
breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened },
}}
@@ -285,45 +285,59 @@ function NavigationDashboard() {
return (
<Stack gap="xs" p="sm">
{navItems.filter((item) => permissions.includes(item.key)).map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute)}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{isActive(item.path as keyof typeof clientRoute) && (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() =>
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute)
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
{navItems
.filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink
key={item.path}
active={isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
leftSection={item.icon}
label={
<Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text>
{(
isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
)
&& (
<Badge
variant="light"
color="teal"
radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex>
}
description={item.description}
onClick={() =>
navigate(clientRoutes[item.path as keyof typeof clientRoute])
}
style={{
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
? "rgba(0,255,200,0.1)"
: "transparent",
borderRadius: "8px",
transition: "all 0.2s ease",
}}
styles={{
label: { color: "white" },
description: { color: "#aaa" },
section: { color: "teal" },
}}
/>
))}
</Stack>
);
}

View File

@@ -1,8 +1,12 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Anchor,
Badge,
Button,
@@ -14,23 +18,31 @@ import {
Group,
List,
Modal,
Select,
Spoiler,
Stack,
Table,
Text,
Textarea,
TextInput,
ThemeIcon,
Title
Title,
Tooltip
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCheck,
IconEdit,
IconFileCertificate,
IconFileCheck,
IconInfoCircle,
IconMessageReport,
IconPhone,
IconUser
IconUser,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
@@ -39,6 +51,11 @@ import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengajuanPage() {
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Pelayanan Surat", link: "/scr/dashboard/pelayanan-surat/list-pelayanan", active: false },
{ title: "Detail Pengajuan Surat", link: "#", active: true },
];
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
@@ -57,9 +74,20 @@ export default function DetailPengajuanPage() {
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
<DetailDataPengajuan
data={data?.data?.pengajuan}
warga={data && data.data && data.data.warga ? data.data.warga : undefined}
syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
@@ -71,7 +99,20 @@ export default function DetailPengajuanPage() {
);
}
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
function DetailDataPengajuan({
data,
warga,
syaratDokumen,
dataText,
onAction,
}: {
data: any;
warga?: { phone?: string | null } | null;
syaratDokumen: any;
dataText: any;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [keterangan, setKeterangan] = useState("");
@@ -80,7 +121,12 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
const [openedPreview, setOpenedPreview] = useState(false);
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState("");
const [viewImg, setViewImg] = useState({ file: "", folder: "" });
const [uploading, setUploading] = useState({ ok: false, file: "" });
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", satuan: null as string | null, option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
const [loadingFS, setLoadingFS] = useState({ value: false, text: "" })
useEffect(() => {
async function fetchHost() {
@@ -88,24 +134,97 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pelayanan"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pelayanan"),
);
setPermissions(onlySetting);
}
}
fetchHost();
}, []);
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try {
setLoadingFS({ value: true, text: "Sending message to warga" })
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "",
alasan: keterangan,
status,
linkSurat,
linkUpdate,
tlp: warga?.phone ?? "",
})
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
if (status == "selesai") {
onAction()
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} catch (error) {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
finally {
setLoadingFS({ value: false, text: "" })
}
}
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setLoadingFS({ value: true, text: "Updating status" })
const statusFix = cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai"
const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
status: statusFix,
keterangan: keterangan,
idUser: host?.id ?? "",
noSurat: noSurat
noSurat: noSurat,
});
if (res?.status === 200) {
if (statusFix == "selesai") {
setTimeout(() => {
setOpenedPreview(true)
}, 1000)
} else {
sendWA({
status: statusFix,
linkSurat: "",
linkUpdate: statusFix == "ditolak" ? res.data?.linkUpdate ?? '' : '',
});
}
onAction();
close();
notification({
@@ -120,7 +239,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
@@ -128,6 +246,45 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
message: "Failed to update pengajuan surat",
type: "error",
});
} finally {
setLoadingFS({ value: false, text: "" })
}
};
async function updateDataText() {
try {
setLoadingUpdate(true)
const res = await apiFetch.api.pelayanan["update-data-pelengkap"].post({
id: editValue.id,
value: editValue.val,
jenis: editValue.key,
idUser: host?.id ?? "",
})
if (res?.status === 200) {
notification({
title: "Success",
message: "Success update data",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
}
} catch (error) {
console.error(error)
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
} finally {
setLoadingUpdate(false)
setOpenEdit(false)
onAction()
}
}
@@ -137,15 +294,110 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
}
}, [viewImg]);
useShallowEffect(() => {
if (uploading.ok && uploading.file) {
sendWA({
status: "selesai",
linkSurat: uploading.file,
linkUpdate: "",
});
}
}, [uploading]);
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
return (
<>
<FullScreenLoading visible={loadingFS.value} text={loadingFS.text} />
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
onClose={() => setOpenEdit(false)}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{editValue.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={editValue.jenis} />}
data={editValue.option ?? []}
placeholder={editValue.jenis}
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
value={editValue.val}
/>
) : editValue.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
setEditValue({
...editValue,
val: formatted
})
}}
value={
editValue.val
? parseTanggalID(editValue.val)
: parseTanggalID(editValue.val)
}
/>
) : (
<TextInput
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
type={editValue.type}
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
value={editValue.val}
rightSection={
editValue.satuan != null &&
<Text mr={"lg"}>{editValue.satuan}</Text>
}
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
Batal
</Button>
<Button
variant="filled"
onClick={updateDataText}
disabled={loadingUpdate || !editValue.val}
loading={loadingUpdate}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
setOpenedPreviewFile(false)
setOpenedPreviewFile(false);
setViewImg({ file: "", folder: "" })
}}
folder="syarat-dokumen"
fileName={viewImg}
folder={viewImg.folder}
fileName={viewImg.file}
/>
{/* MODAL KONFIRMASI */}
@@ -159,14 +411,25 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
{catModal === "tolak" ? (
<>
<Text>
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan
penolakan
</Text>
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
<Textarea
size="md"
minRows={5}
value={keterangan}
onChange={(e) => setKeterangan(e.target.value)}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
<Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
>
Tolak
</Button>
</Group>
@@ -174,21 +437,31 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
) : (
<>
<Text>
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
{
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
}
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" />
)
}
{data.status == "diterima" && (
<Textarea
size="md"
minRows={5}
value={noSurat}
onChange={(e) => setNoSurat(e.target.value)}
placeholder="Contoh : 08/D-IV/11/2025"
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Tidak
</Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}>
<Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
disabled={data.status == "diterima" && noSurat.length < 1}
>
Ya
</Button>
</Group>
@@ -196,11 +469,18 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
)}
</Stack>
</Modal>
{
data?.status == "selesai" &&
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />)
}
{/* MODAL PREVIEW SURAT */}
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat
open={openedPreview}
onClose={(val) => {
setOpenedPreview(false)
setUploading({ ok: val.success, file: val.data })
}}
surat={data?.idSurat}
/>
)}
<Card
radius="md"
@@ -263,7 +543,11 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
>
{syaratDokumen?.map((v: any) => (
<List.Item key={v.id}>
<Anchor onClick={() => { setViewImg(v.value) }}>
<Anchor
onClick={() => {
setViewImg({ file: v.value, folder: "syarat-dokumen" });
}}
>
{v.jenis}
</Anchor>
</List.Item>
@@ -271,8 +555,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
</List>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconAlignJustified size={20} />
@@ -281,79 +563,116 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
<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>
))
}
{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%" }}>
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Text>
{_.upperFirst(item.value)} {item.satuan}
</Text>
<ActionIcon
variant="subtle"
aria-label="Edit"
onClick={() => {
setEditValue({ id: item.id, val: item.value, type: item.type, satuan: item.satuan, option: item.options, jenis: item.jenis, key: item.key })
setOpenEdit(true)
}}>
<IconEdit size={16} />
</ActionIcon>
</Flex>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Flex>
</Stack>
</Grid.Col>
<Grid.Col span={12}>
{
data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.antrian.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.antrian.terima")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.diterima.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.diterima.setujui")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Setujui
</Button>
</Group>
) : (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
)
}
{data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.antrian.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={!permissions.includes("pelayanan.antrian.terima")}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
disabled={!permissions.includes("pelayanan.diterima.tolak")}
variant="light"
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
disabled={
!permissions.includes("pelayanan.diterima.setujui")
}
variant="filled"
onClick={() => {
setCatModal("terima");
open();
}}
>
Setujui
</Button>
</Group>
) : data?.status === "selesai" ?
!data?.fileSurat ?
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setOpenedPreview(true) }}
>
Kirim Ulang Surat
</Button>
</Group>
)
:
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>
@@ -378,42 +697,48 @@ function DetailDataHistori({ data }: { data: any }) {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengajuan Surat
Riwayat Pengajuan Surat
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data?.map((item: any) => (
<Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{
item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
})
}</Table.Td>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</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.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table>
))}
</Table.Tbody>
</Table>
</Spoiler>
</Stack>
</Card>
);

View File

@@ -20,7 +20,7 @@ import {
IconClockHour3,
IconFileSad,
IconSearch,
IconUser
IconUser,
} from "@tabler/icons-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
@@ -123,7 +123,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
take: "",
page: page.toString(),
},
})
}),
);
useShallowEffect(() => {
@@ -131,12 +131,10 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
mutate();
}, [status, value]);
useShallowEffect(() => {
mutate();
}, [page]);
useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe();
@@ -189,8 +187,16 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
<Text
size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group>
</Grid.Col>
</Grid>
@@ -204,7 +210,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
Array.isArray(list) && list?.map((v: any) => (
Array.isArray(list) &&
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -266,7 +273,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
Tanggal Ajuan
</Text>
</Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
<Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -1,3 +1,4 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import ModalFile from "@/components/ModalFile";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
@@ -12,11 +13,12 @@ import {
Grid,
Group,
Modal,
Spoiler,
Stack,
Table,
Text,
Textarea,
Title
Title,
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
@@ -37,7 +39,13 @@ import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengaduanPage() {
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Pengaduan", link: "/scr/dashboard/pengaduan/list", active: false },
{ title: "Detail Pengaduan", link: "#", active: true },
];
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
@@ -56,9 +64,18 @@ export default function DetailPengaduanPage() {
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
<DetailDataPengaduan
data={data?.data?.pengaduan}
phone={data && data.data && data.data.warga ? data.data.warga.phone : null}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} />
</Stack>
</Grid.Col>
@@ -70,13 +87,22 @@ export default function DetailPengaduanPage() {
);
}
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
function DetailDataPengaduan({
data,
phone,
onAction,
}: {
data: any | null;
phone?: string | null;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [openedPreview, setOpenedPreview] = useState(false);
const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchHost() {
@@ -84,7 +110,9 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pengaduan"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pengaduan"),
);
setPermissions(onlySetting);
}
}
@@ -93,14 +121,37 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setIsLoading(true);
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',
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
keterangan: keterangan,
idUser: host?.id ?? ""
idUser: host?.id ?? "",
});
if (res?.status === 200) {
const resWA = await apiFetch.api["send-wa"].pengaduan.post({
noPengaduan: data?.noPengaduan,
judulPengaduan: data?.title,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
alasan: keterangan,
tlp: String(phone),
})
onAction();
close();
notification({
@@ -108,6 +159,28 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
message: "Success update pengaduan",
type: "success",
});
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Error",
@@ -115,7 +188,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
@@ -123,12 +195,13 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
message: "Failed to update pengaduan",
type: "error",
});
} finally {
setIsLoading(false);
}
}
};
return (
<>
{/* MODAL KONFIRMASI */}
<Modal
opened={opened}
@@ -143,24 +216,48 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text>
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
<Textarea
size="md"
minRows={5}
value={keterangan}
onChange={(e) => setKeterangan(e.target.value)}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
<Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
loading={isLoading}
>
Tolak
</Button>
</Group>
</>
) : (
<>
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} 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}>
Tidak
</Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
<Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
loading={isLoading}
>
Ya
</Button>
</Group>
@@ -169,7 +266,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
</Stack>
</Modal>
{/* MODAL GAMBAR */}
<ModalFile
open={openedPreview && !_.isEmpty(data?.image)}
@@ -259,18 +355,20 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
<IconPhotoScan size={20} />
<Text size="md">Gambar</Text>
</Group>
{
data?.image != null && data?.image != ""
?
<Anchor href="#" onClick={() => { setOpenedPreview(true) }}>
Lihat Gambar
</Anchor>
:
<Text size="md" c="white">
-
</Text>
}
{data?.image != null && data?.image != "" ? (
<Anchor
href="#"
onClick={() => {
setOpenedPreview(true);
}}
>
Lihat Gambar
</Anchor>
) : (
<Text size="md" c="white">
-
</Text>
)}
</Flex>
</Stack>
</Grid.Col>
@@ -285,74 +383,76 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
{_.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>
)
}
{data?.keterangan && (
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">
<IconInfoTriangle size={20} />
<Text size="md">Keterangan</Text>
</Group>
<Text size="md" c={"white"}>
{_.upperFirst(data?.keterangan)}
</Text>
</Flex>
)}
</Stack>
</Grid.Col>
<Grid.Col span={12}>
{
data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
variant="light"
disabled={!permissions.includes("pengaduan.antrian.tolak")}
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
disabled={!permissions.includes("pengaduan.antrian.terima")}
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={!permissions.includes("pengaduan.diterima.dikerjakan")}
onClick={() => {
setCatModal("terima");
open();
}}
>
Kerjakan
</Button>
</Group>
) : data?.status === "dikerjakan" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={!permissions.includes("pengaduan.dikerjakan.selesai")}
onClick={() => {
setCatModal("terima");
open();
}}
>
Selesai
</Button>
</Group>
) : <></>
}
{data?.status === "antrian" ? (
<Group justify="center" grow>
<Button
variant="light"
disabled={!permissions.includes("pengaduan.antrian.tolak")}
onClick={() => {
setCatModal("tolak");
open();
}}
>
Tolak
</Button>
<Button
variant="filled"
disabled={!permissions.includes("pengaduan.antrian.terima")}
onClick={() => {
setCatModal("terima");
open();
}}
>
Terima
</Button>
</Group>
) : data?.status === "diterima" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={
!permissions.includes("pengaduan.diterima.dikerjakan")
}
onClick={() => {
setCatModal("terima");
open();
}}
>
Kerjakan
</Button>
</Group>
) : data?.status === "dikerjakan" ? (
<Group justify="center" grow>
<Button
variant="filled"
disabled={
!permissions.includes("pengaduan.dikerjakan.selesai")
}
onClick={() => {
setCatModal("terima");
open();
}}
>
Selesai
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>
@@ -377,41 +477,48 @@ function DetailDataHistori({ data }: { data: any }) {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
Riwayat Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data?.map((item: any) => (
<Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{
item.createdAt.toLocaleString("id-ID", {
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false
})
}</Table.Td>
hour12: false,
})}
</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.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
</Table.Tr>
))
}
</Table.Tbody>
</Table>
))}
</Table.Tbody>
</Table>
</Spoiler>
</Stack>
</Card>
);

View File

@@ -134,7 +134,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
take: "",
page: page.toString(),
},
})
}),
);
useShallowEffect(() => {
@@ -175,7 +175,6 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const pageNow = data?.data?.page || 1;
const toDate = (d: any) => new Date(d);
return (
<Stack gap="xl">
<Grid>
@@ -197,8 +196,16 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Grid.Col>
<Grid.Col span={3}>
<Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} />
<Text
size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group>
</Grid.Col>
</Grid>
@@ -212,7 +219,8 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack>
</Flex>
) : (
Array.isArray(list) && list?.map((v: any) => (
Array.isArray(list) &&
list?.map((v: any) => (
<Card
key={v.id}
radius="lg"
@@ -272,7 +280,13 @@ function ListPengaduan({ status }: { status: StatusKey }) {
Tanggal Aduan
</Text>
</Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text>
<Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex>
<Flex direction={"column"} justify="flex-start">
<Group gap="xs">

View File

@@ -1,3 +1,4 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import DesaSetting from "@/components/DesaSetting";
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
import KategoriPengaduan from "@/components/KategoriPengaduan";
@@ -5,35 +6,39 @@ import ProfileUser from "@/components/ProfileUser";
import UserRoleSetting from "@/components/UserRoleSetting";
import UserSetting from "@/components/UserSetting";
import apiFetch from "@/lib/apiFetch";
import {
Card,
Container,
Grid,
NavLink
} from "@mantine/core";
import { Card, Container, Grid, NavLink } from "@mantine/core";
import {
IconBuildingBank,
IconCategory2,
IconMailSpark,
IconUserCog,
IconUserScreen,
IconUsersGroup
IconUsersGroup,
} from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
export default function DetailSettingPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const type = query.get("type");
const navigate = useNavigate();
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Setting", link: "#", active: false },
{ title: type == "cat-pengaduan" ? "Kategori Pengaduan" : type == "cat-pelayanan" ? "Kategori Pelayanan Surat" : type ? _.upperFirst(type) : "Profile", link: "#", active: true },
];
useEffect(() => {
async function fetchPermissions() {
const { data } = await apiFetch.api.user.find.get();
if (Array.isArray(data?.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("setting"));
const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("setting"),
);
setPermissions(onlySetting);
} else {
setPermissions([]);
@@ -42,7 +47,6 @@ export default function DetailSettingPage() {
fetchPermissions();
}, []);
const navItems = [
{
key: "setting.profile",
@@ -85,13 +89,15 @@ export default function DetailSettingPage() {
icon: <IconBuildingBank size={20} />,
label: "Desa",
description: "Manage desa information",
}
},
];
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={3}>
<Card
radius="md"
@@ -104,17 +110,19 @@ export default function DetailSettingPage() {
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
{
navItems.filter((item) => permissions.includes(item.key)).map((item) => (
{navItems
.filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink
key={item.key}
href={'?type=' + item.path}
onClick={()=>{navigate("?type=" + item.path)}}
label={item.label}
leftSection={item.icon}
active={type === item.path || (!type && item.path === 'profile')}
active={
type === item.path || (!type && item.path === "profile")
}
/>
))
}
))}
</Card>
</Grid.Col>
<Grid.Col span={9}>
@@ -130,17 +138,47 @@ export default function DetailSettingPage() {
}}
>
{type === "cat-pengaduan" ? (
<KategoriPengaduan permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pengaduan"))} />
<KategoriPengaduan
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pengaduan"),
)}
/>
) : type === "cat-pelayanan" ? (
<KategoriPelayananSurat permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pelayanan"))} />
<KategoriPelayananSurat
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pelayanan"),
)}
/>
) : type === "desa" ? (
<DesaSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.desa"))} />
<DesaSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.desa"),
)}
/>
) : type === "user" ? (
<UserSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user."))} />
<UserSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.user."),
)}
/>
) : type === "role" ? (
<UserRoleSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user_role"))} />
<UserRoleSetting
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.user_role"),
)}
/>
) : (
<ProfileUser permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.profile"))} />
<ProfileUser
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.profile"),
)}
/>
)}
</Card>
</Grid.Col>

View File

@@ -1,66 +1,146 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Avatar,
Box,
Button,
Card,
CloseButton,
Container,
Divider,
Flex,
Grid,
Group,
Input,
LoadingOverlay,
Pagination,
Stack,
Table,
Text,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconPhone } from "@tabler/icons-react";
import { IconPhone, IconSearch } from "@tabler/icons-react";
import _ from "lodash";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() {
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Warga", link: "/scr/dashboard/warga/list-warga", active: false },
{ title: "Detail Warga", link: "#", active: true },
];
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.warga.detail.get({
query: {
id: id!,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
// const { data, mutate, isLoading } = useSwr("/", () =>
// apiFetch.api.warga.detail.get({
// query: {
// id: id!,
// },
// }),
// );
// useShallowEffect(() => {
// mutate();
// }, []);
return (
<>
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<LoadingOverlay
// visible={isLoading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={4}>
<DetailWarga data={data?.data?.warga} />
<DetailWarga id={id!} />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
<DetailDataHistori
id={id!}
kategori="pengaduan"
/>
<DetailDataHistori
id={id!}
kategori="pelayanan"
/>
</Stack>
</Grid.Col>
</Grid>
</Container>
</>
);
}
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
function DetailDataHistori({
id,
kategori,
}: {
id: string;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate();
const [data, setData] = useState<any>([]);
const [totalPages, setTotalPages] = useState(1);
const [totalRows, setTotalRows] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
async function getData() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id,
category: kategori,
page: String(page),
search
}
}) as { data: { success: boolean; data: any[]; totalPages: number, totalRows: number } };
if (res?.data?.success) {
setData(res.data.data)
setTotalPages(res?.data?.totalPages)
setTotalRows(res?.data?.totalRows)
} else {
setData([])
setTotalPages(1)
setTotalRows(0)
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
}
useShallowEffect(() => {
getData()
}, [page])
useShallowEffect(() => {
setPage(1)
if (page == 1) {
getData()
}
}, [search]);
return (
<Card
@@ -79,49 +159,83 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
<Title order={4} c="gray.2">
Histori {_.upperFirst(kategori)}
</Title>
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Input
value={search}
placeholder="Cari data..."
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setSearch("")}
style={{ display: search ? undefined : "none" }}
/>
}
/>
<Text size="sm" c="gray.5" >
{`${5 * (page - 1) + 1} ${Math.min(totalRows, 5 * page)} of ${totalRows}`}
</Text>
<Pagination
total={totalPages}
value={page}
onChange={setPage}
withPages={false}
/>
</Flex>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>No {_.upperFirst(kategori)}</Table.Th>
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th>
<Table.Th>
{kategori == "pengaduan" ? "Judul" : "Kategori"}
</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th></Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
kategori == "pengaduan" ?
navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
) :
navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
)
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
{data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
<Table.Td>
{kategori == "pengaduan" ? item.title : item.category}
</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>
<Button
variant="outline"
onClick={() => {
kategori == "pengaduan"
? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
)
: navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
)
}
))
) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">
Tidak ada data
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>
@@ -129,7 +243,33 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
);
}
function DetailWarga({ data }: { data: any }) {
function DetailWarga({ id }: { id: string }) {
const [data, setData] = useState<any>(null);
async function getWarga() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id: id,
category: "warga",
page: "1",
search: "",
},
});
setData(res.data);
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data warga",
type: "error",
});
}
}
useShallowEffect(() => {
getWarga();
}, []);
return (
<Card
radius="md"

View File

@@ -39,7 +39,6 @@ export default function ListWargaPage() {
const pageSize = data?.data?.pageSize || 10;
const pageNow = data?.data?.page || 1;
useShallowEffect(() => {
setPages(1);
mutate();
@@ -49,7 +48,6 @@ export default function ListWargaPage() {
mutate();
}, [pages]);
return (
<Container size="xl" py="xl" w={"100%"}>
<Card
@@ -82,7 +80,12 @@ export default function ListWargaPage() {
/>
<Group>
<Text size="sm">{`${pageSize * (pages - 1) + 1} ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
<Pagination total={totalPage} value={pages} onChange={setPages} withPages={false} />
<Pagination
total={totalPage}
value={pages}
onChange={setPages}
withPages={false}
/>
</Group>
</Flex>
<Divider my={0} />
@@ -95,32 +98,33 @@ export default function ListWargaPage() {
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{
Array.isArray(list) && list?.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
{Array.isArray(list) && list?.length === 0 ? (
<Table.Tr>
<Table.Td colSpan={3} align="center">
Tidak ada data
</Table.Td>
</Table.Tr>
) : (
Array.isArray(list) &&
list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
) : (
Array.isArray(list) && list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
)
}
))
)}
</Table.Tbody>
</Table>
</Stack>

View File

@@ -8,16 +8,19 @@ export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }:
idCategory,
idWarga,
noSurat,
},
select: {
id: true
}
})
if (!surat.id) {
return { success: false, message: 'gagal membuat surat' }
return { success: false, message: 'gagal membuat surat', idSurat: '' }
}
return { success: true, message: 'surat sudah dibuat' }
return { success: true, message: 'surat sudah dibuat', idSurat: surat.id }
} catch (error) {
console.log(error)
console.error(error)
return { success: false, message: 'gagal membuat surat' }
}

View File

@@ -1,6 +1,6 @@
export function isValidPhone(number: string): boolean {
const clean = number.replace(/[\s.-]/g, ""); // hapus spasi, titik, strip
const regex = /^(?:\+628|08)(\d{7,12})$/;
const regex = /^(?:\+62|62|0)8\d{7,12}$/;
return regex.test(clean);
}

View File

@@ -248,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string)
return `✏️ Renamed ${oldName}${newName}`
}
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> {
const localName = localFile || remoteFile;
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`);
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
const localName = localFile || fileName;
// 🔹 gabungkan path folder + file
const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/");
// 🔹 encode path agar aman (spasi, dll)
const params = new URLSearchParams({
p: filePath,
});
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`);
if(!downloadUrlResponse.ok)
return 'gagal'
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
await fs.writeFile(localName, buffer);
return `⬇️ Downloaded ${remoteFile}${localName}`
return `⬇️ Downloaded ${fileName}${localName}`
}
export async function getFileLink(config: Config, fileName: string): Promise<string> {

View File

@@ -0,0 +1,18 @@
export function toSlug(text: string): string {
return encodeURIComponent(
text
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
);
}
export function fromSlug(slug: string): string {
return decodeURIComponent(slug)
.replace(/-/g, " ")
.replace(/\b\w/g, c => c.toUpperCase());
}
export function capitalizeWords(text: string): string {
return text.replace(/\b\w/g, c => c.toUpperCase());
}

View File

@@ -0,0 +1,11 @@
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import "dayjs/locale/id";
dayjs.extend(customParseFormat);
export function parseTanggalID( value: string ): Date | null {
if (!value) return null;
const parsed = dayjs(value, "DD MMMM YYYY", "id", true);
return parsed.isValid() ? parsed.toDate() : null;
}

View File

@@ -128,7 +128,8 @@ function convertToMcpContent(payload: any) {
export async function executeTool(
tool: any,
args: Record<string, any> = {},
baseUrl: string
baseUrl: string,
xPayload: Record<string, any> = {}
) {
const x = tool["x-props"] || {};
const method = (x.method || "GET").toUpperCase();
@@ -247,6 +248,9 @@ export async function executeTool(
// Execute fetch
console.log(`[MCP] → ${method} ${url}`);
for(const [key, value] of Object.entries(xPayload)) {
opts.headers![key] = value;
}
const res = await fetch(url, opts);
const resContentType = (res.headers.get("content-type") || "").toLowerCase();
@@ -281,7 +285,7 @@ export async function executeTool(
/* -------------------------
JSON-RPC Handler
------------------------- */
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
async function handleMCPRequestAsync(request: JSONRPCRequest, xPayload: Record<string, any>): Promise<JSONRPCResponse> {
const { id, method, params } = request;
const makeError = (code: number, message: string, data?: any): JSONRPCResponse => ({
@@ -331,7 +335,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
const baseUrl = (params?.credentials?.baseUrl as string) || process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
const args = params?.arguments || {};
const result = await executeTool(tool, args, baseUrl);
const result = await executeTool(tool, args, baseUrl, xPayload);
// Extract the meaningful payload (prefer nested .data if present)
const raw = extractRaw(result.data);
@@ -365,7 +369,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
Elysia App & Routes
------------------------- */
export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
.post("/mcp", async ({ request, set }) => {
.post("/mcp", async ({ request, set, headers }) => {
set.headers["Content-Type"] = "application/json";
set.headers["Access-Control-Allow-Origin"] = "*";
@@ -378,12 +382,17 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
}
}
const xPayload = {
['x-user']: headers['x-user'] || "",
['x-phone']: headers['x-phone'] || ""
}
try {
const body = await request.json();
// If batch array -> allSettled for resilience
if (Array.isArray(body)) {
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req));
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req, xPayload));
const settled = await Promise.allSettled(promises);
const responses = settled.map((s) =>
s.status === "fulfilled"
@@ -401,7 +410,7 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
return responses;
}
const single = await handleMCPRequestAsync(body as JSONRPCRequest);
const single = await handleMCPRequestAsync(body as JSONRPCRequest, xPayload);
return single;
} catch (err: any) {
set.status = 400;

View File

@@ -5,6 +5,8 @@ import { getLastUpdated } from "../lib/get-last-updated"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { toSlug } from "../lib/slug_converter"
const PelayananRoute = new Elysia({
prefix: "pelayanan",
@@ -19,13 +21,25 @@ const PelayananRoute = new Elysia({
},
orderBy: {
name: "asc"
},
select: {
id: true,
name: true,
syaratDokumen: true,
dataPelengkap: true,
}
})
return data
const dataFix = data.map(item => ({
...item,
link: `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/surat?jenis=${toSlug(item.name)}`
}));
return dataFix
}, {
detail: {
summary: "List Kategori Pelayanan Surat",
description: `tool untuk mendapatkan list kategori pelayanan surat beserta syaratnya untuk memenuhi syarat dokumen sesuai kategori yg dipilih saat melakukan pengajuan surat`,
description: `tool untuk mendapatkan list kategori pelayanan surat dan juga berisi link form pengajuan surat`,
tags: ["mcp"]
}
})
@@ -101,11 +115,60 @@ const PelayananRoute = new Elysia({
description: `tool untuk delete kategori pelayanan surat`
}
})
.get("/category/detail", async ({ query }) => {
const { id } = query
const data = await prisma.categoryPelayanan.findUnique({
where: {
id
}
})
if (!data) {
return;
}
const dataPelengkap: { key: string }[] = Array.isArray(data.dataPelengkap)
? data.dataPelengkap.filter(
(v): v is { key: string } =>
typeof v === "object" &&
v !== null &&
"key" in v &&
typeof (v as any).key === "string"
)
: [];
const syaratDokumen: { name: string }[] = Array.isArray(data.syaratDokumen)
? data.syaratDokumen.filter(
(v): v is { name: string } =>
typeof v === "object" &&
v !== null &&
"name" in v &&
typeof (v as any).name === "string"
)
: [];
return {
id: data.id,
name: data.name,
dataPelengkap,
syaratDokumen,
};
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "Detail Kategori Pelayanan Surat by ID",
description: `tool untuk mendapatkan detail kategori pelayanan surat berdasarkan id`,
}
})
// --- PELAYANAN SURAT ---
.get("/", async ({ query }) => {
const { phone } = query
.get("/", async ({ query, headers }) => {
// const { phone } = query
const phone = headers['x-phone'] || ""
const data = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "asc"
@@ -115,13 +178,34 @@ const PelayananRoute = new Elysia({
Warga: {
phone
}
},
select: {
noPengajuan: true,
status: true,
createdAt: true,
CategoryPelayanan: {
select: {
name: true
}
}
}
})
return data
const dataFix = data.map((item) => {
return {
noPengajuan: item.noPengajuan,
status: item.status,
category: item.CategoryPelayanan.name,
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
}
})
return dataFix
}, {
query: t.Object({
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
}),
// query: t.Object({
// phone: t.String({ minLength: 1, error: "phone harus diisi" }),
// }),
detail: {
summary: "List Ajuan Pelayanan Surat by Phone",
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
@@ -130,17 +214,9 @@ const PelayananRoute = new Elysia({
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pelayananAjuan.findFirst({
where: {
OR: [
{
noPengajuan: id
},
{
id: id
}
]
id: id
},
select: {
id: true,
@@ -151,8 +227,8 @@ const PelayananRoute = new Elysia({
CategoryPelayanan: {
select: {
name: true,
dataText: true,
syaratDokumen: true,
dataPelengkap: true
}
},
Warga: {
@@ -170,6 +246,17 @@ const PelayananRoute = new Elysia({
}
})
if (!data) {
const datafix = {
pengajuan: {},
history: [],
warga: {},
syaratDokumen: [],
dataText: [],
}
return datafix
}
const dataSurat = await prisma.suratPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
@@ -178,6 +265,7 @@ const PelayananRoute = new Elysia({
select: {
id: true,
idCategory: true,
file: true
}
})
@@ -207,10 +295,11 @@ const PelayananRoute = new Elysia({
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
name: string;
desc: string;
key: string;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
return {
id: item.id,
jenis: desc,
@@ -218,14 +307,42 @@ const PelayananRoute = new Elysia({
}
})
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 dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
type: string;
options?: {
label: string,
value: string
}[]; name: string;
desc: string;
key: string;
satuan?: string;
}[];
const refMap = new Map(
dataTextCategory.map((v, i) => [
v.key,
{ ...v, order: i }
])
);
const dataTextFix = dataText
.map((item) => {
const ref = refMap.get(item.jenis);
const nama = dataTextCategory.find((v) => v.key == item.jenis)?.name
return {
id: item.id,
jenis: nama,
key: ref?.key,
value: item.value,
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
satuan: ref?.satuan ?? null
};
})
.sort((a, b) => a.order - b.order)
.map(({ order, ...rest }) => rest); // hapus order
const dataHistory = await prisma.historyPelayanan.findMany({
where: {
@@ -242,6 +359,9 @@ const PelayananRoute = new Elysia({
name: true,
}
}
},
orderBy: {
createdAt: "desc"
}
})
@@ -271,6 +391,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
idSurat: dataSurat?.id,
fileSurat: dataSurat?.file,
}
const datafix = {
@@ -280,22 +401,25 @@ const PelayananRoute = new Elysia({
syaratDokumen: dataSyaratFix,
dataText: dataTextFix,
}
return datafix
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "Detail Ajuan Pelayanan Surat",
description: `tool untuk mendapatkan detail ajuan pelayanan surat`,
tags: ["mcp"]
summary: "Detail Ajuan Pelayanan Surat by ID",
description: `tool untuk mendapatkan detail ajuan pelayanan surat berdasarkan id`,
}
})
.post("/create", async ({ body }) => {
const { kategoriId, namaWarga, noTelepon, dataText, syaratDokumen } = body
.post("/create", async ({ body, headers }) => {
const { kategoriId, dataPelengkap, syaratDokumen, nama, phone } = body
// const namaWarga = headers['x-user'] || ""
// const noTelepon = headers['x-phone'] || ""
const noPengajuan = await generateNoPengajuanSurat()
let idCategoryFix = kategoriId
let idWargaFix = ""
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: kategoriId,
@@ -317,21 +441,21 @@ const PelayananRoute = new Elysia({
}
if (!isValidPhone(noTelepon)) {
if (!isValidPhone(phone)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const nomorHP = normalizePhoneNumber({ phone: phone })
const dataWarga = await prisma.warga.upsert({
where: {
phone: nomorHP
},
create: {
name: namaWarga,
name: nama,
phone: nomorHP,
},
update: {
name: namaWarga,
name: nama,
},
select: {
id: true
@@ -362,20 +486,21 @@ const PelayananRoute = new Elysia({
dataInsertSyaratDokumen.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
jenis: item.key,
value: item.value,
})
}
for (const item of dataText) {
for (const item of dataPelengkap) {
dataInsertDataText.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
jenis: item.key,
value: item.value,
})
}
await prisma.syaratDokumenPelayanan.createMany({
data: dataInsertSyaratDokumen,
})
@@ -392,7 +517,7 @@ const PelayananRoute = new Elysia({
}
})
return { success: true, message: 'pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' }
return { success: true, message: 'Pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini', noPengajuan }
}, {
body: t.Object({
kategoriId: t.String({
@@ -400,21 +525,20 @@ const PelayananRoute = new Elysia({
examples: ["skusaha"],
error: "ID kategori harus diisi"
}),
namaWarga: t.String({
nama: t.String({
description: "Nama warga",
examples: ["Budi Santoso"],
error: "Nama warga harus diisi"
}),
noTelepon: t.String({
phone: t.String({
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
}),
dataText: t.Array(
dataPelengkap: t.Array(
t.Object({
jenis: t.String({
key: t.String({
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "jenis harus diisi"
@@ -429,25 +553,25 @@ const PelayananRoute = new Elysia({
description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [
[
{ jenis: "nama", value: "Budi Santoso" },
{ jenis: "jenis kelamin", value: "Laki-laki" },
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ jenis: "negara", value: "Indonesia" },
{ jenis: "agama", value: "Islam" },
{ jenis: "status perkawinan", value: "Belum menikah" },
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
{ jenis: "jenis usaha", value: "usaha makanan" },
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
{ key: "nama", value: "Budi Santoso" },
{ key: "jenis kelamin", value: "Laki-laki" },
{ key: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ key: "negara", value: "Indonesia" },
{ key: "agama", value: "Islam" },
{ key: "status perkawinan", value: "Belum menikah" },
{ key: "alamat", value: "Jl. Mawar No. 10" },
{ key: "pekerjaan", value: "Karyawan Swasta" },
{ key: "jenis usaha", value: "usaha makanan" },
{ key: "alamat usaha", value: "Jl. Melati No. 21" },
]
],
error: "dataText harus berupa array"
error: "Data Pelengkap harus berupa array"
}
),
syaratDokumen: t.Array(
t.Object({
jenis: t.String({
key: t.String({
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "jenis harus diisi"
@@ -462,18 +586,249 @@ const PelayananRoute = new Elysia({
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" }
{ key: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ key: "ktp/kk", value: "kk_budi.png" },
{ key: "foto lokasi", value: "foto_lokasi_budi.png" }
]
],
error: "syaratDokumen harus berupa array"
error: "Syarat Dokumen harus berupa array"
}
),
}),
detail: {
summary: "Buat Pengajuan Pelayanan Surat",
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
}
})
.post("/detail-data", async ({ body }) => {
const { nomerPengajuan } = body
const data = await prisma.pelayananAjuan.findFirst({
where: {
noPengajuan: {
equals: nomerPengajuan,
mode: "insensitive"
}
},
select: {
id: true,
noPengajuan: true,
status: true,
createdAt: true,
updatedAt: true,
CategoryPelayanan: {
select: {
name: true,
dataPelengkap: true,
syaratDokumen: true,
}
},
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
},
}
})
if (!data) {
return {
success: false,
message: "Data tidak ditemukan",
pengajuan: {},
history: [],
warga: {},
syaratDokumen: [],
dataPelengkap: [],
}
}
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 dataPelengkap = 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;
key: string;
required: boolean;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
const required = syaratDokumen.find((v) => v.key == item.jenis)?.required
return {
id: item.id,
key: item.jenis,
value: item.value,
name: name ?? '',
desc: desc ?? '',
required: required ?? true
}
})
const dataPelengkapList = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
type: string;
options?: {
label: string,
value: string
}[];
name: string;
desc: string;
key: string;
required: boolean;
satuan?: string;
}[];
const refMap = new Map(
dataPelengkapList.map((v, i) => [
v.key,
{ ...v, order: i }
])
);
const dataTextFix = dataPelengkap
.map((item) => {
const ref = refMap.get(item.jenis);
return {
id: item.id,
key: item.jenis,
value: item.value,
desc: ref?.desc ?? "",
name: ref?.name ?? "",
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
required: ref?.required ?? true,
satuan: ref?.satuan ?? null
};
})
.sort((a, b) => a.order - b.order)
.map(({ order, ...rest }) => rest); // hapus order
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 alasanDitolak = await prisma.historyPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
status: "ditolak"
},
select: {
keteranganAlasan: true,
},
orderBy: {
createdAt: "desc"
}
})
const dataHistoryFix = dataHistory.map((item) => {
return {
id: item.id,
deskripsi: item.deskripsi,
status: item.status,
createdAt: item.createdAt,
idUser: item.idUser,
nameUser: item.User?.name,
}
})
const 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,
alasan: alasanDitolak?.keteranganAlasan,
}
const datafix = {
success: true,
message: 'sukses',
pengajuan: dataPengajuan,
linkUpdate: `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/update-data-surat?pengajuan=${data.noPengajuan}`,
history: dataHistoryFix,
warga: warga,
syaratDokumen: dataSyaratFix,
dataPelengkap: dataTextFix,
}
return datafix
}, {
body: t.Object({
nomerPengajuan: t.String({
description: "Nomor pengajuan pelayanan surat yang ingin diakses.",
examples: ["PS-101225-001", "PS-101225-002"],
error: "Nomor pengajuan harus diisi"
})
}),
detail: {
summary: "Detail Pengajuan Pelayanan Surat By Nomor Pengajuan",
description: `tool untuk mendapatkan detail pengajuan pelayanan surat berdasarkan nomor pengajuan`,
tags: ["mcp"]
}
})
@@ -497,9 +852,14 @@ const PelayananRoute = new Elysia({
})
if (!pengajuan) {
return { success: false, message: 'gagal update status pengajuan surat' }
return { success: false, message: 'gagal update status pengajuan surat', linkUpdate: '', idSurat: '' }
}
const dataPengajuan = await prisma.pelayananAjuan.findUnique({
where: { id: pengajuan.id },
select: { noPengajuan: true }
});
if (status === "diterima") {
deskripsi = "Pengajuan surat diterima"
} else if (status === "ditolak") {
@@ -518,11 +878,19 @@ const PelayananRoute = new Elysia({
}
})
let idSurat = "";
if (status === "selesai") {
await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
const result = await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
idSurat = result.idSurat ?? "";
}
return { success: true, message: 'pengajuan surat sudah diperbarui' }
return {
success: true,
message: 'pengajuan surat sudah diperbarui',
linkUpdate: status == "ditolak" ? `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/update-data-surat?pengajuan=${dataPengajuan?.noPengajuan}` : '',
idSurat: idSurat,
}
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -534,7 +902,235 @@ const PelayananRoute = new Elysia({
detail: {
summary: "Update Status Pengajuan Pelayanan Surat",
description: `tool untuk update status pengajuan pelayanan surat`,
tags: ["mcp"]
}
})
.post("/update", async ({ body }) => {
const { id, syaratDokumen, dataPelengkap } = body
let dataUpdate = []
const pengajuan = await prisma.pelayananAjuan.findUnique({
where: {
id
}
})
if (!pengajuan) {
return { success: false, message: 'data pengajuan surat tidak ditemukan' }
}
if (pengajuan.status != "ditolak" && pengajuan.status != "antrian") {
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
}
if (dataPelengkap && dataPelengkap.length > 0) {
for (const item of dataPelengkap) {
dataUpdate.push(item.key)
const upd = await prisma.dataTextPelayanan.update({
where: {
id: item.id
},
data: {
value: item.value,
}
})
}
}
const category = await prisma.categoryPelayanan.findUnique({
where: {
id: pengajuan.idCategory,
}
})
type SyaratDokumen = {
desc: string;
name: string;
};
const syarat = category?.syaratDokumen as SyaratDokumen[] | undefined
if (syaratDokumen && syaratDokumen.length > 0) {
for (const item of syaratDokumen) {
dataUpdate.push(item.key)
const upd = await prisma.syaratDokumenPelayanan.update({
where: {
id: item.id
},
data: {
value: item.value,
}
})
}
}
const keys = dataUpdate.join(", ");
if (pengajuan.status == "ditolak") {
const updStatus = await prisma.pelayananAjuan.update({
where: {
id: pengajuan.id,
},
data: {
status: "antrian",
}
})
}
const history = await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: pengajuan.id,
deskripsi: `Pengajuan surat diupdate oleh warga (data yg diupdate: ${keys})`,
status: "antrian",
}
})
return { success: true, message: 'pengajuan surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({
error: "id harus diisi",
description: "ID yang ingin diupdate"
}),
dataPelengkap: t.Optional(t.Array(
t.Object({
id: t.String({
description: "ID Data Pelengkap.",
error: "id harus diisi"
}),
key: t.String({
description: "Key field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "key harus diisi"
}),
value: t.String({
description: "Isi atau nilai dari jenis field terkait.",
examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"],
error: "value harus diisi"
}),
}),
{
description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [
[
{ id: "1", key: "nama", value: "Budi Santoso" },
{ id: "2", key: "jenis kelamin", value: "Laki-laki" },
{ id: "3", key: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ id: "4", key: "negara", value: "Indonesia" },
{ id: "5", key: "agama", value: "Islam" },
{ id: "6", key: "status perkawinan", value: "Belum menikah" },
{ id: "7", key: "alamat", value: "Jl. Mawar No. 10" },
{ id: "8", key: "pekerjaan", value: "Karyawan Swasta" },
{ id: "9", key: "jenis usaha", value: "usaha makanan" },
{ id: "10", key: "alamat usaha", value: "Jl. Melati No. 21" },
]
],
}
)),
syaratDokumen: t.Optional(t.Array(
t.Object({
id: t.String({
description: "ID syarat dokumen",
error: "id harus diisi"
}),
key: t.String({
description: "Key dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "key harus diisi"
}),
value: t.String({
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: [
[
{ id: "1", key: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ id: "2", key: "ktp/kk", value: "kk_budi.png" },
{ id: "3", key: "foto lokasi", value: "foto_lokasi_budi.png" }
]
],
}
)),
}),
detail: {
summary: "Update Data Pengajuan Pelayanan Surat",
description: `tool untuk update data pengajuan pelayanan surat`,
}
})
.post("/update-data-pelengkap", async ({ body }) => {
const { id, value, jenis, idUser } = body
const dataPelengkap = await prisma.dataTextPelayanan.findUnique({
where: {
id
},
select: {
idPengajuanLayanan: true,
PelayananAjuan: {
select: {
status: true
}
}
}
})
if (!dataPelengkap) {
return { success: false, message: 'data pelengkap surat tidak ditemukan' }
}
const upd = await prisma.dataTextPelayanan.update({
where: {
id
},
data: {
value: value,
}
})
const history = await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: dataPelengkap.idPengajuanLayanan,
deskripsi: `Pengajuan surat diupdate oleh user (data yg diupdate: ${jenis})`,
status: dataPelengkap.PelayananAjuan.status,
idUser
}
})
return { success: true, message: 'data pelengkap surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({
error: "id harus diisi",
description: "ID yang ingin diupdate"
}),
value: t.String({
error: "value harus diisi",
description: "Value yang ingin diupdate"
}),
jenis: t.String({
error: "jenis harus diisi",
description: "Jenis data yang ingin diupdate"
}),
idUser: t.String({
error: "idUser harus diisi",
description: "ID user yang melakukan update"
})
}),
detail: {
summary: "Update Data Pelengkap Pengajuan Pelayanan Surat oleh user admin",
description: `tool untuk update data pelengkap pengajuan pelayanan surat oleh user admin`,
}
})
.get("/list", async ({ query }) => {
@@ -681,5 +1277,47 @@ const PelayananRoute = new Elysia({
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
}
})
.post("/get-no-pengajuan", async ({ body }) => {
const { phone, noPengajuan } = body;
if (!isValidPhone(phone)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
}
const nomorHP = normalizePhoneNumber({ phone: phone })
const data = await prisma.pelayananAjuan.findMany({
where: {
noPengajuan: noPengajuan,
Warga: {
phone: nomorHP
}
},
select: {
id: true,
noPengajuan: true,
status: true,
createdAt: true,
}
});
if (data.length == 0) {
return { success: false, message: 'Data pengajuan tidak ditemukan' };
}
return {
success: true,
nomer: noPengajuan
};
}, {
body: t.Object({
phone: t.String({ minLength: 1, error: "Nomor telepon harus diisi" }),
noPengajuan: t.String({ minLength: 1, error: "Nomor pengajuan harus diisi" }),
}),
detail: {
summary: "Cek Nomor Pengajuan Surat",
description: `tool untuk memeriksa apakah nomor pengajuan surat valid dan terkait dengan nomor telepon warga. Jika valid, mengembalikan nomor pengajuan.`,
}
})
export default PelayananRoute

View File

@@ -1,14 +1,16 @@
import Elysia, { t } from "elysia"
import type { StatusPengaduan } from "generated/prisma"
import _ from "lodash"
import { v4 as uuidv4 } from "uuid"
import { getLastUpdated } from "../lib/get-last-updated"
import { mimeToExtension } from "../lib/mimetypeToExtension"
import { generateNoPengaduan } from "../lib/no-pengaduan"
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { renameFile } from "../lib/rename-file"
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile"
import Elysia, { t } from "elysia";
import fs from 'fs';
import type { StatusPengaduan } from "generated/prisma";
import _ from "lodash";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { getLastUpdated } from "../lib/get-last-updated";
import { mimeToExtension } from "../lib/mimetypeToExtension";
import { generateNoPengaduan } from "../lib/no-pengaduan";
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone";
import { prisma } from "../lib/prisma";
import { renameFile } from "../lib/rename-file";
import { catFile, defaultConfigSF, downloadFile, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile";
const PengaduanRoute = new Elysia({
prefix: "pengaduan",
@@ -107,8 +109,10 @@ const PengaduanRoute = new Elysia({
// --- PENGADUAN ---
.post("/create", async ({ body }) => {
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, namaWarga, noTelepon } = body
.post("/create", async ({ body, headers }) => {
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId } = body
const namaWarga = headers['x-user'] || ""
const noTelepon = headers['x-phone'] || ""
let imageFix = namaGambar
const noPengaduan = await generateNoPengaduan()
let idCategoryFix = kategoriId
@@ -128,19 +132,21 @@ const PengaduanRoute = new Elysia({
}
})
if (!cariCategory) {
idCategoryFix = "lainnya"
} else {
idCategoryFix = cariCategory.id
}
}
} else {
idCategoryFix = "lainnya"
}
if (!isValidPhone(noTelepon)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
return { success: false, message: `nomor telepon ${noTelepon} tidak valid, harap masukkan nomor yang benar` }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
@@ -216,20 +222,20 @@ const PengaduanRoute = new Elysia({
})),
kategoriId: t.Optional(t.String({
examples: ["kebersihan"],
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
examples: ["kebersihan", "infrastruktur", "keamanan"],
description: "Nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
})),
namaWarga: t.String({
examples: ["budiman"],
description: "Nama warga yang melapor"
}),
// namaWarga: t.String({
// examples: ["budiman"],
// description: "Nama warga yang melapor"
// }),
noTelepon: t.String({
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
}),
// noTelepon: t.String({
// error: "Nomor telepon harus diisi",
// examples: ["08123456789", "+628123456789"],
// description: "Nomor telepon warga pelapor"
// }),
}),
detail: {
@@ -289,18 +295,90 @@ const PengaduanRoute = new Elysia({
description: `tool untuk update status pengaduan`
}
})
.post("/update", async ({ body }) => {
const { noPengaduan, judul, detail, lokasi, namaGambar } = body
let dataUpdate = {}
const cek = await prisma.pengaduan.findFirst({
where: {
noPengaduan,
},
select: {
id: true
}
})
if (!cek) {
return { success: false, message: 'gagal update status pengaduan, nomer ' + noPengaduan + ' tidak ditemukan' }
}
if (judul) {
dataUpdate = { title: judul }
}
if (detail) {
dataUpdate = { ...dataUpdate, detail }
}
if (lokasi) {
dataUpdate = { ...dataUpdate, location: lokasi }
}
if (namaGambar) {
dataUpdate = { ...dataUpdate, image: namaGambar }
}
const pengaduan = await prisma.pengaduan.updateMany({
where: {
noPengaduan
},
data: dataUpdate
})
const keys = Object.keys(dataUpdate).join(", ");
await prisma.historyPengaduan.create({
data: {
idPengaduan: cek.id,
deskripsi: `Pengaduan diupdate oleh warga (data yg diupdate: ${keys})`,
}
})
return { success: true, message: 'pengaduan dengan nomer ' + noPengaduan + ' sudah diupdate' }
}, {
body: t.Object({
noPengaduan: t.String({
error: "nomer pengaduan harus diisi",
description: "Nomer pengaduan yang ingin diupdate"
}),
judul: t.Optional(t.String({
error: "judul harus diisi",
description: "Judul pengaduan yang ingin diupdate"
})),
detail: t.Optional(t.String({
description: "detail pengaduan yang ingin diupdate"
})),
lokasi: t.Optional(t.String({
description: "lokasi pengaduan yang ingin diupdate"
})),
namaGambar: t.Optional(t.String({
description: "Nama file gambar yang telah diupload untuk update data pengaduan"
})),
}),
detail: {
summary: "Update Data Pengaduan",
description: `tool untuk update data pengaduan`,
tags: ["mcp"]
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const data = await prisma.pengaduan.findFirst({
where: {
OR: [
{
noPengaduan: id
}, {
id: id
}
]
id: id
},
select: {
id: true,
@@ -335,6 +413,16 @@ const PengaduanRoute = new Elysia({
}
})
if (!data) {
const datafix = {
pengaduan: {},
history: [],
warga: null,
}
return datafix
}
const dataHistory = await prisma.historyPengaduan.findMany({
where: {
idPengaduan: data?.id,
@@ -350,6 +438,9 @@ const PengaduanRoute = new Elysia({
name: true,
}
}
},
orderBy: {
createdAt: "desc"
}
})
@@ -389,50 +480,51 @@ const PengaduanRoute = new Elysia({
}
return datafix
}, {
detail: {
summary: "Detail Pengaduan Warga",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
tags: ["mcp"]
summary: "Detail Pengaduan Warga By ID",
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id pengaduan`,
}
})
.get("/", async ({ query }) => {
const { take, page, search, phone } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
.get("/", async ({ query, headers }) => {
// const { take, page, search } = query
const phone = headers['x-phone'] || ""
// const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
const data = await prisma.pengaduan.findMany({
skip,
take: !take ? 10 : Number(take),
// skip,
// take: !take ? 10 : Number(take),
orderBy: {
createdAt: "asc"
},
where: {
isActive: true,
OR: [
{
title: {
contains: search ?? "",
mode: "insensitive"
},
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
},
{
detail: {
contains: search ?? "",
mode: "insensitive"
},
}
],
AND: {
Warga: {
phone: phone
}
}
// OR: [
// {
// title: {
// contains: search ?? "",
// mode: "insensitive"
// },
// },
// {
// noPengaduan: {
// contains: search ?? "",
// mode: "insensitive"
// },
// },
// {
// detail: {
// contains: search ?? "",
// mode: "insensitive"
// },
// }
// ],
// AND: {
// Warga: {
// phone: phone
// }
// }
},
select: {
id: true,
@@ -467,12 +559,11 @@ const PengaduanRoute = new Elysia({
return dataFix
}, {
query: t.Object({
take: t.String({ optional: true }),
page: t.String({ optional: true }),
search: t.String({ optional: true }),
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
}),
// query: t.Object({
// take: t.String({ optional: true }),
// page: t.String({ optional: true }),
// search: t.String({ optional: true }),
// }),
detail: {
summary: "List Pengaduan Warga By Phone",
description: `tool untuk mendapatkan list pengaduan warga by phone`,
@@ -513,10 +604,46 @@ const PengaduanRoute = new Elysia({
detail: {
summary: "Upload File (FormData)",
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
// tags: ["mcp"],
consumes: ["multipart/form-data"]
},
})
.get("/download", async ({ query, set }) => {
const { file, folder } = query;
// Validasi file
if (!file) {
return { success: false, message: "File tidak ditemukan" };
}
// if (!folder) {
// return { success: false, message: "Folder tidak ditemukan" };
// }
const localPath = path.join("/tmp", file);
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer();
const result = await downloadFile(defaultConfigSF, file, 'surat', localPath);
if(result=="gagal") {
return { success: false, message: "Download gagal" };
}
set.headers["Content-Type"] = "application/pdf";
set.headers["Content-Disposition"] = `attachment; filename="${file}"`;
// 🔹 kirim file ke browser
return fs.createReadStream(localPath);
}, {
body: t.Object({
file: t.Any(),
folder: t.String(),
}),
detail: {
summary: "Download Surat",
description: "Tool untuk download surat dari Seafile",
},
})
.post("/upload-file-form-data", async ({ body }) => {
const { file } = body;
@@ -557,7 +684,6 @@ const PengaduanRoute = new Elysia({
detail: {
summary: "Upload File (FormData)",
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
// tags: ["mcp"],
consumes: ["multipart/form-data"]
},
})
@@ -599,7 +725,6 @@ const PengaduanRoute = new Elysia({
detail: {
summary: "Upload File (Base64)",
description: "Tool untuk upload file ke Seafile dalam format Base64",
// tags: ["mcp"],
consumes: ["multipart/form-data"]
},
})
@@ -802,6 +927,118 @@ const PengaduanRoute = new Elysia({
description: "Tool untuk delete file Seafile",
},
})
.post("/detail-data", async ({ body }) => {
const { nomerPengaduan } = body
const data = await prisma.pengaduan.findFirst({
where: {
noPengaduan: nomerPengaduan
},
select: {
id: true,
noPengaduan: true,
title: true,
detail: true,
location: true,
image: true,
idCategory: true,
idWarga: true,
status: true,
keterangan: true,
createdAt: true,
updatedAt: true,
CategoryPengaduan: {
select: {
name: true
}
},
Warga: {
select: {
name: true,
phone: true,
_count: {
select: {
Pengaduan: true,
PelayananAjuan: true,
}
}
}
}
}
})
if (!data) {
return { success: false, message: "Data tidak ditemukan" };
}
const dataHistory = await prisma.historyPengaduan.findMany({
where: {
idPengaduan: data?.id,
},
select: {
id: true,
deskripsi: true,
status: true,
createdAt: true,
idUser: true,
User: {
select: {
name: true,
}
}
}
})
const dataHistoryFix = dataHistory.map((item: any) => ({
..._.omit(item, ["User", "createdAt"]),
nameUser: item.User?.name,
createdAt: item.createdAt
}))
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,
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
}, {
body: t.Object({
nomerPengaduan: t.String({
description: "Nomer pengaduan yg ingin diakses",
examples: ["PGD-101225-001", "PGD-101225-002"],
error: "Nomer pengaduan harus diisi",
}),
}),
detail: {
summary: "Detail Pengaduan Warga By Nomor Pengaduan",
description: `tool untuk mendapatkan detail data pengaduan berdasarkan nomor pengaduan`,
tags: ["mcp"]
}
})
;
export default PengaduanRoute

View File

@@ -0,0 +1,153 @@
import Elysia, { t } from "elysia";
const SendWaRoute = new Elysia({
prefix: "send-wa",
tags: ["send-wa"],
})
// --- KATEGORI PENGADUAN ---
.post("/pengaduan", async ({ body }) => {
const { noPengaduan, judulPengaduan, status, alasan, tlp } = body
let text = ""
if (status === "ditolak") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Kami informasikan bahwa aduan tersebut tidak dapat ditindaklanjuti (ditolak).
Alasan penolakan:${alasan}
Terima kasih atas pengertian Bapak/Ibu.`
} else if (status == "diterima") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Telah kami terima dan akan segera diproses sesuai ketentuan yang berlaku.
Terima kasih atas laporan Bapak/Ibu.`
} else if (status == "dikerjakan") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Saat ini sedang dalam proses penanganan oleh petugas terkait.
Mohon menunggu informasi selanjutnya.`
} else if (status == "selesai") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Telah selesai ditindaklanjuti.
Terima kasih atas partisipasi dan kepercayaan Bapak/Ibu.`
}
const textFix = encodeURIComponent(text)
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
}
);
if (res.status !== 200)
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
}, {
body: t.Object({
noPengaduan: t.String({ minLength: 1, error: "nomer pengaduan harus diisi" }),
judulPengaduan: t.String({ minLength: 1, error: "judul pengaduan harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
alasan: t.String({ optional: true }),
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
}),
detail: {
summary: "Send pemberitahuan pengaduan lewat WA",
description: `tool untuk send pemberitahuan pengaduan lewat WA`
}
})
.post("/pengajuan-surat", async ({ body }) => {
const { noPengajuan, jenisSurat, status, alasan, tlp, linkSurat, linkUpdate } = body
let text = ""
if (status === "ditolak") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut tidak dapat diproses (ditolak).
Alasan penolakan: ${alasan}
Bapak/Ibu dapat melakukan perbaikan atau pembaruan data melalui tautan berikut:
👉 ${linkUpdate}
Setelah data diperbarui, pengajuan akan diproses kembali sesuai ketentuan yang berlaku.
Terima kasih atas pengertian Bapak/Ibu.`
} else if (status == "diterima") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat yang Bapak/Ibu ajukan telah kami terima dan sedang menunggu proses verifikasi serta penanganan lebih lanjut.
Terima kasih atas kesabaran Bapak/Ibu.`
} else if (status == "selesai") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut telah selesai diproses.
Bapak/Ibu dapat mengunduh surat melalui tautan berikut:
👉 ${linkSurat}
Terima kasih atas kepercayaan Bapak/Ibu.`
}
const textFix = encodeURIComponent(text)
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
}
);
if (res.status !== 200)
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
}, {
body: t.Object({
noPengajuan: t.String({ minLength: 1, error: "nomer pengajuan harus diisi" }),
jenisSurat: t.String({ minLength: 1, error: "jenis surat harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
alasan: t.String({ optional: true }),
linkSurat: t.String({ optional: true }),
linkUpdate: t.String({ optional: true }),
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
}),
detail: {
summary: "Send pemberitahuan pengajuan surat lewat WA",
description: `tool untuk send pemberitahuan pengajuan surat lewat WA`
}
})
;
export default SendWaRoute

View File

@@ -17,6 +17,7 @@ const SuratRoute = new Elysia({
noSurat: true,
idCategory: true,
createdAt: true,
file: true,
PelayananAjuan: {
select: {
DataTextPelayanan: true,
@@ -44,6 +45,7 @@ const SuratRoute = new Elysia({
idCategory: dataSurat?.idCategory,
nameCategory: dataSurat?.CategoryPelayanan?.name,
noSurat: dataSurat?.noSurat,
file: dataSurat?.file,
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
},
@@ -60,6 +62,33 @@ const SuratRoute = new Elysia({
}
})
.post("/update", async ({ body }) => {
const { id, filename } = body
await prisma.suratPelayanan.update({
where: {
id,
},
data: {
file: filename,
}
})
return {
success: true,
message: 'surat sudah diperbarui',
link: `${process.env.BUN_PUBLIC_BASE_URL}/api/pengaduan/download?file=${filename}`
}
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
filename: t.String({ minLength: 1, error: "filename harus diisi" }),
}),
detail: {
summary: "update file surat",
description: `tool untuk update file surat`
}
})
;
export default SuratRoute

View File

@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const { id, category, search, page } = query
const skip = !page ? 0 : (Number(page) - 1) * 5
const dataWarga = await prisma.warga.findUnique({
where: {
id
}
})
const dataPengaduan = await prisma.pengaduan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
if (!dataWarga)
return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
if (category == "warga") {
return dataWarga
} else if (category == "pengaduan") {
const where: any = {
isActive: true,
idWarga: id
},
select: {
id: true,
status: true,
noPengaduan: true,
title: true
idWarga: id,
OR: [
{
title: {
contains: search ?? "",
mode: "insensitive"
},
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
}
]
}
})
const totalData = await prisma.pengaduan.count({
where
});
const dataPengaduan = await prisma.pengaduan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
status: true,
noPengaduan: true,
title: true
}
})
const dataReturn = {
success: true,
message: "data pengaduan berhasil diambil",
data: dataPengaduan,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
}
const dataPelayanan = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
return dataReturn
} else if (category == "pelayanan") {
const where: any = {
isActive: true,
idWarga: id
},
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
idWarga: id,
OR: [
{
CategoryPelayanan: {
name: {
contains: search ?? "",
mode: "insensitive"
},
},
},
{
noPengajuan: {
contains: search ?? "",
mode: "insensitive"
},
},
]
}
const totalData = await prisma.pelayananAjuan.count({
where
});
const dataPelayanan = await prisma.pelayananAjuan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
}
}
}
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
const dataReturn = {
success: true,
message: "data pelayanan berhasil diambil",
data: dataPelayanFix,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
}
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
return {
warga: dataWarga,
pengaduan: dataPengaduan,
pelayanan: dataPelayanFix
return dataReturn
}
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" })
id: t.String({ minLength: 1, error: "id harus diisi" }),
category: t.String({ minLength: 1, error: "kategori harus diisi" }),
page: t.String({ optional: true }),
search: t.String({ optional: true }),
}),
detail: {
summary: "Detail Warga",