Compare commits

...

2 Commits

Author SHA1 Message Date
17b20e0d40 Add Layanan Polsek submenu polsek terdekat
Seeder menu keamanan -> menu ekonomi submenu : demografi pekerjaan, junlah pengangguran, lowongan kerja lokal, pasar desa, program kemiskinan, sektor unggulan, struktur organisasi
2026-01-17 10:32:48 +08:00
184854d273 Fix Table Admin Preview Desktop
Seeder Menu Kesehatan
2026-01-13 11:45:55 +08:00
176 changed files with 4711 additions and 1739 deletions

View File

@@ -0,0 +1,56 @@
[
{
"id": "cmkf3kv0b0004vnys6hh7ugj2",
"pekerjaan": "Petani/Pekebun",
"lakiLaki": 180,
"perempuan": 120
},
{
"id": "cmkf3kv0b0004vnys6hh8vhk3",
"pekerjaan": "Perajin Industri",
"lakiLaki": 95,
"perempuan": 140
},
{
"id": "cmkf3kv0b0004vnys6hh9wil4",
"pekerjaan": "Pedagang/UMKM",
"lakiLaki": 130,
"perempuan": 170
},
{
"id": "cmkf3kv0b0004vnys6hh0xjm5",
"pekerjaan": "Karyawan Swasta",
"lakiLaki": 260,
"perempuan": 310
},
{
"id": "cmkf3kv0b0004vnys6hh1ykn6",
"pekerjaan": "PNS/TNI/Polri",
"lakiLaki": 85,
"perempuan": 75
},
{
"id": "cmkf3kv0b0004vnys6hh2zlo7",
"pekerjaan": "Buruh Harian Lepas",
"lakiLaki": 140,
"perempuan": 90
},
{
"id": "cmkf3kv0b0004vnys6hh3amp8",
"pekerjaan": "Wiraswasta",
"lakiLaki": 165,
"perempuan": 110
},
{
"id": "cmkf3kv0b0004vnys6hh4bnq9",
"pekerjaan": "Pelajar/Mahasiswa",
"lakiLaki": 220,
"perempuan": 240
},
{
"id": "cmkf3kv0b0004vnys6hh5cor0",
"pekerjaan": "Belum/Tidak Bekerja",
"lakiLaki": 70,
"perempuan": 95
}
]

View File

@@ -0,0 +1,57 @@
[
{
"id": "3b3e817b-c136-4488-ac79-9a7d408939a2",
"posisi": "Lowongan TPS3R Pudak Mesari",
"namaPerusahaan": "TPS3R Pudak Mesari Desa Darmasaba",
"lokasi": "Desa Darmasaba, Abiansemal, Kabupaten Badung, Bali",
"tipePekerjaan": "Freelance",
"gaji": "1.500.000",
"deskripsi": "Menjalankan tugas di TPS3R Pudak Mesari.",
"kualifikasi": "Usia 18-30 tahun, SMA/SMK minimal",
"notelp": "089647037426"
},
{
"id": "3b3e817b-c136-4488-bd80-9a7d408939a2",
"posisi": "Marketing Executive",
"namaPerusahaan": "PT Mitra Krida Mandiri (Dealer Honda MKM Darmasaba)",
"lokasi": "Jalan Raya Darmasaba No.169, Abiansemal, Badung, Bali",
"tipePekerjaan": "Full Time",
"gaji": "2.500.000",
"deskripsi": "Menjalankan tugas pemasaran dan penjualan produk Honda di area Darmasaba.",
"kualifikasi": "Usia 18-30 tahun, SMA/SMK minimal, memiliki sepeda motor Honda dan smartphone.",
"notelp": "081296001047"
},
{
"id": "3b3e817b-c136-4488-ce91-9a7d408939a2",
"posisi": "Kasir",
"namaPerusahaan": "GOGO DARMASABA",
"lokasi": "Jl. Raya Darmasaba, Darmasaba, Kec. Abiansemal, Kabupaten Badung, Bali",
"tipePekerjaan": "Full Time",
"gaji": "2.500.000",
"deskripsi": "Melakukan pelayanan kasir dan administrasi pelanggan di restoran/food service.",
"kualifikasi": "Wanita, 18-30 tahun, SMA/SMK minimal pengalaman 1-3 tahun sebagai kasir atau Customer Service.",
"notelp": "089647037426"
},
{
"id": "3b3e817b-c136-4488-df02-9a7d408939a2",
"posisi": "Kasir / Teknisi Handphone",
"namaPerusahaan": "Jaya Cell Darmasaba",
"lokasi": "Jl. Raya Darmasaba, Darmasaba, Abiansemal, Badung, Bali",
"tipePekerjaan": "Full Time",
"gaji": "2.000.000",
"deskripsi": "Melakukan pelayanan kasir serta teknisi ponsel termasuk troubleshooting dan perbaikan.",
"kualifikasi": "Tidak disebutkan pengalaman khusus, memiliki KTP dan keinginan kuat untuk bekerja.",
"notelp": "089647037426"
},
{
"id": "3b3e817b-c136-4488-eg13-9a7d408939a2",
"posisi": "Guru Les (Pengajar Anak)",
"namaPerusahaan": "Bimba AIUEO Darmasaba",
"lokasi": "Darmasaba, Kabupaten Badung, Bali",
"tipePekerjaan": "Full Time",
"gaji": "2.000.000",
"deskripsi": "Mengajar calistung dan perkembangan dasar anak usia 3-6 tahun.",
"kualifikasi": "Minimal SMA/SMK, komunikasi baik, berinteraksi dengan anak-anak.",
"notelp": "089647037426"
}
]

View File

@@ -6,5 +6,17 @@
{
"id": "5c06chf7-123f-6hfe-0663-5e9h76e55060",
"nama": "Minuman"
},
{
"id": "5c06chf7-123f-7igd-0663-5e9h76e55060",
"nama": "Sembako"
},
{
"id": "5c06chf7-123f-8jhe-0663-5e9h76e55060",
"nama": "Sayur Mayur"
},
{
"id": "5c06chf7-123f-9kif-0663-5e9h76e55060",
"nama": "Protein Hewani"
}
]

View File

@@ -0,0 +1,42 @@
[
{
"id": "f6b52033-5016-45d9-b0fd-b9d4b6c4729b",
"kategoriId": "5c06chf7-123f-9kif-0663-5e9h76e55060",
"pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789"
},
{
"id": "d2ef373c-043c-44b5-adde-6a25a54199d3",
"kategoriId": "5c06chf7-123f-7igd-0663-5e9h76e55060",
"pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789"
},
{
"id": "ad427752-fea0-4ef3-a312-5961eefd5ee3",
"kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"pasarDesaId": "d62660a2-ac6b-428a-acf6-58cc837ef789"
},
{
"id": "bd00ab59-7ac8-4d40-94de-a86bb0eb4557",
"kategoriId": "5c06chf7-123f-8jhe-0663-5e9h76e55060",
"pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438"
},
{
"id": "b7d311a2-a23a-499d-a339-823c5e30849a",
"kategoriId": "5c06chf7-123f-7igd-0663-5e9h76e55060",
"pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438"
},
{
"id": "50ccc6c9-92c1-4d86-9585-85d48d35f640",
"kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"pasarDesaId": "24c6b992-49da-4c6e-aebb-72cf89f75438"
},
{
"id": "3b27f795-1d1d-4655-90f9-b779a009094e",
"kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"pasarDesaId": "6dea2257-b710-4cd2-8d94-9b6737e658d8"
},
{
"id": "d45873c5-5948-40f9-a88d-aa0861132bae",
"kategoriId": "4b95bge6-012e-5ged-9552-4d8g65d44959",
"pasarDesaId": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2"
}
]

View File

@@ -0,0 +1,46 @@
[
{
"id": "1b7a17ea-83f7-4e73-a94d-96e2b4a623f2",
"nama": "Warung Pasar Darmasaba",
"harga": 30000,
"imageId": "cmkew56ls0000vnysrnzr9ttx",
"rating": 4.3,
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba, Kec. Abiansemal",
"kontak": "081234567890",
"deskripsi": "Warung tradisional yang menjual kebutuhan pokok harian seperti sembako, jajanan pasar, dan minuman.",
"kategoriProdukId": "5c06chf7-123f-7igd-0663-5e9h76e55060"
},
{
"id": "6dea2257-b710-4cd2-8d94-9b6737e658d8",
"nama": "Jajanan Pasar Bu Made",
"imageId": "cmkewaa2s0001vnysvvs9tu56",
"harga": 5000,
"rating": 4.6,
"alamatUsaha": "Jl. Raya Darmasaba, dekat Banjar Baler Pasar",
"kontak": "082145678901",
"deskripsi": "Menjual berbagai jajanan pasar tradisional Bali seperti laklak, klepon, dan pisang rai.",
"kategoriProdukId": "4b95bge6-012e-5ged-9552-4d8g65d44959"
},
{
"id": "24c6b992-49da-4c6e-aebb-72cf89f75438",
"nama": "Sayur Segar Pak Wayan",
"imageId": "cmkewcvfq0002vnys6985nm90",
"harga": 20000,
"rating": 4.4,
"alamatUsaha": "Area Pasar Desa Darmasaba",
"kontak": "087865432109",
"deskripsi": "Lapak sayur segar yang menyediakan sayuran lokal hasil petani sekitar Desa Darmasaba.",
"kategoriProdukId": "5c06chf7-123f-8jhe-0663-5e9h76e55060"
},
{
"id": "d62660a2-ac6b-428a-acf6-58cc837ef789",
"nama": "Ayam & Daging Segar Darmasaba",
"imageId": "cmkewf4u90003vnys87en35nj",
"harga": 80000,
"rating": 4.2,
"alamatUsaha": "Br. Baler Pasar, Desa Darmasaba",
"kontak": "081998877665",
"deskripsi": "Menjual ayam potong dan daging segar untuk kebutuhan rumah tangga dan upacara adat.",
"kategoriProdukId": "5c06chf7-123f-9kif-0663-5e9h76e55060"
}
]

View File

@@ -0,0 +1,23 @@
[
{
"id": "dd92a029-cd7d-4b60-8a3b-dd88e61fe715",
"nama": "BLT-DD (Bantuan Langsung Tunai Dana Desa)",
"icon": "bantuan",
"deskripsi": "<p>Program pemberian Bantuan Langsung Tunai yang dibiayai dari Dana Desa untuk meringankan beban ekonomi keluarga miskin/prasejahtera di Desa Darmasaba.</p>",
"statistikId": "d59481a3-ff7f-4e52-cd5c-89e143eeb869"
},
{
"id": "dd92a029-cd7d-4b60-9b4c-dd88e61fe715",
"nama": "Penguatan Ketahanan Pangan",
"icon": "air",
"deskripsi": "<p>Kegiatan pemberdayaan masyarakat dalam ketahanan pangan untuk mendukung ketersediaan pangan keluarga kurang mampu dan meningkatkan kemampuan produksi pangan lokal.</p>",
"statistikId": "d59481a3-ff7f-4e52-de6d-89e143eeb869"
},
{
"id": "dd92a029-cd7d-4b60-0c5d-dd88e61fe715",
"nama": "Peningkatan IKM berbasis E-commerce",
"icon": "ekonomi",
"deskripsi": "<p>Program peningkatan keterampilan usaha mikro kecil (IKM) termasuk pelatihan branding, pengemasan, dan promosi digital untuk memperkuat ekonomi rumah tangga melalui pemasaran online.</p>",
"statistikId": "d59481a3-ff7f-4e52-df7e-89e143eeb869"
}
]

View File

@@ -0,0 +1,17 @@
[
{
"id": "d59481a3-ff7f-4e52-cd5c-89e143eeb869",
"tahun": 2023,
"jumlah": 20
},
{
"id": "d59481a3-ff7f-4e52-de6d-89e143eeb869",
"tahun": 2024,
"jumlah": 30
},
{
"id": "d59481a3-ff7f-4e52-df7e-89e143eeb869",
"tahun": 2025,
"jumlah": 20
}
]

View File

@@ -0,0 +1,44 @@
[
{
"id": "053999e8-e5c4-4a50-b587-0e0ce15aba1a",
"name": "Pertanian",
"description": "Sektor pertanian meliputi kegiatan bercocok tanam padi, palawija, dan tanaman lain di subak yang menjadi basis mata pencaharian warga",
"value": 90
},
{
"id": "8e0d2f2d-512d-4c05-8880-b6e7d144a34d",
"name": "UMKM Kecil",
"description": "Usaha Mikro Kecil Menengah termasuk IKM berbasis pengolahan pangan dan kuliner yang tumbuh di desa sebagai penggerak ekonomi lokal",
"value": 45
},
{
"id": "0378b10a-f0e3-421c-9272-225d931179ce",
"name": "Peternakan",
"description": "Peternakan ikan lele dan mata pencaharian lain yang mendukung ketahanan pangan dan ekonomi masyarakat desa",
"value": 30
},
{
"id": "4fa28680-8014-4c46-9dd0-1aa910630fd3",
"name": "BUMDes",
"description": "BUMDes Pudak Mesari sebagai lembaga usaha desa yang mengembangkan potensi lokal dan layanan ekonomi",
"value": 20
},
{
"id": "669464b2-dd7e-44be-b609-97a9b844df8b",
"name": "Kawasan Kuliner",
"description": "Potensi kawasan kuliner desa yang menjadi daya tarik ekonomi dan pariwisata kecil di daerah Darmasaba",
"value": 15
},
{
"id": "ef65e122-84ce-4483-93e9-c1a8bcee9b79",
"name": "Pariwisata",
"description": "Potensi wisata lokal seperti Jogging Track, taman dan water park yang memberikan nilai tambah ekonomi masyarakat",
"value": 35
},
{
"id": "08443c84-8ca9-4690-b900-e5e3e753cc97",
"name": "Kerajinan Genteng",
"description": "Kerajinan genteng press di Desa Adat Tegal yang merupakan usaha kerajinan lokal dengan kontribusi ekonomi",
"value": 25
}
]

View File

@@ -926,5 +926,248 @@
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/TDQReg1lQ73s39crXW0ra-desktop.webp",
"category": "image"
},
{
"id": "cmkao2zm90007vntzxqkjy5mt",
"name": "d6hJgycQawWN3VEcHaqtR-desktop.webp",
"realName": "puskesmas.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/d6hJgycQawWN3VEcHaqtR-desktop.webp",
"category": "image"
},
{
"id": "cmkatoru10000vny38y0wxd6s",
"name": "cg78Sb_QzZFlli9s2FPVc-desktop.webp",
"realName": "puskesmas2.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/cg78Sb_QzZFlli9s2FPVc-desktop.webp",
"category": "image"
},
{
"id": "cmkay1e590010vn6y24pgaa1r",
"name": "hLeF0GRFZqDUngZnDMAAk-desktop.webp",
"realName": "pk1.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/hLeF0GRFZqDUngZnDMAAk-desktop.webp",
"category": "image"
},
{
"id": "cmkay6hob0011vn6ybjwejcej",
"name": "hyyTFi8EApjzFEZ9EvJgB-desktop.webp",
"realName": "pk2.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/hyyTFi8EApjzFEZ9EvJgB-desktop.webp",
"category": "image"
},
{
"id": "cmkay8vmd0012vn6ylsk2vzfo",
"name": "l4qsUEw2JiclGAkkrXp9g-desktop.webp",
"realName": "pk3.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/l4qsUEw2JiclGAkkrXp9g-desktop.webp",
"category": "image"
},
{
"id": "cmkayd8o90013vn6ye7n8805q",
"name": "Gc79mlIlGuoRQuTqskFj--desktop.webp",
"realName": "pk-4.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Gc79mlIlGuoRQuTqskFj--desktop.webp",
"category": "image"
},
{
"id": "cmkayi0x90016vn6ykddxqyq3",
"name": "OsMY3AYPyGC_CoN1xUjOn-desktop.webp",
"realName": "posyandu1.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/OsMY3AYPyGC_CoN1xUjOn-desktop.webp",
"category": "image"
},
{
"id": "cmkaykipf0019vn6yknjno3k1",
"name": "M9QlgVKIEfCdY3g4F_tRZ-desktop.webp",
"realName": "pk6.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/M9QlgVKIEfCdY3g4F_tRZ-desktop.webp",
"category": "image"
},
{
"id": "cmkayz2h8001cvn6yrb7uptjs",
"name": "Gi8EX3pBmT719AfzXirDS-desktop.webp",
"realName": "pd1.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/Gi8EX3pBmT719AfzXirDS-desktop.webp",
"category": "image"
},
{
"id": "cmkawq38m0009vn6yi7evbhap",
"name": "v7Ac2xQvTiJy-HYh1AxF4-desktop.webp",
"realName": "posko-siaga.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/v7Ac2xQvTiJy-HYh1AxF4-desktop.webp",
"category": "image"
},
{
"id": "cmkawso29000cvn6y879ahra0",
"name": "jYxEXspWH5g6eTTVqK72c-desktop.webp",
"realName": "ambulance.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/jYxEXspWH5g6eTTVqK72c-desktop.webp",
"category": "image"
},
{
"id": "cmkawu7qj000fvn6yubhimyiv",
"name": "3tNQ9J8I3Ewq5H8CWuqvp-desktop.webp",
"realName": "penanganan darurat.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/3tNQ9J8I3Ewq5H8CWuqvp-desktop.webp",
"category": "image"
},
{
"id": "cmkb6488i001fvn6ylkddch1j",
"name": "g4ICsRrmOaIqS_yqlQLZK-desktop.webp",
"realName": "puskesmas2.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/g4ICsRrmOaIqS_yqlQLZK-desktop.webp",
"category": "image"
},
{
"id": "cmkb681og001gvn6ykb5uasln",
"name": "1NkzPzQailqE5yNOiUjB9-desktop.webp",
"realName": "puskesmas.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/1NkzPzQailqE5yNOiUjB9-desktop.webp",
"category": "image"
},
{
"id": "cmkb6brrf0000vn14u8c7wnox",
"name": "NBPAqjPXn7GQmYTDBI5hu-desktop.webp",
"realName": "kd3.webp",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/NBPAqjPXn7GQmYTDBI5hu-desktop.webp",
"category": "image"
},
{
"id": "cmkb6ehpi0001vn14hjp4tdye",
"name": "EcQIGOF6LW1dIKE53vmba-desktop.webp",
"realName": "kd4.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/EcQIGOF6LW1dIKE53vmba-desktop.webp",
"category": "image"
},
{
"id": "cmkbynxxo0000vn67wi2nsyl3",
"name": "pps1ZgzJxDb4VZxEvtZeu-desktop.webp",
"realName": "infowp-1.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/pps1ZgzJxDb4VZxEvtZeu-desktop.webp",
"category": "image"
},
{
"id": "cmkbyr3mk0003vn673xrqv8xv",
"name": "JhJigMo269K1TFGzSB1OS-desktop.webp",
"realName": "infowp-2.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/JhJigMo269K1TFGzSB1OS-desktop.webp",
"category": "image"
},
{
"id": "cmkax3o8g000rvn6ygqpmo1nb",
"name": "5giLSHSnWEFoZoMEcjhL7-desktop.webp",
"realName": "diare.webp",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/5giLSHSnWEFoZoMEcjhL7-desktop.webp",
"category": "image"
},
{
"id": "cmkax5ukz000uvn6yho3aj2nf",
"name": "3faPo-1wjhVDVU6S7S8sS-desktop.webp",
"realName": "tbc.png",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/3faPo-1wjhVDVU6S7S8sS-desktop.webp",
"category": "image"
},
{
"id": "cmkax72nw000xvn6ymcuvlzom",
"name": "DyX82oztXbHfu6HEvbrpt-desktop.webp",
"realName": "dbd.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/DyX82oztXbHfu6HEvbrpt-desktop.webp",
"category": "image"
},
{
"id": "cmkc2tcn30000vnt9esmx8kyb",
"name": "K0wY911212dinYA3AFB_f-desktop.webp",
"realName": "keamananl-1.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/K0wY911212dinYA3AFB_f-desktop.webp",
"category": "image"
},
{
"id": "cmkc2xm1z0003vnt98682dv0a",
"name": "x0_-siY2V8IehBzo4_uph-desktop.webp",
"realName": "keamananl-2.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/x0_-siY2V8IehBzo4_uph-desktop.webp",
"category": "image"
},
{
"id": "cmkc36q2j0006vnt9g87h5it4",
"name": "TXknK9CSRSxwvM2hPW6BO-desktop.webp",
"realName": "pecalang.jpeg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/TXknK9CSRSxwvM2hPW6BO-desktop.webp",
"category": "image"
},
{
"id": "cmkccs50d0000vn2mfuk0d9dw",
"name": "U7rePDZq5E59z-Eo9tLBe-desktop.webp",
"realName": "tips-keamanan-1.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/U7rePDZq5E59z-Eo9tLBe-desktop.webp",
"category": "image"
},
{
"id": "cmkccyh7t0003vn2mjdrqtuu0",
"name": "TTur8BttDlAS9UgZVe3M8-desktop.webp",
"realName": "tipskaman.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/TTur8BttDlAS9UgZVe3M8-desktop.webp",
"category": "image"
},
{
"id": "cmkdu8kb20002vn4lihwo4k86",
"name": "6DQbAvn0St-xHdPGW3vpY-desktop.webp",
"realName": "Jamban4.jpg",
"path": "uploads/images",
"mimeType": "image/webp",
"link": "/api/fileStorage/findUnique/6DQbAvn0St-xHdPGW3vpY-desktop.webp",
"category": "image"
}
]

View File

@@ -0,0 +1,20 @@
[
{
"id": "cmkc2tcs00002vnt9c0ssj05n",
"name": "Sosialisasi dan Pembinaan Keamanan Lingkungan Desa Darmasaba",
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan Sosialisasi dan Pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai di Wantilan Perum Darmasaba Permai, Desa Darmasaba. Kegiatan ini melibatkan Perbekel Darmasaba, Bhabinkamtibmas, Babinsa, anggota BPD, LPM Desa, KBD dan KBA untuk mengajak warga berperan aktif dalam menjaga keamanan lingkungan, serta mendukung pemasangan lampu penerangan jalan guna mencegah kriminalitas dan kecelakaan di wilayah lingkungan.</p>",
"imageId": "cmkc2tcn30000vnt9esmx8kyb"
},
{
"id": "cmkc2xmdh0005vnt9ri6f4nk8",
"name": "Sinergi Aparat dan Masyarakat untuk Keamanan Lingkungan",
"deskripsi": "<p>Desa Darmasaba bersama aparat seperti Polres Badung dan elemen masyarakat berkomitmen menjalin sinergi untuk menciptakan keamanan dan ketertiban lingkungan yang kondusif, memperkuat kepedulian serta tindakan nyata dalam menjaga situasi kamtibmas desa.</p>",
"imageId": "cmkc2xm1z0003vnt98682dv0a"
},
{
"id": "cmkc36qbl0008vnt9odvekex6",
"name": "Peran Sistem Keamanan Lingkungan (Siskamling) dan Pecalang di Bali",
"deskripsi": "<p>Sistem keamanan lingkungan (Siskamling) di Bali termasuk di Desa Darmasaba melibatkan kolaborasi antara pemerintah desa, satlinmas, dan pecalang sebagai pranata adat Bali. Sinergi ini penting untuk menjaga ketertiban masyarakat serta harmoni sosial berdasarkan kearifan lokal seperti Tri Hita Karana, meskipun perlu pembinaan dan koordinasi terus menerus dari desa dan aparat terkait.</p>",
"imageId": "cmkc36q2j0006vnt9g87h5it4"
}
]

View File

@@ -0,0 +1,20 @@
[
{
"id": "keamanan-polisi",
"nama": "Kepolisian",
"icon": "keamanan",
"kategoriId": "item-polisi"
},
{
"id": "keamanan-damkar",
"nama": "Pemadam Kebakaran",
"icon": "pemadam",
"kategoriId": "item-damkar"
},
{
"id": "keamanan-sar",
"nama": "SAR & Evakuasi",
"icon": "sar",
"kategoriId": "item-sar"
}
]

View File

@@ -0,0 +1,17 @@
[
{
"id": "map-polisi-1",
"kontakDaruratId": "keamanan-polisi",
"kontakItemId": "item-polsek-darmasaba"
},
{
"id": "map-polisi-2",
"kontakDaruratId": "keamanan-polisi",
"kontakItemId": "item-polres-badung"
},
{
"id": "map-damkar-1",
"kontakDaruratId": "keamanan-damkar",
"kontakItemId": "item-damkar-badung"
}
]

View File

@@ -0,0 +1,45 @@
[
{
"id": "item-polisi",
"nama": "Polisi",
"nomorTelepon": "110",
"icon": "keamanan"
},
{
"id": "item-damkar",
"nama": "Pemadam Kebakaran",
"nomorTelepon": "113",
"icon": "pemadam"
},
{
"id": "item-sar",
"nama": "BASARNAS",
"nomorTelepon": "115",
"icon": "sar"
},
{
"id": "item-polsek-darmasaba",
"nama": "Polsek Darmasaba",
"nomorTelepon": "0361123456",
"icon": "bangunan"
},
{
"id": "item-polres-badung",
"nama": "Polres Badung",
"nomorTelepon": "0361123999",
"icon": "bangunan"
},
{
"id": "item-damkar-badung",
"nama": "Damkar Kabupaten Badung",
"nomorTelepon": "0361900113",
"icon": "pemadam"
},
{
"id": "item-bpbd-badung",
"nama": "BPBD Badung",
"nomorTelepon": "0361900113",
"icon": "sar"
}
]

View File

@@ -0,0 +1,16 @@
[
{
"id": "cmkdt14my0000vn4lrvfv6jxr",
"judul": "LAPORAN REALISASI APBDES SEMESTER I TAHUN ANGGARAN 2025",
"lokasi": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung",
"tanggalWaktu": "2025-08-04T08:58:55.080Z",
"kronologi": "<p>Pemerintah Desa Darmasaba menyampaikan realisasi pendapatan dan belanja desa hingga semester I tahun 2025 yang mencakup berbagai sumber pendapatan dan rincian belanja desa.</p>"
},
{
"id": "cmkdt14my0000vn4lrvfv7kys",
"judul": "Aksi Penanganan Maraknya Pembuangan Sampah Liar di Wilayah Desa Darmasaba",
"lokasi": "Desa Darmasaba, Kabupaten Badung",
"tanggalWaktu": "2025-11-24T08:58:55.080Z",
"kronologi": "<p>Tim mendatangi rumah pihak yang diduga melakukan pembuangan sampah liar, melakukan pendataan dan penelusuran, serta koordinasi lintas wilayah untuk memastikan penanganan yang tepat.</p>"
}
]

View File

@@ -0,0 +1,12 @@
[
{
"id": "cmkdt41lx0001vn4lrlcqz735",
"laporanId": "cmkdt14my0000vn4lrvfv6jxr",
"deskripsi": "<p>Laporan ini disampaikan sebagai bentuk komitmen transparansi pengelolaan keuangan desa dan dapat dimonitor oleh masyarakat.</p>"
},
{
"id": "cmkdt41lx0002vn4lrlcqz846",
"laporanId": "cmkdt14my0000vn4lrvfv7kys",
"deskripsi": "<p>Pemerintah Desa bersama Penyidik Lingkungan Hidup melakukan investigasi lapangan terhadap laporan masyarakat mengenai aksi pembuangan sampah liar, serta melakukan koordinasi untuk penindakan sesuai ketentuan.</p>"
}
]

View File

@@ -0,0 +1,23 @@
[
{
"id": "cmh48mn850003qq091pvs7rf1",
"judul": "Maling Motor di Darmasaba, Residivis Begal Didor",
"deskripsi": "<p>Maling Motor di Darmasaba, Residivis Begal Didor David Andiansyah kembali terancam dibui lantaran melakukan aksi pencurian.</p>",
"deskripsiSingkat": "<p>Maling Motor di Darmasaba, Residivis Begal Didor David Andiansyah kembali terancam dibui lantaran melakukan aksi pencurian. Mantan narapidana kasus begal ini diciduk polisi usai mencuri sepeda motor di garase rumah, kawasan Banjar Cabe, Desa Darmasaba, Abiansemal, Badung, Kamis (24/6). Kasi Humas Polres Badung Iptu Ketut Sudana menerangkan, peristiwa tersebut bermula ketika korban Ni Made Desniari berkunjung ke rumah tetangganya Putu Juliati. Saat itu korban memarkirkan sepeda motor Honda Beat miliknya di garase (TKP), tetapi kunci motornya masih nyantol di motor. Ketika korban kembali sekitar pukul 13.00, didapati motornya telah raib. Atas kejadian ini wanita asli Banjar Cabe melapor ke polisi.</p>",
"linkVideo": "https://www.youtube.com/embed/2rxK5A-KoeY"
},
{
"id": "cmh59no850003qq091pvs7rf1",
"judul": "Integrasi Digital & Akuntabilitas Desa (Pencegahan Penyalahgunaan Wewenang)",
"deskripsi": "<p>Video ini membahas bagaimana integrasi digital seperti Simpades, Siskeudes, dan program Jaga Desa memperkuat akuntabilitas pemerintahan desa Darmasaba. Program tersebut mendukung transparansi dan pencegahan tindak pidana administrasi maupun penyalahgunaan wewenang karena tata kelola data desa yang baik membantu mencegah korupsi atau kecurangan dalam pengelolaan dana dan layanan desa.</p>",
"deskripsiSingkat": "<p>Integrasi digital meningkatkan transparansi dan mencegah penyalahgunaan wewenang di pemerintahan desa.</p>",
"linkVideo": "https://www.youtube.com/embed/l7NIqjA2b_k"
},
{
"id": "cmh60op850003qq091pvs7rf1",
"judul": "PROFIL DESA DARMASABA",
"deskripsiSingkat": "PROFIL DESA DARMASABA Desa Darmasaba adalah permata di ujung selatan Kecamatan Abiansemal, Kabupaten Badung",
"deskripsi": "<p>PROFIL DESA DARMASABA Desa Darmasaba adalah permata di ujung selatan Kecamatan Abiansemal, Kabupaten Badung, yang dikenal dengan kekayaan budaya, inovasi desa, dan potensi ekonomi yang terus berkembang. Berbatasan langsung dengan Kota Denpasar, Desa Darmasaba memiliki luas wilayah 567 hektar dengan jumlah penduduk 10.141 jiwa (per akhir Desember 2024). Video ini menampilkan potensi dan keunggulan Desa Darmasaba: 🏞️ Keindahan alam dan kawasan persawahan yang dikelola empat subak aktif. 🍽️ Keberhasilan UMKM dan kuliner lokal, termasuk produk unggulan ACK yang merambah pasar nasional. 🌱 Inovasi ketahanan pangan, pengolahan sampah ramah lingkungan (TPS3R Pudak Mesari, CINTA Darmasaba), hingga Graha Sari Boga dengan program makan bergizi gratis. 🎭 Pelestarian seni, budaya, dan tradisi, termasuk maskot Sekar Pudak dan tradisi Ngerebeg. 🏆 Prestasi desa tingkat nasional dan internasional. 📱 Transformasi digital dengan aplikasi Darmasaba Digital App, talkshow Bicara Darmasaba, hingga perpustakaan digital Pustaka Ananta Loka. Dengan motto \\\"Menggali Warisan, Merangkai Inovasi\\\", Desa Darmasaba menghadirkan 13 inovasi unggulan di bidang pemerintahan, kewilayahan, UMKM, dan kemasyarakatan. Desa ini membuktikan bahwa kolaborasi masyarakat dan pemerintah mampu mewujudkan desa yang mandiri, berdaya saing, dan berkelanjutan. 🎥 Produksi: Tim Media Kreatif Desa Darmasaba 🤝 Dukungan: Seluruh Lembaga &amp; Elemen Masyarakat Desa Darmasaba</p>",
"linkVideo": "https://www.youtube.com/watch?v=9eCnhJvdv6A"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478",
"nama": "Layanan Administrasi & Surat-Menyurat"
},
{
"id": "b5af284c-6aa1-4442-935f-869d78eb7ecf",
"nama": "Penanganan Laporan & Pengaduan"
},
{
"id": "56b37d7f-d717-4e33-b05d-ea22b5f7af91",
"nama": "Perlindungan & Pengayoman Masyarakat"
}
]

View File

@@ -0,0 +1,62 @@
[
{
"id": "a50d52f2-e70f-4f29-9133-e294c40d14d3",
"layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478",
"polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d"
},
{
"id": "012454f8-f5d7-41c0-9dce-2754c0356523",
"layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf",
"polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d"
},
{
"id": "50736038-4ba6-47f8-8399-35b73b389f12",
"layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91",
"polsekTerdekatId": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d"
},
{
"id": "e2dc3487-1f62-4f63-9a12-49ac30da3372",
"layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478",
"polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2"
},
{
"id": "47fe1f9c-4072-4203-90f9-3294d1369ac5",
"layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf",
"polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2"
},
{
"id": "2cc1ba81-6b62-42ff-af21-09f8165a2dd0",
"layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91",
"polsekTerdekatId": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2"
},
{
"id": "3ca2ce42-2e7d-4750-87b7-f1f52ed141de",
"layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478",
"polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea"
},
{
"id": "90472bca-cf3d-47ca-92e5-db43c4c7e579",
"layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf",
"polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea"
},
{
"id": "41cfc1fe-a193-446d-b574-64b1124c6f55",
"layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91",
"polsekTerdekatId": "9a3fff54-8854-4929-b9b5-b5b2751011ea"
},
{
"id": "f35443e1-6aca-416d-8c55-00e3f4a0f5f9",
"layananId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478",
"polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf"
},
{
"id": "e09797f6-82e5-4b77-946e-319eee431c8f",
"layananId": "b5af284c-6aa1-4442-935f-869d78eb7ecf",
"polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf"
},
{
"id": "bd9b1f27-cd1b-4e23-b162-3a757745f78a",
"layananId": "56b37d7f-d717-4e33-b05d-ea22b5f7af91",
"polsekTerdekatId": "c2d272e1-737d-44f5-bd85-ae268cb06cbf"
}
]

View File

@@ -0,0 +1,54 @@
[
{
"id": "b92ee048-fb7f-44e9-aa5b-822e6cd0085d",
"nama": "Kantor Polisi Abian Semal",
"jarakKeDesa": "9,6 Km",
"alamat": "ABIAN SEMAL POLICE STAT, JL. Pasar, Blahkiuh, Kec. Abiansemal, Kabupaten Badung, Bali 80352",
"nomorTelepon": "0361813972",
"jamOperasional": "Buka 24 Jam",
"embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d63127.118683990586!2d115.16592643905703!3d-8.553143643198624!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23ceb4f6e5363%3A0xa353662f070f47d8!2sAbian%20Semal%20Police%20Station!5e0!3m2!1sid!2sid!4v1768376981008!5m2!1sid!2sid",
"namaTempatMaps": "Abian Semal Police Station",
"alamatMaps": "ABIAN SEMAL POLICE STAT, JL. Pasar, Blahkiuh, Kec. Abiansemal, Kabupaten Badung, Bali 80352",
"linkPetunjukArah": "https://maps.app.goo.gl/GhHVNQqffNrXSMFX7",
"layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478"
},
{
"id": "de769c40-10d4-4fbc-a5ef-12f2ce53a0a2",
"nama": "Polres Badung",
"jarakKeDesa": "5,8 Km",
"alamat": "Jl. Kebo Iwa No.1, Mengwitani, Kec. Mengwi, Kabupaten Badung, Bali 80351",
"nomorTelepon": "0361829949",
"jamOperasional": "Buka 24 Jam",
"embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d15780.907469872707!2d115.17829950197888!3d-8.574172113520685!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd238cade1488c3%3A0x918ba5cac3ef00b7!2sPolres%20Badung!5e0!3m2!1sid!2sid!4v1768377100998!5m2!1sid!2sid",
"namaTempatMaps": "Polres Badung",
"alamatMaps": "Jl. Kebo Iwa No.1, Mengwitani, Kec. Mengwi, Kabupaten Badung, Bali 80351",
"linkPetunjukArah": "https://maps.app.goo.gl/7yQQod4PFhpqqe7Z8",
"layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478"
},
{
"id": "9a3fff54-8854-4929-b9b5-b5b2751011ea",
"nama": "Polsek Mengwi",
"jarakKeDesa": "9,7 Km",
"alamat": "Jl. I Gusti Ngurah Rai No.110, Werdi Bhuwana, Kec. Mengwi, Kabupaten Badung, Bali 80351",
"nomorTelepon": "0361411270",
"jamOperasional": "Buka 24 Jam",
"embedMapUrl": "https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d63126.100310757916!2d115.1716545!3d-8.5592871!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23b9ddc3980af%3A0xa1f4f195483537b!2sPolsek%20Mengwi!5e0!3m2!1sid!2sid!4v1768377317955!5m2!1sid!2sid",
"namaTempatMaps": "Polsek Mengwi",
"alamatMaps": "Jl. I Gusti Ngurah Rai No.110, Werdi Bhuwana, Kec. Mengwi, Kabupaten Badung, Bali 80351",
"linkPetunjukArah": "https://maps.app.goo.gl/cJD44NSUdpA7Ly2m6",
"layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478"
},
{
"id": "c2d272e1-737d-44f5-bd85-ae268cb06cbf",
"nama": "Pos Polisi Ahmad Yani",
"jarakKeDesa": "7 Km",
"alamat": "Jl. Ahmad Yani Utara No.5, Peguyangan, Kec. Denpasar Utara, Kota Denpasar, Bali 80239",
"nomorTelepon": "-",
"jamOperasional": "-",
"embedMapUrl": "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d31558.650325984465!2d115.18791122296605!3d-8.612190901728288!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2dd23f635f1186b1%3A0xcfdde508d897fb04!2sPos%20Polisi%20Simpang%20Ahmad%20Yani!5e0!3m2!1sid!2sid!4v1768377470154!5m2!1sid!2sid",
"namaTempatMaps": "Pos Polisi Simpang Ahmad Yani",
"alamatMaps": "Jl. Ahmad Yani Utara No.5, Peguyangan, Kec. Denpasar Utara, Kota Denpasar, Bali 80239",
"linkPetunjukArah": "https://maps.app.goo.gl/D8HGs4mSAQqJm9KRA",
"layananPolsekId": "7ded635b-bf4a-4c1b-b6fa-2f13f4dfc478"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": "cmh48wo9c0006qq09txnxusql",
"judul": "Keamanan Rumah",
"deskripsi": "<p><ul><li><p>Pastikan pintu dan jendela selalu terkunci saat meninggalkan rumah</p></li><li><p>Pasang lampu penerangan di halaman dan area sekitar rumah untuk mencegah tindak kejahatan.</p></li><li><p>Jangan mudah memberikan akses masuk ke orang yang tidak dikenal.</p></li></ul></p>",
"imageId": "cmkccs50d0000vn2mfuk0d9dw"
},
{
"id": "cmh48wo9c1117rr10txnxusql",
"judul": "Keamanan Lingkungan Tanggungjawab Bersama",
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan sosialisasi dan pembinaan tentang keamanan dan ketertiban lingkungan kepada warga Perumahan Darmasaba Permai. Warga diajak berperan aktif dalam menjaga keamanan lingkungan serta mendukung penyediaan lampu penerangan jalan untuk mencegah tindak kriminal dan kecelakaan. Bhabinkamtibmas dan Babinsa turut memberikan materi keamanan dan ketertiban kepada warga, menekankan pentingnya partisipasi masyarakat dalam menjaga keamanan desa.</p>",
"imageId": "cmkccyh7t0003vn2mjdrqtuu0"
}
]

View File

@@ -0,0 +1,37 @@
[
{
"id": "cmkax3ptc000tvn6ytq1lpb2z",
"name": "Diare dan Kolera",
"deskripsiSingkat": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p>",
"deskripsiLengkap": "<p>Apa itu Diare dan Kolera penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan Diare dan Kolera yang efektif untuk melindungi keluarga anda.</p><ul><li><p>Penyebab: Bakteri Vibrio cholerae (Kolera) atau Escherichia coli (diare) akibat makanan/minuman yang terkontaminasi.</p></li><li><p>Gejala: Buang air besar cair terus-menerus, dehidrasi, dan lemas. Pencegahan: Menjaga kebersihan makanan dan air, serta mencuci tangan dengan sabun.</p></li></ul>",
"imageId": "cmkax3o8g000rvn6ygqpmo1nb"
},
{
"id": "cmkax5urc000wvn6yxfw0970w",
"name": "TBC (Tuberkulosis)",
"deskripsiSingkat": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p>",
"deskripsiLengkap": "<p>Apa itu TBC penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan TBC yang efektif untuk melindungi keluarga anda.</p><p>Penyebab: Bakteri Mycobacterium tuberculosis yang menyebar melalui udara.</p><p>Gejala: Batuk lebih dari 2 minggu, berkeringat di malam hari, dan berat badan turun.</p><p>Pencegahan: Vaksin BCG, pola hidup sehat, dan pengobatan bagi penderita agar tidak menular.</p>",
"imageId": "cmkax5ukz000uvn6yho3aj2nf"
},
{
"id": "cmkax72s7000zvn6yz3nmvrry",
"name": "Demam Berdarah Dengue (DBD)",
"deskripsiSingkat": "<p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p>",
"deskripsiLengkap": "<p>Apa itu DBD penyebab, gejala dan cara penanganannya?</p><p>Yuk Kenali gelaja dan cara penanganan DBD yang efektif untuk melindungi keluarga anda selama musim hujan.</p><p>Penyebab: Virus dengue yang ditularkan oleh nyamuk Aedes aegypti.</p><p>Gejala: Demam tinggi, nyeri sendi, ruam kulit, dan pendarahan ringan.</p><p>Pencegahan: Menguras tempat air, menutup wadah air, fogging, dan menggunakan lotion anti-nyamuk.</p>",
"imageId": "cmkax72nw000xvn6ymcuvlzom"
},
{
"id": "cmkbyny4f0002vn67kmjmjrpl",
"name": "Fogging sebagai Pencegah DBD di Br. Umahanyar Desa Darmasaba",
"deskripsiSingkat": "<p>Pemerintah Desa Darmasaba melaksanakan fogging di wilayah Br. Umahanyar sebagai upaya pencegahan DBD di Desa Darmasaba.</p>",
"deskripsiLengkap": "<p>Pemerintah Desa Darmasaba melaksanakan fogging (pengasapan) di wilayah Br. Umahanyar Desa Darmasaba Kecamatan Abiansemal Kabupaten Badung dari tanggal 12 sampai dengan 13 April 2023.</p><p>Fogging ini merupakan salah satu metode yang dilakukan oleh Pemdes Darmasaba dalam pencegahan penyakit Demam Berdarah Dengue (DBD) dengan menargetkan nyamuk Aedes aegypti sebagai vektor penyebabnya.</p>",
"imageId": "cmkbynxxo0000vn67wi2nsyl3"
},
{
"id": "cmkbyr3rx0005vn674uhycsxc",
"name": "Gerakan Serentak Penyemprotan Pencegahan PMK di Desa Darmasaba",
"deskripsiSingkat": "<p>Penyemprotan serentak dilakukan di Desa Darmasaba untuk mencegah Penyakit Mulut dan Kaki (PMK) pada hewan ternak.</p>",
"deskripsiLengkap": "<p>Setelah dilakukan vaksinasi Penyakit Mulut dan Kaki (PMK) pada hewan ternak yaitu sapi di wilayah Desa Darmasaba, Pemerintah Desa Darmasaba melaksanakan gerakan serentak penyemprotan pencegahan PMK pada hari Rabu (20/7/2022) di seputaran wilayah Desa Darmasaba.</p><p>Upaya ini dilakukan sebagai bentuk pencegahan terhadap penyebaran PMK dan menjaga kesehatan hewan ternak di desa.</p>",
"imageId": "cmkbyr3mk0003vn673xrqv8xv"
}
]

View File

@@ -0,0 +1,30 @@
[
{
"id": "cmkax1vks000qvn6yyxuvfsi8",
"name": "Puskesmas Pembantu Darmasaba",
"deskripsi": "<p>Puskesmas Pembantu Darmasaba merupakan fasilitas kesehatan tingkat pertama yang berada di Desa Darmasaba, melayani berbagai layanan kesehatan masyarakat termasuk pemeriksaan umum dan imunisasi.</p>",
"imageId": "cmkb6488i001fvn6ylkddch1j",
"whatsapp": "089647037430"
},
{
"id": "cmkawzrvg000nvn6ywyx529em",
"name": "UPTD Puskesmas Abiansemal III (melayani Darmasaba)",
"deskripsi": "<p>Puskesmas Abiansemal III adalah fasilitas kesehatan utama di kecamatan Abiansemal yang melayani wilayah Desa Darmasaba dan sekitarnya. Puskesmas ini memiliki layanan 24 jam serta pelayanan darurat kesehatan dasar.</p>",
"imageId": "cmkb681og001gvn6ykb5uasln",
"whatsapp": "03618463263"
},
{
"id": "cmkawy5in000kvn6yza82pkkg",
"name": "UPTD Puskesmas Abiansemal I",
"deskripsi": "<p>Puskesmas Abiansemal I melayani masyarakat di wilayah kecamatan Abiansemal, termasuk pelayanan kesehatan darurat dan program kesehatan masyarakat.</p>",
"imageId": "cmkb6brrf0000vn14u8c7wnox",
"whatsapp": "087858367111"
},
{
"id": "cmkb6ehu20003vn14ca4xr057",
"name": "Kantor Desa Darmasaba (Kontak Informasi Kesehatan)",
"deskripsi": "<p>Kantor Pemerintahan Desa Darmasaba dapat menjadi saluran kontak awal untuk rujukan layanan kesehatan darurat atau informasi lebih lanjut mengenai fasilitas kesehatan di wilayah desa.</p>",
"imageId": "cmkb6ehpi0001vn14hjp4tdye",
"whatsapp": "081239580000"
}
]

View File

@@ -0,0 +1,26 @@
[
{
"id": "cmkawso7y000evn6ygob15cqb",
"name": "Rembug Stunting di Desa Darmasaba",
"deskripsi": "<p>Pemerintah Desa Darmasaba melaksanakan kegiatan rembug stunting dengan melibatkan bidan desa, kader posyandu, dan tokoh masyarakat. Tujuan kegiatan ini adalah untuk memperkuat upaya pencegahan kekerdilan (stunting) melalui koordinasi layanan kesehatan, edukasi gizi, serta percepatan penanganan gizi buruk di lingkungan desa sebagai bagian dari respons terhadap kondisi kesehatan yang mendesak.</p>",
"imageId": "cmkayz2h8001cvn6yrb7uptjs"
},
{
"id": "cmkawq3ef000bvn6y387vub0y",
"name": "Posko Kesehatan Darurat dan Bencana",
"deskripsi": "<p>Posko Kesehatan Darurat dan Bencana Desa Darmasaba dibentuk sebagai pusat koordinasi dan pertolongan bagi warga yang terdampak situasi darurat seperti banjir, tanah longsor, atau wabah penyakit. Posko ini dilengkapi dengan tenaga medis, obat-obatan dasar, serta dukungan logistik untuk memastikan penanganan cepat dan tepat sasaran. Kegiatan ini juga melibatkan kader kesehatan desa dan karang taruna sebagai relawan lapangan.</p>",
"imageId": "cmkawq38m0009vn6yi7evbhap"
},
{
"id": "cmkawso7y000evn6ygob14bpa",
"name": "Layanan Ambulans Desa Darmasaba",
"deskripsi": "<p>Layanan Ambulans Desa Darmasaba disiapkan untuk membantu masyarakat yang membutuhkan transportasi medis darurat ke fasilitas kesehatan terdekat. Layanan ini beroperasi 24 jam dan dapat dihubungi melalui nomor darurat desa. Tim ambulans terdiri dari relawan terlatih dan tenaga medis yang siap memberikan pertolongan pertama di lokasi kejadian sebelum dirujuk ke rumah sakit atau puskesmas.</p>",
"imageId": "cmkawso29000cvn6y879ahra0"
},
{
"id": "cmkawu7te000hvn6yh3pdnv4w",
"name": "Penanganan Darurat Sosial & Kesehatan Desa Darmasaba",
"deskripsi": "<p>Program Penanganan Darurat Sosial &amp; Kesehatan Desa Darmasaba bertujuan memberikan respon cepat terhadap situasi darurat seperti warga sakit mendadak, kecelakaan, bencana alam, maupun kondisi sosial yang membutuhkan bantuan segera. Tim Siaga Desa Darmasaba berkoordinasi dengan Puskesmas Abiansemal dan BPBD untuk memastikan penanganan yang cepat, tepat, dan manusiawi. Program ini juga mencakup layanan ambulans desa, posko kesehatan darurat, serta bantuan logistik bagi warga terdampak.</p>",
"imageId": "cmkawu7qj000fvn6yubhimyiv"
}
]

View File

@@ -0,0 +1,51 @@
[
{
"id": "cmkawkji50002vn6yzyrlqhh1",
"name": "Gerakan Kulkul PKK dan Posyandu Desa Darmasaba",
"deskripsiSingkat": "<p>Kegiatan bersama PKK dan Posyandu untuk meningkatkan pelayanan kesehatan masyarakat.</p>",
"deskripsi": "<p>Pada hari Minggu, 11 Januari 2025, Pemerintah Desa Darmasaba melalui TP PKK dan TP Posyandu melaksanakan kegiatan Gerakan Kulkul PKK dan Posyandu yang berlangsung serentak di seluruh wilayah Desa Darmasaba untuk memperkuat pelayanan kesehatan dasar dan peningkatan partisipasi masyarakat dalam program Posyandu.</p>",
"imageId": "cmkay1e590010vn6y24pgaa1r"
},
{
"id": "cmkawmlg40005vn6yja2xiev0",
"name": "Pendampingan Kunjungan Rumah oleh Puskesmas Abiansemal 3",
"deskripsiSingkat": "<p>Pendataan kesehatan penyandang disabilitas lewat kunjungan rumah di Desa Darmasaba.</p>",
"deskripsi": "<p>Pemerintah Desa Darmasaba bersama Kelian Banjar Dinas dan kader kesehatan mendampingi kegiatan kunjungan rumah yang dilaksanakan oleh Puskesmas Abiansemal 3 pada 21 Juli 2025, difokuskan pada pendataan dan pemantauan kondisi kesehatan penyandang disabilitas di Banjar Bersih, Desa Darmasaba.</p>",
"imageId": "cmkay6hob0011vn6ybjwejcej"
},
{
"id": "cmkawnr9k0008vn6ymwv0foiv",
"name": "Kegiatan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali di Desa Darmasaba",
"deskripsiSingkat": "<p>Aksi sosial TP Posyandu Bali untuk memperkuat pelayanan posyandu di desa.</p>",
"deskripsi": "<p>Pada 10 Desember 2025, Desa Darmasaba menjadi lokasi pelaksanaan Aksi Sosial Tim Penggerak Posyandu Provinsi Bali yang bertujuan memperkuat pelayanan Posyandu serta meningkatkan kesejahteraan masyarakat, khususnya keluarga dan balita.</p>",
"imageId": "cmkay8vmd0012vn6ylsk2vzfo"
},
{
"id": "cmkawnr9k0008vn6ymwv0dpjw",
"name": "Inovasi BAJRA dalam Penanggulangan Rabies",
"deskripsiSingkat": "<p>Program BAJRA untuk penanggulangan rabies di Desa Darmasaba.</p>",
"deskripsi": "<p>Desa Darmasaba mengembangkan inovasi BAJRA (Bersama Jaga Rabies), sebuah program berbasis komunitas untuk penanggulangan rabies yang mengintegrasikan pelaporan cepat masyarakat, edukasi berkelanjutan dan koordinasi lintas sektor antara kesehatan hewan, manusia, dan pemerintahan desa.</p>",
"imageId": "cmkayd8o90013vn6ye7n8805q"
},
{
"id": "cmkawnr9k0008vn6ymwv0eqkx",
"name": "Posyandu Pudak Amara Berkompetisi",
"deskripsiSingkat": "<p>Partisipasi Posyandu Pudak Amara dalam lomba prestasi Posyandu tingkat provinsi.</p>",
"deskripsi": "<p>Kader Posyandu Pudak Amara Br. Cabe mendapat pendampingan dari Perbekel Darmasaba, Dinas Kesehatan Kab. Badung, Puskesmas Abiansemal III, dan Pustu Desa Darmasaba dalam ajang lomba kader dan Posyandu berprestasi tingkat Provinsi Bali tahun 2025.</p>",
"imageId": "cmkayi0x90016vn6ykddxqyq3"
},
{
"id": "cmkawnr9k0008vn6ymwv1frly",
"name": "Outbound Kader Posyandu Darmasaba",
"deskripsiSingkat": "<p>Program pembinaan dan pengembangan kapasitas kader Posyandu.</p>",
"deskripsi": "<p>Pemdes Darmasaba melaksanakan kegiatan Outbound Posyandu untuk meningkatkan kapasitas dan wawasan Kader Posyandu se-Desa Darmasaba sebagai bagian dari upaya peningkatan kualitas pelayanan kesehatan dasar di masyarakat.</p>",
"imageId": "cmkaykipf0019vn6yknjno3k1"
},
{
"id": "cmkdu8ki10004vn4lpbxm2zqo",
"name": "PEMBANGUNAN JAMBAN BAGI MASYARAKAT",
"deskripsiSingkat": "<p>Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p>",
"deskripsi": "<p>Desa Darmasaba sebagai desa yang berkomitmen selalu selaras dengan pembangunan Pemerintah Kabupaten Badung pada tahun anggaran 2023 ini turut ambil bagian dalam menyukseskan program Bupati Badung I Nyoman Giri Prasta, S.Sos dalam bidang kesehatan sanitasi masyarakat. Program pengadaan jamban bagi Masyarakat ini diharapkan menjadi stimulus agar masyarakat peduli terhadap lingkungan sehat sehingga Badung Open Defection Free atau terbebas dari buang air besar di tempat terbuka dapat terwujud.</p><p style=\"text-align: justify\">Pemberian bantuan jamban ini dilaksanakan di 11 banjar dengan menyasar 22 keluarga yang memang belum memiliki jamban yang sumber dananya sepenuhnya dari APBDes Darmasaba T. A. 2023. Pembangunan Jamban bagi Masyarakat ini juga menjadi bukti komitmen Pemerintah Desa Darmasaba dalam melaksanakan salah satu visi mewujudkan masyarakat yang sejahtera dan berbudaya untuk menjaga lingkungan yang bersih dan sehat.</p>",
"imageId": "cmkdu8kb20002vn4lihwo4k86"
}
]

View File

@@ -0,0 +1,14 @@
[
{
"id": "cmkao2zwx0008vntzmvqdsdzo",
"workDays": "09:00",
"weekDays": "17:00",
"holiday": "08:00 - 16:00"
},
{
"id": "cmkao2zwx0008vntzmvqdseal",
"workDays": "08:00",
"weekDays": "12:00",
"holiday": ""
}
]

View File

@@ -0,0 +1,16 @@
[
{
"id": "cmkao2zxc0009vntz00kev051",
"kontakPuskesmas": "(0361) 8463263",
"email": "puskesmas@gmail.com",
"facebook": "puskesmas@gmail.com",
"kontakUGD": "(0361) 8463263"
},
{
"id": "cmkao2zxc0009vntz00kev162",
"kontakPuskesmas": "",
"email": "",
"facebook": "",
"kontakUGD": ""
}
]

View File

@@ -0,0 +1,18 @@
[
{
"id": "cmkao2zxk000bvntzbavkbg6p",
"name": "Puskesmas Abiansemal III",
"alamat": "Jl. Ratna, Sibang Kaja, Abiansemal, Badung, Bali 80352",
"jamId": "cmkao2zwx0008vntzmvqdsdzo",
"imageId": "cmkao2zm90007vntzxqkjy5mt",
"kontakId": "cmkao2zxc0009vntz00kev051"
},
{
"id": "cmkao2zxk000bvntzbavkbh7q",
"name": "Puskesmas Pembantu Darmasaba",
"alamat": "Desa Darmasaba, Kecamatan Abiansemal, Kabupaten Badung, Bali",
"jamId": "cmkao2zwx0008vntzmvqdseal",
"imageId": "cmkatoru10000vny38y0wxd6s",
"kontakId": "cmkao2zxc0009vntz00kev162"
}
]

View File

@@ -607,7 +607,7 @@ model Berita {
id String @id @default(cuid())
judul String
deskripsi String
image FileStorage? @relation(fields: [imageId], references: [id])
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
content String @db.Text
createdAt DateTime @default(now())
@@ -1113,17 +1113,17 @@ model DoctorSign {
// ========================================= POSYANDU ========================================= //
model Posyandu {
id String @id @default(cuid())
id String @id @default(cuid())
name String
nomor String
deskripsi String
jadwalPelayanan String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PUSKESMAS ========================================= //
@@ -1133,8 +1133,8 @@ model Puskesmas {
alamat String
jam JamOperasional @relation(fields: [jamId], references: [id])
jamId String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
kontak KontakPuskesmas @relation(fields: [kontakId], references: [id])
kontakId String
createdAt DateTime @default(now())
@@ -1170,57 +1170,57 @@ model KontakPuskesmas {
// ========================================= PROGRAM KESSEHATAN ========================================= //
model ProgramKesehatan {
id String @id @default(cuid())
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= PENANGANAN DARURAT ========================================= //
model PenangananDarurat {
id String @id @default(cuid())
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KONTAK DARURAT ========================================= //
model KontakDarurat {
id String @id @default(cuid())
id String @id @default(cuid())
name String
deskripsi String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
whatsapp String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= INFO WABAH PENYAKIT ========================================= //
model InfoWabahPenyakit {
id String @id @default(cuid())
id String @id @default(cuid())
name String
deskripsiSingkat String
deskripsiLengkap String
image FileStorage @relation(fields: [imageId], references: [id])
imageId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
image FileStorage? @relation(fields: [imageId], references: [id])
imageId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= MENU KEAMANAN ========================================= //
@@ -1239,7 +1239,7 @@ model KeamananLingkungan {
// ========================================= POLSEK TERDEKAT ========================================= //
model PolsekTerdekat {
id String @id @default(uuid())
id String @id @default(uuid())
nama String
jarakKeDesa String
alamat String
@@ -1249,22 +1249,36 @@ model PolsekTerdekat {
namaTempatMaps String
alamatMaps String
linkPetunjukArah String
layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id])
layananPolsek LayananPolsek @relation(fields: [layananPolsekId], references: [id])
layananPolsekId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
LayananToPolsek LayananToPolsek[]
}
model LayananPolsek {
id String @id @default(uuid())
nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
PolsekTerdekat PolsekTerdekat[]
id String @id @default(uuid())
nama String // contoh: "Pelayanan SKCK", "Laporan Kriminal"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
isActive Boolean @default(true)
PolsekTerdekat PolsekTerdekat[]
LayananToPolsek LayananToPolsek[]
}
model LayananToPolsek {
id String @id @default(uuid())
layanan LayananPolsek @relation(fields: [layananId], references: [id])
layananId String
polsekTerdekat PolsekTerdekat @relation(fields: [polsekTerdekatId], references: [id])
polsekTerdekatId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}
// ========================================= KONTAK DARURAT ========================================= //
@@ -1377,6 +1391,7 @@ model PasarDesa {
rating Float
alamatUsaha String
kontak String
deskripsi String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())

File diff suppressed because it is too large Load Diff

View File

@@ -45,7 +45,7 @@ export default async function seedAssets() {
// 1. Download zip
const url =
"https://cld-dkr-makuro-seafile.wibudev.com/f/bc437c719af64c0bb7f2/?dl=1";
"https://cld-dkr-makuro-seafile.wibudev.com/f/88293b915cf34b939819/?dl=1";
const res = await fetchWithRetry(url, 3, 20000);
// Validasi content-type

View File

@@ -27,8 +27,25 @@ import {
IconFiretruck,
IconBuilding,
IconAlertTriangle,
// ===== Tambahan =====
IconLifebuoy,
IconRun,
IconShield,
IconPhoneCall,
IconFirstAidKit,
IconStethoscope,
IconBuildingCommunity,
IconFileText,
IconInfoCircle,
IconMessageReport,
IconUsers,
IconQuestionMark,
} from '@tabler/icons-react'
/* =======================
Icon Keys (DB Safe)
======================= */
export type IconKey =
| 'ekowisata'
| 'kompetisi'
@@ -50,14 +67,33 @@ export type IconKey =
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
// ===== Keamanan & Darurat =====
| 'polisi'
| 'ambulans'
| 'pemadam'
| 'rumahSakit'
| 'bangunan'
| 'darurat'
| 'sar'
| 'evakuasi'
| 'keamanan'
| 'teleponDarurat'
// ===== Kesehatan =====
| 'rumahSakit'
| 'puskesmas'
| 'klinik'
// ===== Pemerintahan =====
| 'bangunan'
| 'kantorDesa'
| 'administrasi'
| 'informasi'
| 'pengaduan'
| 'layananPublik'
/* =======================
Icon Map
======================= */
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
@@ -79,22 +115,45 @@ const iconMap: Record<IconKey, React.FC<any>> = {
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
// ===== Keamanan & Darurat =====
polisi: IconShieldFilled,
ambulans: IconAmbulance,
pemadam: IconFiretruck,
darurat: IconAlertTriangle,
sar: IconLifebuoy,
evakuasi: IconRun,
keamanan: IconShield,
teleponDarurat: IconPhoneCall,
// ===== Kesehatan =====
rumahSakit: IconHospital,
puskesmas: IconFirstAidKit,
klinik: IconStethoscope,
// ===== Pemerintahan =====
bangunan: IconBuilding,
darurat: IconAlertTriangle
kantorDesa: IconBuildingCommunity,
administrasi: IconFileText,
informasi: IconInfoCircle,
pengaduan: IconMessageReport,
layananPublik: IconUsers,
}
/* =======================
Icon Mapper Component
======================= */
type Props = {
name: IconKey
size?: number
color?: string
}
export const IconMapper: React.FC<Props> = ({ name, size = 24, color }) => {
const IconComponent = iconMap[name]
if (!IconComponent) return null
export const IconMapper: React.FC<Props> = ({
name,
size = 24,
color,
}) => {
const IconComponent = iconMap[name] ?? IconQuestionMark
return <IconComponent size={size} color={color} />
}

View File

@@ -6,27 +6,38 @@ import {
IconAlertTriangle,
IconAmbulance,
IconBuilding,
IconBuildingCommunity,
IconCash,
IconChartLine,
IconChristmasTreeFilled,
IconClipboardTextFilled,
IconDroplet,
IconFileText,
IconFiretruck,
IconFirstAidKit,
IconHome,
IconHomeEco,
IconHospital,
IconInfoCircle,
IconLeaf,
IconLifebuoy,
IconMessageReport,
IconPhoneCall,
IconRecycle,
IconRun,
IconScale,
IconSchool,
IconShield,
IconShieldFilled,
IconShoppingCart,
IconStethoscope,
IconTent,
IconTrashFilled,
IconTree,
IconTrendingUp,
IconTrophy,
IconTruckFilled,
IconUsers,
} from '@tabler/icons-react';
import { useEffect, useState } from 'react';
@@ -51,15 +62,32 @@ const iconMap = {
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
// ===== Keamanan & Darurat =====
polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: { label: 'Bangunan', icon: IconBuilding },
pemadam: { label: 'Pemadam Kebakaran', icon: IconFiretruck },
darurat: { label: 'Darurat', icon: IconAlertTriangle },
sar: { label: 'SAR / Basarnas', icon: IconLifebuoy },
evakuasi: { label: 'Evakuasi', icon: IconRun },
keamanan: { label: 'Keamanan', icon: IconShield },
teleponDarurat: { label: 'Telepon Darurat', icon: IconPhoneCall },
// ===== Kesehatan =====
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
puskesmas: { label: 'Puskesmas', icon: IconFirstAidKit },
klinik: { label: 'Klinik', icon: IconStethoscope },
// ===== Pemerintahan & Fasilitas =====
bangunan: { label: 'Bangunan', icon: IconBuilding },
kantorDesa: { label: 'Kantor Desa', icon: IconBuildingCommunity },
administrasi: { label: 'Administrasi', icon: IconFileText },
informasi: { label: 'Informasi', icon: IconInfoCircle },
pengaduan: { label: 'Pengaduan', icon: IconMessageReport },
layananPublik: { label: 'Layanan Publik', icon: IconUsers },
};
type IconKey = keyof typeof iconMap;
const iconList = Object.entries(iconMap).map(([value, data]) => ({

View File

@@ -26,6 +26,17 @@ import {
IconTruckFilled,
IconBuilding,
IconAlertTriangle,
IconBuildingCommunity,
IconFileText,
IconFirstAidKit,
IconInfoCircle,
IconLifebuoy,
IconMessageReport,
IconPhoneCall,
IconRun,
IconShield,
IconStethoscope,
IconUsers,
} from '@tabler/icons-react';
const iconMap = {
@@ -49,12 +60,29 @@ const iconMap = {
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
// ===== Keamanan & Darurat =====
polisi: { label: 'Polisi', icon: IconShieldFilled },
ambulans: { label: 'Ambulans', icon: IconAmbulance },
pemadam: { label: 'Pemadam', icon: IconFiretruck },
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
bangunan: { label: 'Bangunan', icon: IconBuilding },
pemadam: { label: 'Pemadam Kebakaran', icon: IconFiretruck },
darurat: { label: 'Darurat', icon: IconAlertTriangle },
sar: { label: 'SAR / Basarnas', icon: IconLifebuoy },
evakuasi: { label: 'Evakuasi', icon: IconRun },
keamanan: { label: 'Keamanan', icon: IconShield },
teleponDarurat: { label: 'Telepon Darurat', icon: IconPhoneCall },
// ===== Kesehatan =====
rumahSakit: { label: 'Rumah Sakit', icon: IconHospital },
puskesmas: { label: 'Puskesmas', icon: IconFirstAidKit },
klinik: { label: 'Klinik', icon: IconStethoscope },
// ===== Pemerintahan & Fasilitas =====
bangunan: { label: 'Bangunan', icon: IconBuilding },
kantorDesa: { label: 'Kantor Desa', icon: IconBuildingCommunity },
administrasi: { label: 'Administrasi', icon: IconFileText },
informasi: { label: 'Informasi', icon: IconInfoCircle },
pengaduan: { label: 'Pengaduan', icon: IconMessageReport },
layananPublik: { label: 'Layanan Publik', icon: IconUsers },
};
export type IconKey = keyof typeof iconMap;

View File

@@ -13,6 +13,7 @@ const templatePasarDesaForm = z.object({
rating: z.number().min(1, "Rating minimal 1"),
kategoriId: z.array(z.string()).min(1, "Minimal pilih satu kategori"),
kontak: z.string().min(1, "Kontak wajib diisi"),
deskripsi: z.string().min(1, "Deskripsi wajib diisi"),
});
const defaultPasarDesaForm = {
@@ -23,6 +24,7 @@ const defaultPasarDesaForm = {
rating: 0,
kategoriId: [] as string[],
kontak: "",
deskripsi: ""
};
const pasarDesa = proxy({
@@ -191,6 +193,7 @@ const pasarDesa = proxy({
rating: data.rating,
kategoriId: data.kategoriId,
kontak: data.kontak,
deskripsi: data.deskripsi
};
return data;
} else {
@@ -229,6 +232,7 @@ const pasarDesa = proxy({
rating: this.form.rating,
kategoriId: this.form.kategoriId,
kontak: this.form.kontak,
deskripsi: this.form.deskripsi
}),
});
if (!response.ok) {

View File

@@ -15,7 +15,7 @@ const templateForm = z.object({
namaTempatMaps: z.string().min(1, "Nama Tempat Maps minimal 1 karakter"),
alamatMaps: z.string().min(1, "Alamat Maps minimal 1 karakter"),
linkPetunjukArah: z.string().min(1, "Link Petunjuk Arah minimal 1 karakter"),
layananPolsekId: z.string().min(1, "Layanan Polsek Id minimal 1 karakter"),
layananPolsekId: z.array(z.string()).min(1, "Pilih minimal 1 layanan polsek"),
});
const defaultForm = {
@@ -28,7 +28,7 @@ const defaultForm = {
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
layananPolsekId: [] as string[],
};
const polsekTerdekatState = proxy({
@@ -66,6 +66,11 @@ const polsekTerdekatState = proxy({
| Prisma.PolsekTerdekatGetPayload<{
include: {
layananPolsek: true;
LayananToPolsek: {
include: {
layanan: true;
}
}
};
}>[]
| null,
@@ -104,7 +109,14 @@ const polsekTerdekatState = proxy({
},
findUnique: {
data: null as Prisma.PolsekTerdekatGetPayload<{
include: { layananPolsek: true };
include: {
layananPolsek: true;
LayananToPolsek: {
include: {
layanan: true;
}
}
};
}> | null,
async load(id: string) {
try {
@@ -117,7 +129,7 @@ const polsekTerdekatState = proxy({
polsekTerdekatState.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
console.error("Gagal fetch detail polsek terdekat:", error);
polsekTerdekatState.findUnique.data = null;
}
},
@@ -273,10 +285,13 @@ const polsekTerdekatState = proxy({
};
}> | null,
loading: false,
load: async () => { // Changed to arrow function
load: async () => {
// Changed to arrow function
polsekTerdekatState.findFirst.loading = true;
try {
const res = await ApiFetch.api.keamanan.polsekterdekat["find-first"].get();
const res = await ApiFetch.api.keamanan.polsekterdekat[
"find-first"
].get();
if (res.status === 200 && res.data?.success) {
polsekTerdekatState.findFirst.data = res.data.data || null;
} else {
@@ -287,8 +302,284 @@ const polsekTerdekatState = proxy({
} finally {
polsekTerdekatState.findFirst.loading = false;
}
}
}
},
},
});
export default polsekTerdekatState;
const templateFormLayananPolsek = z.object({
nama: z.string().min(1, "Nama harus diisi"),
});
const defaultFormLayananPolsek = {
nama: "",
};
const layananPolsek = proxy({
create: {
form: { ...defaultFormLayananPolsek },
loading: false,
async create() {
const cek = templateFormLayananPolsek.safeParse(
layananPolsek.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
layananPolsek.create.loading = true;
const res = await ApiFetch.api.keamanan["layananpolsek"][
"create"
].post(layananPolsek.create.form);
if (res.status === 200) {
layananPolsek.findManyAll.load();
return toast.success("Data Kategori Berita Berhasil Dibuat");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
layananPolsek.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.LayananPolsekGetPayload<{
omit: {
isActive: true;
};
}>[]
| null,
page: 1,
totalPages: 1,
loading: false,
search: "",
load: async (page = 1, limit = 10, search = "") => {
layananPolsek.findMany.loading = true; // ✅ Akses langsung via nama path
layananPolsek.findMany.page = page;
layananPolsek.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.keamanan["layananpolsek"]["findMany"].get({ query });
if (res.status === 200 && res.data?.success) {
layananPolsek.findMany.data = res.data.data ?? [];
layananPolsek.findMany.totalPages = res.data.totalPages ?? 1;
} else {
layananPolsek.findMany.data = [];
layananPolsek.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch layanan polsek paginated:", err);
layananPolsek.findMany.data = [];
layananPolsek.findMany.totalPages = 1;
} finally {
layananPolsek.findMany.loading = false;
}
},
},
findManyAll: {
data: [] as Prisma.LayananPolsekGetPayload<{ omit: { isActive: true } }>[],
loading: false,
async load() {
this.loading = true;
try {
const res = await ApiFetch.api.keamanan["layananpolsek"][
"findManyAll"
].get();
if (res.status === 200 && res.data?.success) {
this.data = (res.data.data ?? []).map((item: any) => ({
id: String(item.id),
nama: String(item.nama || ""),
createdAt: item.createdAt ? new Date(item.createdAt) : new Date(),
updatedAt: item.updatedAt ? new Date(item.updatedAt) : new Date(),
deletedAt: item.deletedAt ? new Date(item.deletedAt) : null,
}));
} else {
this.data = [];
}
} catch (error) {
console.error("Gagal fetch layanan polsek:", error);
this.data = [];
} finally {
this.loading = false;
}
},
},
findUnique: {
data: null as Prisma.LayananPolsekGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(`/api/keamanan/layananpolsek/${id}`);
if (res.ok) {
const data = await res.json();
layananPolsek.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
layananPolsek.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
layananPolsek.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
layananPolsek.delete.loading = true;
const response = await fetch(`/api/keamanan/layananpolsek/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Data layanan polsek berhasil dihapus"
);
await layananPolsek.findManyAll.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus Data layanan polsek");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus Data layanan polsek");
} finally {
layananPolsek.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultFormLayananPolsek },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(`/api/keamanan/layananpolsek/${id}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
nama: data.nama,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading layanan polsek:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateFormLayananPolsek.safeParse(
layananPolsek.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
layananPolsek.update.loading = true;
const response = await fetch(
`/api/keamanan/layananpolsek/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama: this.form.nama,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update data layanan polsek");
await layananPolsek.findManyAll.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update data layanan polsek");
}
} catch (error) {
console.error("Error updating data layanan polsek:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update data layanan polsek"
);
return false;
} finally {
layananPolsek.update.loading = false;
}
},
reset() {
layananPolsek.update.id = "";
layananPolsek.update.form = { ...defaultFormLayananPolsek };
},
},
});
const statePolsekTerdekat = proxy({
polsekTerdekatState,
layananPolsek,
});
export default statePolsekTerdekat;

View File

@@ -102,8 +102,10 @@ function ListKategoriBerita({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} miw={0}>
<TableThead>
<TableTr>
<TableTh w="50%">

View File

@@ -79,8 +79,10 @@ function ListBerita({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} miw={0}>
<TableThead>
<TableTr>
<TableTh w="50%">Judul</TableTh>

View File

@@ -85,8 +85,10 @@ function ListFoto({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh>Judul Foto</TableTh>

View File

@@ -87,66 +87,66 @@ function ListVideo({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh>Judul Video</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" c="dimmed" lh={1.45}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
px="xs"
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} striped verticalSpacing="sm">
<TableThead>
<TableTr>
<TableTh>Judul Video</TableTh>
<TableTh>Tanggal</TableTh>
<TableTh>Deskripsi</TableTh>
<TableTh>Aksi</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="md" fw={500} lh={1.45} truncate="end" lineClamp={1}>
{item.name}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" c="dimmed" lh={1.45}>
{new Date(item.createdAt).toLocaleDateString('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</Text>
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Text fz="sm" lh={1.45} truncate="end" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd style={{ maxWidth: 250 }}>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/desa/gallery/video/${item.id}`)}
fz="sm"
px="xs"
>
<IconDeviceImac size={18} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada video yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}

View File

@@ -76,8 +76,10 @@ function ListAjukanPermohonan({ search }: { search: string }) {
</Title>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} miw={0}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} lh={1.4}>Nama</TableTh>

View File

@@ -99,7 +99,9 @@ function ListSuratKeterangan({ search }: { search: string }) {
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="left">

View File

@@ -85,8 +85,10 @@ function ListPelayananTelunjukSakti({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="left" c="gray.8" w="30%">

View File

@@ -81,8 +81,10 @@ function ListPenghargaan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh w="35%">Nama</TableTh>

View File

@@ -116,8 +116,10 @@ function ListKategoriPengumuman({ search }: { search: string }) {
</Stack>
</Box>
<Box visibleFrom="md">
<Table highlightOnHover striped withRowBorders>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} striped withRowBorders>
<TableThead>
<TableTr>
<TableTh w="60%">

View File

@@ -83,8 +83,10 @@ function ListPengumuman({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="left">

View File

@@ -96,8 +96,10 @@ function ListKategoriPotensi({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover striped withRowBorders miw={700}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} striped withRowBorders miw={700}>
<TableThead>
<TableTr>
<TableTh w="60%">

View File

@@ -90,8 +90,10 @@ function ListPotensi({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={700}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} miw={700}>
<TableThead>
<TableTr>
<TableTh w="20%">

View File

@@ -80,8 +80,10 @@ function ListPerbekelDariMasaKeMasa({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover miw={0}>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} miw={0}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} ta="left" c="dark.9">Nama Perbekel</TableTh>

View File

@@ -98,7 +98,7 @@ function ListAPBDesa({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -114,7 +114,7 @@ function ListBelanja({ search }: { search: string }) {
</Button>
</Group>
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -108,7 +108,7 @@ function ListPembiayaan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
striped

View File

@@ -111,7 +111,7 @@ function ListPendapatan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -104,7 +104,7 @@ function ListPegawaiBumdes({ search }: { search: string }) {
</Group>
{/* Desktop: Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -105,7 +105,7 @@ function ListPosisiOrganisasiBumDes({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -129,7 +129,7 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -112,7 +112,7 @@ function ListJumlahPendudukMiskin({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -152,7 +152,7 @@ function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -133,7 +133,7 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: stri
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -99,7 +99,7 @@ function ListDetailDataPengangguran({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -82,7 +82,7 @@ function ListLowonganKerjaLokal({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -94,7 +94,7 @@ function ListKategoriProduk({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import pasarDesaState from '@/app/admin/(dashboard)/_state/ekonomi/pasar-desa/pasar-desa';
import colors from '@/con/colors';
import ApiFetch from '@/lib/api-fetch';
@@ -33,6 +34,7 @@ type FormData = {
rating: number;
kategoriId: string[];
kontak: string;
deskripsi: string;
};
function EditPasarDesa() {
@@ -51,6 +53,7 @@ function EditPasarDesa() {
rating: 0,
kategoriId: [],
kontak: '',
deskripsi: ''
});
const [originalData, setOriginalData] = useState({
@@ -62,6 +65,7 @@ function EditPasarDesa() {
rating: 0,
kategoriId: [],
kontak: '',
deskripsi: ''
});
// load data awal
@@ -83,6 +87,7 @@ function EditPasarDesa() {
rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '',
deskripsi: data.deskripsi || ''
});
setOriginalData({
nama: data.nama || '',
@@ -93,6 +98,7 @@ function EditPasarDesa() {
rating: data.rating || 0,
kategoriId: data.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: data.kontak || '',
deskripsi: data.deskripsi || ''
});
if (data.image?.link) setPreviewImage(data.image.link);
}
@@ -120,12 +126,13 @@ function EditPasarDesa() {
rating: originalData.rating,
kategoriId: (originalData as any)?.KategoriToPasar?.map((k: any) => k.kategoriId) || [],
kontak: originalData.kontak,
deskripsi: originalData.deskripsi
});
setPreviewImage(originalData.imageUrl || null);
setFile(null);
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
@@ -157,7 +164,7 @@ function EditPasarDesa() {
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
@@ -316,6 +323,19 @@ function EditPasarDesa() {
error={!formData.kategoriId.length ? 'Pilih minimal satu kategori' : undefined}
/>
{/* Input Deskripsi */}
<Box>
<Text fz="sm" fw="bold" mb={6}>
Deskripsi
</Text>
<EditEditor
value={formData.deskripsi}
onChange={(htmlContent) =>
setFormData((prev) => ({ ...prev, deskripsi: htmlContent }))
}
/>
</Box>
<Group justify="right">
<Button
variant="outline"

View File

@@ -23,6 +23,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import pasarDesaState from '../../../../_state/ekonomi/pasar-desa/pasar-desa';
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
export default function CreatePasarDesa() {
const router = useRouter();
@@ -44,6 +45,7 @@ export default function CreatePasarDesa() {
rating: 0,
kategoriId: [],
kontak: '',
deskripsi: ''
};
setPreviewImage(null);
setFile(null);
@@ -80,7 +82,7 @@ export default function CreatePasarDesa() {
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header dengan tombol kembali */}
<Group mb="md">
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
@@ -234,6 +236,18 @@ export default function CreatePasarDesa() {
}
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Produk
</Text>
<CreateEditor
value={statePasar.pasarDesa.create.form.deskripsi}
onChange={(val) => {
statePasar.pasarDesa.create.form.deskripsi = val;
}}
/>
</Box>
{/* Tombol Submit */}
<Group justify="right">
<Button

View File

@@ -81,7 +81,7 @@ function ListPasarDesa({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -109,7 +109,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -106,7 +106,7 @@ function ListSektorUnggulanDesa({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -73,7 +73,9 @@ function ListAjukanIdeInovatif({ search }: { search: string }) {
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4}>Daftar Ide Inovatif</Title>
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh style={{ width: '20%' }}>Nama</TableTh>

View File

@@ -83,7 +83,7 @@ function ListInfoTeknologiTepatGuna({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -84,7 +84,7 @@ function ListKolaborasiInovasi({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -103,7 +103,7 @@ function ListMitraKolaborasi({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -70,7 +70,7 @@ function ListAdministrasiOnline({ search }: { search: string }) {
</Title>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -90,7 +90,7 @@ function ListJenisLayanan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -96,7 +96,7 @@ function ListJenisPengaduan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -79,7 +79,7 @@ function ListPengaduanMasyarakat({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -132,7 +132,7 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
</Button>
</Group>
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -93,8 +93,10 @@ function ListKeamananLingkungan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}
miw={0}
style={{ tableLayout: 'fixed', width: '100%' }}>
<TableThead>

View File

@@ -93,7 +93,7 @@ function ListKontakItem({ search }: { search: string }) {
</Group>
{/* Desktop: Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -92,7 +92,7 @@ function ListKontakDaruratKeamanan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -88,7 +88,7 @@ function ListLaporanPublik({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -92,7 +92,7 @@ function ListPencegahanKriminalitas({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -1,457 +0,0 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import polsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat";
import colors from "@/con/colors";
import {
Box,
Button,
Card,
Group,
Loader,
Modal,
Paper,
Select,
Stack,
Text,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
function EditPolsekTerdekat() {
const polsekState = useProxy(polsekTerdekat);
const params = useParams();
const router = useRouter();
const [layananOptions, setLayananOptions] = useState<
{ value: string; label: string }[]
>([]);
const [modalOpen, setModalOpen] = useState(false);
const [modalUpdateOpen, setModalUpdateOpen] = useState(false);
const [namaLayananBaru, setNamaLayananBaru] = useState("");
const [selectedLayananId, setSelectedLayananId] = useState<string | null>(
null
);
const [namaLayananUpdate, setNamaLayananUpdate] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
});
const [originalData, setOriginalData] = useState({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
});
// load data untuk form edit
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.layananPolsekId || "",
});
setOriginalData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.layananPolsekId || "",
});
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const fetchLayanan = async () => {
try {
const res = await fetch("/api/keamanan/layanan-polsek/find-many");
const data = await res.json();
if (data.success) {
const options = data.data.map((item: any) => ({
value: item.id,
label: item.nama,
}));
setLayananOptions(options);
}
} catch {
toast.error("Gagal memuat layanan polsek");
}
};
const handleTambahLayanan = async () => {
if (!namaLayananBaru.trim())
return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch("/api/keamanan/layanan-polsek/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaLayananBaru }),
});
const data = await res.json();
if (data.success) {
const newLayanan = {
value: data.data.id,
label: data.data.nama,
};
setLayananOptions((prev) => [...prev, newLayanan]);
await fetchLayanan();
polsekState.create.form.layananPolsekId = data.data.id;
toast.success("Layanan baru ditambahkan!");
setModalOpen(false);
setNamaLayananBaru("");
} else {
toast.error(data.message || "Gagal menambah layanan");
}
} catch {
toast.error("Error menambah layanan");
}
};
const handleUpdateLayanan = async (id: string, namaBaru: string) => {
if (!namaBaru.trim())
return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch(`/api/keamanan/layanan-polsek/update/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaBaru }),
});
const data = await res.json();
if (data.success) {
await fetchLayanan();
toast.success("Layanan berhasil diupdate!");
setModalUpdateOpen(false);
setNamaLayananUpdate("");
} else {
toast.error(data.message || "Gagal mengupdate layanan");
}
} catch {
toast.error("Error mengupdate layanan");
}
};
const handleDeleteLayanan = async (id: string) => {
const confirmDelete = confirm("Yakin ingin menghapus layanan ini?");
if (!confirmDelete) return;
try {
const res = await fetch(`/api/keamanan/layanan-polsek/del/${id}`, {
method: "DELETE",
});
const data = await res.json();
if (data.success) {
await fetchLayanan();
setLayananOptions((prev) =>
prev.filter((layanan) => layanan.value !== id)
);
toast.success("Layanan berhasil dihapus!");
} else {
toast.error(data.message || "Gagal menghapus layanan");
}
} catch {
toast.error("Error menghapus layanan");
}
};
useEffect(() => {
fetchLayanan();
}, []);
const handleChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
jarakKeDesa: originalData.jarakKeDesa,
alamat: originalData.alamat,
nomorTelepon: originalData.nomorTelepon,
jamOperasional: originalData.jamOperasional,
embedMapUrl: originalData.embedMapUrl,
namaTempatMaps: originalData.namaTempatMaps,
alamatMaps: originalData.alamatMaps,
linkPetunjukArah: originalData.linkPetunjukArah,
layananPolsekId: originalData.layananPolsekId,
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
polsekState.edit.form = { ...formData }; // update global state hanya di sini
await polsekState.edit.update();
toast.success("Polsek terdekat berhasil diperbarui!");
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memperbarui data polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Modal Tambah */}
<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
title="Tambah Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananBaru}
onChange={(e) => setNamaLayananBaru(e.currentTarget.value)}
/>
<Button onClick={handleTambahLayanan}>Simpan</Button>
</Stack>
</Modal>
{/* Modal Update */}
<Modal
opened={modalUpdateOpen}
onClose={() => setModalUpdateOpen(false)}
title="Update Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananUpdate}
onChange={(e) => setNamaLayananUpdate(e.currentTarget.value)}
/>
<Button
onClick={() => {
if (!selectedLayananId)
return toast.warn("ID layanan tidak ditemukan");
handleUpdateLayanan(selectedLayananId, namaLayananUpdate);
}}
>
Simpan
</Button>
</Stack>
</Modal>
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Polsek Terdekat
</Title>
</Group>
{/* Form utama */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Input fields */}
<TextInput
value={formData.nama}
onChange={(e) => handleChange("nama", e.currentTarget.value)}
label="Nama Polsek Terdekat"
placeholder="Masukkan nama Polsek Terdekat"
required
/>
<TextInput
value={formData.jarakKeDesa}
onChange={(e) => handleChange("jarakKeDesa", e.currentTarget.value)}
label="Jarak Polsek Terdekat"
/>
<TextInput
value={formData.alamat}
onChange={(e) => handleChange("alamat", e.currentTarget.value)}
label="Alamat Polsek Terdekat"
/>
<TextInput
value={formData.nomorTelepon}
onChange={(e) => handleChange("nomorTelepon", e.currentTarget.value)}
label="Nomor Telepon"
/>
<TextInput
value={formData.jamOperasional}
onChange={(e) => handleChange("jamOperasional", e.currentTarget.value)}
label="Jam Operasional"
/>
<TextInput
value={formData.embedMapUrl}
onChange={(e) => handleChange("embedMapUrl", e.currentTarget.value)}
label="Embed Map URL"
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(e) => handleChange("namaTempatMaps", e.currentTarget.value)}
label="Nama Tempat Maps"
/>
<TextInput
value={formData.alamatMaps}
onChange={(e) => handleChange("alamatMaps", e.currentTarget.value)}
label="Alamat Maps"
/>
<TextInput
value={formData.linkPetunjukArah}
onChange={(e) => handleChange("linkPetunjukArah", e.currentTarget.value)}
label="Link Petunjuk Arah"
/>
<Select
label="Layanan Polsek"
placeholder="Pilih layanan polsek"
data={layananOptions}
value={formData.layananPolsekId}
onChange={(val) => handleChange("layananPolsekId", val || "")}
/>
<Button
variant="light"
size="xs"
onClick={() => setModalOpen(true)}
>
+ Tambah Layanan Baru
</Button>
{/* List layanan */}
<Text fw="bold" fz="sm">
Daftar Layanan Polsek
</Text>
{layananOptions.map((item) => (
<Card
key={item.value}
style={{ border: "1px solid #ccc" }}
bg={colors["white-1"]}
p="md"
radius="md"
shadow="sm"
>
<Group justify="space-between">
<Text>{item.label}</Text>
<Group>
<Button
variant="light"
size="xs"
onClick={() => {
setSelectedLayananId(item.value);
setNamaLayananUpdate(item.label);
setModalUpdateOpen(true);
}}
>
Edit
</Button>
<Button
variant="outline"
color="red"
size="xs"
onClick={() => handleDeleteLayanan(item.value)}
>
Hapus
</Button>
</Group>
</Group>
</Card>
))}
{/* Submit */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPolsekTerdekat;

View File

@@ -0,0 +1,150 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, ScrollArea, Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { IconBuilding, IconTool } from '@tabler/icons-react';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutPolsek({ children }: { children: React.ReactNode }) {
const router = useRouter();
const pathname = usePathname();
const tabs = [
{
label: "Daftar Polsek Terdekat",
value: "daftar-polsek-terdekat",
href: "/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat",
icon: <IconBuilding size={18} stroke={1.8} />
},
{
label: "Layanan Polsek",
value: "layanan-polsek",
href: "/admin/keamanan/polsek-terdekat/layanan-polsek",
icon: <IconTool size={18} stroke={1.8} />
}
];
const currentTab = tabs.find(tab => tab.href === pathname);
const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => {
const tab = tabs.find(t => t.value === value);
if (tab) {
router.push(tab.href);
}
setActiveTab(value);
};
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname);
if (match) {
setActiveTab(match.value);
}
}, [pathname]);
return (
<Stack gap="lg">
<Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
Polsek Terdekat
</Title>
<Tabs
color={colors["blue-button"]}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
{/* ✅ Scroll horizontal wrapper */}
<Box visibleFrom='md' pb={10}>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem", // ✅ biar nggak nempel ke tepi
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
<Box hiddenFrom='md' pb={10}>
<ScrollArea
type="auto"
offsetScrollbars={false}
w="100%"
>
<TabsList
p="xs" // lebih kecil
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
width: "max-content", // ⬅️ kunci
maxWidth: "100%", // ⬅️ penting
}}
>
{tabs.map((tab, i) => (
<TabsTab
key={i}
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
paddingInline: "0.75rem", // ⬅️ lebih ramping
flexShrink: 0, // ✅ jangan mengecil aneh-aneh
}}
>
{tab.label}
</TabsTab>
))}
</TabsList>
</ScrollArea>
</Box>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children}
</TabsPanel>
))}
</Tabs>
</Stack>
);
}
export default LayoutPolsek;

View File

@@ -0,0 +1,279 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import statePolsekTerdekat from "@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat";
import colors from "@/con/colors";
import {
Box,
Button,
Group,
Loader,
MultiSelect,
Paper,
Stack,
TextInput,
Title
} from "@mantine/core";
import { IconArrowBack } from "@tabler/icons-react";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { toast } from "react-toastify";
import { useProxy } from "valtio/utils";
type FormData = {
nama: string;
jarakKeDesa: string;
alamat: string;
nomorTelepon: string;
jamOperasional: string;
embedMapUrl: string;
namaTempatMaps: string;
alamatMaps: string;
linkPetunjukArah: string;
layananPolsekId: string[];
};
function EditPolsekTerdekat() {
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const params = useParams();
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<FormData>({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: []
});
const [originalData, setOriginalData] = useState({
nama: "",
jarakKeDesa: "",
alamat: "",
nomorTelepon: "",
jamOperasional: "",
embedMapUrl: "",
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: []
});
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
// load data untuk form edit
useEffect(() => {
const loadPolsekTerdekat = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await polsekState.edit.load(id);
if (data) {
setFormData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
setOriginalData({
nama: data.nama || "",
jarakKeDesa: data.jarakKeDesa || "",
alamat: data.alamat || "",
nomorTelepon: data.nomorTelepon || "",
jamOperasional: data.jamOperasional || "",
embedMapUrl: data.embedMapUrl || "",
namaTempatMaps: data.namaTempatMaps || "",
alamatMaps: data.alamatMaps || "",
linkPetunjukArah: data.linkPetunjukArah || "",
layananPolsekId: data.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
}
} catch (error) {
console.error("Error loading polsek terdekat:", error);
toast.error("Gagal memuat data polsek terdekat");
}
};
loadPolsekTerdekat();
}, [params?.id]);
const handleChange = (key: keyof FormData, value: any) => {
setFormData((prev) => ({ ...prev, [key]: value }));
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
jarakKeDesa: originalData.jarakKeDesa,
alamat: originalData.alamat,
nomorTelepon: originalData.nomorTelepon,
jamOperasional: originalData.jamOperasional,
embedMapUrl: originalData.embedMapUrl,
namaTempatMaps: originalData.namaTempatMaps,
alamatMaps: originalData.alamatMaps,
linkPetunjukArah: originalData.linkPetunjukArah,
layananPolsekId: (originalData as any)?.LayananToPolsek?.map((l: any) => l.layananId) || [],
});
toast.info("Form dikembalikan ke data awal");
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
await polsekState.edit.update();
toast.success("Polsek terdekat berhasil diperbarui!");
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
} catch (error) {
console.error("Error updating polsek terdekat:", error);
toast.error("Gagal memperbarui data polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors["blue-button"]} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Polsek Terdekat
</Title>
</Group>
{/* Form utama */}
<Paper
w={{ base: "100%", md: "50%" }}
bg={colors["white-1"]}
p="lg"
radius="md"
shadow="sm"
style={{ border: "1px solid #e0e0e0" }}
>
<Stack gap="md">
{/* Input fields */}
<TextInput
value={formData.nama}
onChange={(e) => handleChange("nama", e.currentTarget.value)}
label="Nama Polsek Terdekat"
placeholder="Masukkan nama Polsek Terdekat"
required
/>
<TextInput
value={formData.jarakKeDesa}
onChange={(e) => handleChange("jarakKeDesa", e.currentTarget.value)}
label="Jarak Polsek Terdekat"
/>
<TextInput
value={formData.alamat}
onChange={(e) => handleChange("alamat", e.currentTarget.value)}
label="Alamat Polsek Terdekat"
/>
<TextInput
value={formData.nomorTelepon}
onChange={(e) => handleChange("nomorTelepon", e.currentTarget.value)}
label="Nomor Telepon"
/>
<TextInput
value={formData.jamOperasional}
onChange={(e) => handleChange("jamOperasional", e.currentTarget.value)}
label="Jam Operasional"
/>
<TextInput
value={formData.embedMapUrl}
onChange={(e) => handleChange("embedMapUrl", e.currentTarget.value)}
label="Embed Map URL"
/>
<TextInput
value={formData.namaTempatMaps}
onChange={(e) => handleChange("namaTempatMaps", e.currentTarget.value)}
label="Nama Tempat Maps"
/>
<TextInput
value={formData.alamatMaps}
onChange={(e) => handleChange("alamatMaps", e.currentTarget.value)}
label="Alamat Maps"
/>
<TextInput
value={formData.linkPetunjukArah}
onChange={(e) => handleChange("linkPetunjukArah", e.currentTarget.value)}
label="Link Petunjuk Arah"
/>
<MultiSelect
label="Layanan Polsekl"
placeholder="Pilih layanan polsek"
value={formData.layananPolsekId}
onChange={(val) => handleChange('layananPolsekId', val)}
data={
statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []
}
clearable
searchable
required
error={!formData.layananPolsekId.length ? 'Pilih minimal satu layanan polsek' : undefined}
/>
{/* Submit */}
<Group justify="right">
{/* Tombol Batal */}
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditPolsekTerdekat;

View File

@@ -6,12 +6,12 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
import { ModalKonfirmasiHapus } from '../../../../_com/modalKonfirmasiHapus';
import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat';
function DetailPolsekTerdekat() {
const router = useRouter();
const polsekState = useProxy(polsekTerdekat);
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false);
const params = useParams();
@@ -25,7 +25,7 @@ function DetailPolsekTerdekat() {
polsekState.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
router.push("/admin/keamanan/polsek-terdekat");
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
}
};
@@ -40,7 +40,7 @@ function DetailPolsekTerdekat() {
const data = polsekState.findUnique.data;
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Tombol Back */}
<Button
variant="subtle"
@@ -149,10 +149,20 @@ function DetailPolsekTerdekat() {
</Text>
</Box>
{/* Layanan Polsek */}
<Box>
<Text fz="lg" fw="bold">Layanan Polsek</Text>
<Text fz="md" c="dimmed">{data?.layananPolsek?.nama || "-"}</Text>
<Stack gap={4}>
{data.LayananToPolsek && data.LayananToPolsek.length > 0 ? (
data.LayananToPolsek.map((layanan) => (
<Text fz="md" c="dimmed" key={layanan.id}>
{layanan.layanan.nama}
</Text>
))
) : (
<Text fz="sm" c="dimmed">Tidak ada layanan polsek</Text>
)}
</Stack>
</Box>
{/* Aksi */}
@@ -172,7 +182,7 @@ function DetailPolsekTerdekat() {
<Button
color="green"
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${data.id}/edit`)}
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${data.id}/edit`)}
variant="light"
radius="md"
size="md"

View File

@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import {
@@ -6,9 +5,8 @@ import {
Button,
Group,
Loader,
Modal,
MultiSelect,
Paper,
Select,
Stack,
Text,
TextInput,
@@ -19,16 +17,17 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import polsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
import statePolsekTerdekat from '../../../../_state/keamanan/polsek-terdekat';
function CreatePolsekTerdekat() {
const polsekState = useProxy(polsekTerdekat);
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const router = useRouter();
const [layananOptions, setLayananOptions] = useState<{ value: string; label: string }[]>([]);
const [modalOpen, setModalOpen] = useState(false);
const [namaLayananBaru, setNamaLayananBaru] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
statePolsekTerdekat.layananPolsek.findManyAll.load();
}, []);
const resetForm = () => {
polsekState.create.form = {
nama: "",
@@ -40,44 +39,44 @@ function CreatePolsekTerdekat() {
namaTempatMaps: "",
alamatMaps: "",
linkPetunjukArah: "",
layananPolsekId: "",
layananPolsekId: [],
};
};
const isValidGoogleMapsEmbed = (url: string): boolean => {
try {
const u = new URL(url);
return (
u.hostname === 'www.google.com' &&
u.pathname === '/maps/embed' &&
u.searchParams.has('pb')
);
} catch {
return false;
}
};
try {
const u = new URL(url);
return (
u.hostname === 'www.google.com' &&
u.pathname === '/maps/embed' &&
u.searchParams.has('pb')
);
} catch {
return false;
}
};
const handleSubmit = async () => {
const { embedMapUrl } = polsekState.create.form;
const { embedMapUrl } = polsekState.create.form;
// ✅ Validasi Google Maps Embed URL (jika diisi)
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
return;
}
// ✅ Validasi Google Maps Embed URL (jika diisi)
if (embedMapUrl && !isValidGoogleMapsEmbed(embedMapUrl)) {
toast.error("URL embed peta tidak valid. Harap paste iframe dari Google Maps.");
return;
}
try {
setIsSubmitting(true);
await polsekState.create.create();
resetForm();
router.push("/admin/keamanan/polsek-terdekat");
} catch (error) {
console.error(error);
toast.error("Gagal menambah polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
try {
setIsSubmitting(true);
await polsekState.create.create();
resetForm();
router.push("/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat");
} catch (error) {
console.error(error);
toast.error("Gagal menambah polsek terdekat");
} finally {
setIsSubmitting(false);
}
};
const extractEmbedUrl = (input: string): string => {
// Jika sudah berupa URL embed yang valid
@@ -96,77 +95,8 @@ function CreatePolsekTerdekat() {
return input.trim();
};
const fetchLayanan = async () => {
try {
const res = await fetch("/api/keamanan/layanan-polsek/find-many");
const data = await res.json();
if (data.success) {
const options = data.data.map((item: any) => ({
value: item.id,
label: item.nama,
}));
setLayananOptions(options);
}
} catch {
toast.error("Gagal memuat layanan polsek");
}
};
const handleTambahLayanan = async () => {
if (!namaLayananBaru.trim()) return toast.warn("Nama layanan tidak boleh kosong");
try {
const res = await fetch("/api/keamanan/layanan-polsek/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama: namaLayananBaru }),
});
const data = await res.json();
if (data.success) {
const newLayanan = {
value: data.data.id,
label: data.data.nama,
};
setLayananOptions((prev) => [...prev, newLayanan]);
await fetchLayanan();
polsekState.create.form.layananPolsekId = data.data.id;
toast.success("Layanan baru ditambahkan!");
setModalOpen(false);
setNamaLayananBaru("");
} else {
toast.error(data.message || "Gagal menambah layanan");
}
} catch {
toast.error("Error menambah layanan");
}
};
useEffect(() => {
fetchLayanan();
}, []);
return (
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Modal Tambah Layanan */}
<Modal
opened={modalOpen}
onClose={() => setModalOpen(false)}
title="Tambah Layanan Polsek"
centered
>
<Stack>
<TextInput
label="Nama Layanan"
placeholder="Masukkan nama layanan"
value={namaLayananBaru}
onChange={(e) => setNamaLayananBaru(e.currentTarget.value)}
/>
<Button onClick={handleTambahLayanan}>Simpan</Button>
</Stack>
</Modal>
<Box px={{ base: 0, md: 'xs' }} py="xs">
{/* Header */}
<Group mb="md">
<Button
@@ -255,36 +185,23 @@ function CreatePolsekTerdekat() {
label={<Text fw="bold" fz="sm">Link Petunjuk Arah</Text>}
placeholder="Masukkan link petunjuk arah"
/>
<Select
<MultiSelect
label="Layanan Polsek"
placeholder="Pilih layanan polsek"
data={layananOptions}
value={polsekState.create.form.layananPolsekId || null}
onChange={(val: string | null) => {
if (val) {
const selected = layananOptions.find(
(item) => item.value === val
);
if (selected) {
polsekState.create.form.layananPolsekId = selected.value;
}
} else {
polsekState.create.form.layananPolsekId = '';
}
placeholder="Pilih layanan polsek (bisa lebih dari satu)"
data={statePolsekTerdekat.layananPolsek.findManyAll.data?.map((v) => ({
value: v.id,
label: v.nama,
})) || []}
value={polsekState.create.form.layananPolsekId}
onChange={(val) => {
polsekState.create.form.layananPolsekId = val;
}}
searchable
clearable
nothingFoundMessage="Tidak ditemukan"
nothingFoundMessage="Tidak ada layanan ditemukan"
required
error={polsekState.create.form.layananPolsekId?.length === 0 ? "Pilih minimal 1 layanan polsek" : undefined}
/>
<Button
variant="light"
size="xs"
onClick={() => setModalOpen(true)}
>
+ Tambah Layanan Baru
</Button>
{/* Tombol Submit */}
<Group justify="right">
<Button

View File

@@ -23,8 +23,9 @@ import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import polsekTerdekat from '../../_state/keamanan/polsek-terdekat';
import HeaderSearch from '../../../_com/header';
import statePolsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
function PolsekTerdekat() {
const [search, setSearch] = useState("");
@@ -45,7 +46,7 @@ function PolsekTerdekat() {
}
function ListPolsekTerdekat({ search }: { search: string }) {
const polsekState = useProxy(polsekTerdekat);
const polsekState = useProxy(statePolsekTerdekat.polsekTerdekatState);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
@@ -82,14 +83,14 @@ function ListPolsekTerdekat({ search }: { search: string }) {
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/polsek-terdekat/create')}
onClick={() => router.push('/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}
@@ -145,7 +146,7 @@ function ListPolsekTerdekat({ search }: { search: string }) {
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${item.id}`)}
w="100%"
>
<IconDeviceImac size={18} />
@@ -207,7 +208,7 @@ function ListPolsekTerdekat({ search }: { search: string }) {
variant="light"
color="blue"
fullWidth
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/${item.id}`)}
onClick={() => router.push(`/admin/keamanan/polsek-terdekat/daftar-polsek-terdekat/${item.id}`)}
>
<IconDeviceImac size={18} />
<Text ml={5} fz="sm" fw={500} lh={1.4}>

View File

@@ -0,0 +1,161 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import statePolsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function EditLayananPolsek() {
const editState = useProxy(statePolsekTerdekat.layananPolsek);
const router = useRouter();
const params = useParams();
const [isSubmitting, setIsSubmitting] = useState(false);
const [originalData, setOriginalData] = useState({
nama: '',
});
const [formData, setFormData] = useState({
nama: '',
});
useEffect(() => {
const loadLayananPolsek = async () => {
const id = params?.id as string;
if (!id) return;
try {
const data = await editState.update.load(id);
if (data) {
setFormData({
nama: data.nama || '',
});
setOriginalData({
nama: data.nama || '',
});
}
} catch (error) {
console.error('Error loading layanan polsek:', error);
toast.error('Gagal memuat data layanan polsek');
}
};
loadLayananPolsek();
}, [params?.id]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const handleResetForm = () => {
setFormData({
nama: originalData.nama,
});
toast.info('Form dikembalikan ke data awal');
};
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// update global state hanya saat submit
editState.update.form = {
...editState.update.form,
nama: formData.nama,
};
await editState.update.update();
toast.success('Layanan Polsek berhasil diperbarui!');
router.push('/admin/keamanan/polsek-terdekat/layanan-polsek');
} catch (error) {
console.error('Error updating layanan polsek:', error);
toast.error('Terjadi kesalahan saat memperbarui layanan polsek');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Back Button + Title */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Edit Layanan Polsek
</Title>
</Group>
{/* Form Wrapper */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
name="nama"
label="Nama Layanan Polsek"
placeholder="Masukkan nama layanan polsek"
value={formData.nama}
onChange={handleChange}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={handleResetForm}
>
Batal
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default EditLayananPolsek;

View File

@@ -0,0 +1,111 @@
'use client';
import statePolsekTerdekat from '@/app/admin/(dashboard)/_state/keamanan/polsek-terdekat';
import colors from '@/con/colors';
import {
Box,
Button,
Group,
Loader,
Paper,
Stack,
TextInput,
Title
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
function CreateLayananPolsek() {
const createState = useProxy(statePolsekTerdekat.layananPolsek);
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const resetForm = () => {
createState.create.form = {
nama: '',
};
};
const handleSubmit = async () => {
setIsSubmitting(true);
try {
await createState.create.create();
resetForm();
router.push('/admin/keamanan/polsek-terdekat/layanan-polsek');
} catch (error) {
console.error('Error creating layanan polsek:', error);
toast.error('Gagal menambahkan layanan polsek');
} finally {
setIsSubmitting(false);
}
};
return (
<Box px={{ base: 0, md: 'lg' }} py="xs">
{/* Header dengan back button */}
<Group mb="md">
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
<Title order={4} ml="sm" c="dark">
Tambah Layanan Polsek
</Title>
</Group>
{/* Form utama */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Layanan Polsek"
placeholder="Masukkan nama layanan polsek"
value={createState.create.form.nama || ''}
onChange={(e) => (createState.create.form.nama = e.target.value)}
required
/>
<Group justify="right">
<Button
variant="outline"
color="gray"
radius="md"
size="md"
onClick={resetForm}
>
Reset
</Button>
{/* Tombol Simpan */}
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
{isSubmitting ? <Loader size="sm" color="white" /> : 'Simpan'}
</Button>
</Group>
</Stack>
</Paper>
</Box>
);
}
export default CreateLayananPolsek;

View File

@@ -0,0 +1,265 @@
'use client'
import colors from '@/con/colors';
import {
Box,
Button,
Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
} from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconPlus, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../../_com/header';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import statePolsekTerdekat from '../../../_state/keamanan/polsek-terdekat';
function LayananPolsek() {
const [search, setSearch] = useState("");
return (
<Box>
<HeaderSearch
title='Layanan Polsek'
placeholder='Cari layanan polsek...'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListLayananPolsek search={search} />
</Box>
);
}
function ListLayananPolsek({ search }: { search: string }) {
const layananState = useProxy(statePolsekTerdekat.layananPolsek);
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter();
const [debouncedSearch] = useDebouncedValue(search, 1000);
const {
data,
page,
totalPages,
loading,
load,
} = layananState.findMany;
useShallowEffect(() => {
load(page, 10, debouncedSearch);
}, [page, debouncedSearch]);
const handleDelete = () => {
if (selectedId) {
layananState.delete.delete(selectedId);
setModalHapus(false);
setSelectedId(null);
load(page, 10, search);
}
};
const filteredData = data || [];
if (loading || !data) {
return (
<Stack py={{ base: 'sm', md: 'lg' }}>
<Skeleton height={600} radius="md" />
</Stack>
);
}
return (
<Box py={{ base: 'sm', md: 'lg' }}>
<Paper withBorder bg={colors['white-1']} p={{ base: 'md', md: 'lg' }} shadow="md" radius="md">
<Group justify="space-between" mb={{ base: 'md', md: 'lg' }}>
<Title order={4} lh={{ base: 1.2, md: 1.2 }}>
Daftar Layanan Polsek
</Title>
<Button
leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/keamanan/polsek-terdekat/layanan-polsek/create')}
>
Tambah Baru
</Button>
</Group>
{/* Desktop Table */}
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}
style={{
tableLayout: 'fixed',
width: '100%',
}}
>
<TableThead>
<TableTr>
<TableTh style={{ width: '25%' }}>
<Text fz="sm" fw={600} lh={1.4}>
Nama Layanan Polsek
</Text>
</TableTh>
<TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4} ta="center">Edit</Text>
</TableTh>
<TableTh w="20%">
<Text fz="sm" fw={600} lh={1.4} ta="center">Hapus</Text>
</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>
<Text fz="md" fw={500} lh={1.5} truncate="end">
{item.nama}
</Text>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="green"
onClick={() =>
router.push(
`/admin/keamanan/polsek-terdekat/layanan-polsek/${item.id}`
)
}
size="compact-sm"
>
<IconEdit size={16} />
</Button>
</TableTd>
<TableTd ta="center">
<Button
variant="light"
color="red"
disabled={layananState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
size="compact-sm"
>
<IconTrash size={16} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data Polsek yang cocok
</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
{/* Mobile Cards */}
<Box hiddenFrom="md">
<Stack gap="sm">
{filteredData.length > 0 ? (
filteredData.map((item) => (
<Paper key={item.id} withBorder p="sm" radius="sm">
<Stack gap={"xs"}>
<Box>
<Text fz="sm" fw={600} lh={1.4}>
Nama Layanan Polsek
</Text>
<Text fz="sm" fw={500} lh={1.45}>
{item.nama}
</Text>
</Box>
<Group mt="sm" justify="flex-end" gap="xs">
<Button
variant="light"
color="green"
size="compact-xs"
onClick={() =>
router.push(
`/admin/keamanan/polsek-terdekat/layanan-polsek/${item.id}`
)
}
>
<IconEdit size={14} />
</Button>
<Button
variant="light"
color="red"
size="compact-xs"
disabled={layananState.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={14} />
</Button>
</Group>
</Stack>
</Paper>
))
) : (
<Center py={24}>
<Text c="dimmed" fz="sm" lh={1.4}>
Tidak ada data Layanan Polsek yang cocok
</Text>
</Center>
)}
</Stack>
</Box>
</Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt={{ base: 'lg', md: 'xl' }}
mb={{ base: 'lg', md: 'xl' }}
color="blue"
radius="md"
/>
</Center>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text="Apakah anda yakin ingin menghapus layanan polsek ini?"
/>
</Box>
);
}
export default LayananPolsek;

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react';
import LayoutPolsek from './_com/layoutPolsek';
import { usePathname } from 'next/navigation';
import { Box } from '@mantine/core';
function Layout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// Contoh path:
// - /darmasaba/desa/berita/semua → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan → panjang 5 → list
// - /darmasaba/desa/berita/Pemerintahan/123 → panjang 6 → detail
const segments = pathname.split('/').filter(Boolean);
const isDetailPage = segments.length >= 5;
if (isDetailPage) {
// Tampilkan tanpa tab menu
return (
<Box>
{children}
</Box>
);
}
return (
<LayoutPolsek>
{children}
</LayoutPolsek>
);
}
export default Layout;

View File

@@ -85,7 +85,7 @@ function ListTipsKeamanan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table
highlightOnHover
miw={0}

View File

@@ -84,7 +84,9 @@ function ListArtikelKesehatan({ search }: { search: string }) {
{/* Tabel */}
<Box style={{ overflowX: "auto" }}>
<Table highlightOnHover>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh style={{ minWidth: 200 }}>Judul</TableTh>

View File

@@ -81,8 +81,10 @@ function ListDokterTenagaMedis({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Nama Dokter</Text></TableTh>

View File

@@ -170,8 +170,10 @@ function ListFasilitasKesehatan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh>Fasilitas Kesehatan</TableTh>

View File

@@ -94,8 +94,10 @@ function ListTarifLayanan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh>

View File

@@ -85,8 +85,10 @@ function ListJadwalKegiatan({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh fz="sm" fw={600} lh={1.2}>Nama</TableTh>

View File

@@ -147,8 +147,10 @@ function ListGrafikHasilKepuasanMasyarakat({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh>Nama</TableTh>

View File

@@ -99,8 +99,10 @@ function ListKelahiran({ search }: { search: string }) {
</Group>
{/* Desktop Table */}
<Box visibleFrom="md">
<Table highlightOnHover fz="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false} fz="md">
<TableThead>
<TableTr>
<TableTh><Text fz="sm" fw={600} lh={1.4}>Nama</Text></TableTh>

View File

@@ -95,8 +95,10 @@ function ListKematian({ search }: { search: string }) {
</Group>
{/* Tabel untuk desktop */}
<Box visibleFrom="md">
<Table highlightOnHover>
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table highlightOnHover
layout="fixed" // 🔥 PENTING
withColumnBorders={false}>
<TableThead>
<TableTr>
<TableTh><Text fz="sm" fw={600} lh={1.2}>Nama</Text></TableTh>

View File

@@ -234,7 +234,7 @@ function GrafikPersentaseKelahiranKematian() {
{/* Desktop: Table */}
<Box visibleFrom="md">
<Box visibleFrom="md" style={{ overflowX: 'auto' }}>
<Table striped withTableBorder highlightOnHover>
<Table.Thead>
<Table.Tr>

Some files were not shown because too many files have changed in this diff Show More