Merge pull request 'amalia/28-nov-25' (#42) from amalia/28-nov-25 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/42
This commit is contained in:
2025-11-28 17:02:35 +08:00
8 changed files with 80 additions and 43 deletions

View File

@@ -1,6 +1,6 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { Card, Divider, Flex, Stack, Title } from "@mantine/core"; import { Card, Divider, Flex, Stack, Text, Title } from "@mantine/core";
import { IconSettings } from "@tabler/icons-react"; import { IconChartBar } from "@tabler/icons-react";
import type { EChartsOption } from "echarts"; import type { EChartsOption } from "echarts";
import EChartsReact from "echarts-for-react"; import EChartsReact from "echarts-for-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -21,7 +21,7 @@ export default function DashboardGrafik() {
darkMode: true, darkMode: true,
animation: true, animation: true,
legend: { legend: {
textStyle: { color: "#fff" } // warna legend putih textStyle: { color: "#fff" }
}, },
tooltip: {}, tooltip: {},
dataset: { dataset: {
@@ -34,7 +34,8 @@ export default function DashboardGrafik() {
}, },
yAxis: { yAxis: {
type: "value", type: "value",
minInterval: 1 minInterval: 1,
axisLabel: { color: "#fff" }
}, },
color: ["#1abc9c", "#10816aff"], color: ["#1abc9c", "#10816aff"],
series: [ series: [
@@ -62,20 +63,19 @@ export default function DashboardGrafik() {
boxShadow: "0 0 25px rgba(0,255,200,0.08)", boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}} }}
> >
<Stack gap="md"> <Stack gap="sm">
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Flex direction={"column"}>
<Title order={4} c="gray.0"> <Title order={4} c="gray.0">
System Performance Grafik Pengaduan dan Pelayanan Surat
</Title> </Title>
<IconSettings size={20} color="gray" /> <Text size="sm">7 Hari Terakhir</Text>
</Flex>
<IconChartBar size={20} color="gray" />
</Flex> </Flex>
<Divider my="xs" /> <Divider my="xs" />
<Stack gap="sm"> <Stack gap="sm">
<EChartsReact style={{ height: 400, width: "100%" }} option={options} /> <EChartsReact style={{ height: 400 }} option={options} />
{/* <ProgressSection label="CPU Usage" value={68} color="teal" />
<ProgressSection label="Memory Usage" value={75} color="cyan" />
<ProgressSection label="Network Load" value={42} color="blue" />
<ProgressSection label="Disk Space" value={88} color="red" /> */}
</Stack> </Stack>
</Stack> </Stack>
</Card> </Card>

View File

@@ -80,7 +80,7 @@ export default function DashboardLastData() {
<PengaduanSection <PengaduanSection
key={index} key={index}
id={item.id} id={item.id}
nomer={item.noPengaduan} nomer={item.noPengajuan}
judul={item.title} judul={item.title}
status={item.status} status={item.status}
updated={item.updatedAt} updated={item.updatedAt}

View File

@@ -7,6 +7,7 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
const [viewFile, setViewFile] = useState<string>(""); const [viewFile, setViewFile] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [typeFile, setTypeFile] = useState<string>(""); const [typeFile, setTypeFile] = useState<string>("");
const [error, setError] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (open && fileName) { if (open && fileName) {
@@ -27,23 +28,36 @@ export default function ModalFile({ open, onClose, folder, fileName }: { open: b
// load file // load file
const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName; const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName;
const res = await fetch(urlApi); const res = await fetch(urlApi);
if (!res.ok) if (!res.ok) {
setError(true);
return notification({ return notification({
title: "Error", title: "Error",
message: "Failed to load image", message: "Failed to load image",
type: "error", type: "error",
}); });
}
const blob = await res.blob(); const blob = await res.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
setViewFile(url); setViewFile(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); setError(true);
notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => {
if (error) {
onClose();
}
}, [error]);
return ( return (

View File

@@ -17,7 +17,7 @@ export const categoryPelayananSurat = [
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }, { name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" } { name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
], ],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "status perkawinan"] dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "agama", "pekerjaan"]
}, },
{ {
id: "skdomisiliorganisasi", id: "skdomisiliorganisasi",
@@ -27,7 +27,7 @@ export const categoryPelayananSurat = [
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" }, { name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
{ name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi" } { name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi" }
], ],
dataText: ["nama organisasi", "alamat organisasi", "nama pemohon", "jabatan pemohon", "kontak", "penanggung jawab", "tanggal berdiri"] dataText: ["nama organisasi", "jenis organisasi", "alamat organisasi/sekretariat", "no telepon", "nama pimpinan", "keperluan"]
}, },
{ {
id: "skkelahiran", id: "skkelahiran",
@@ -45,7 +45,7 @@ export const categoryPelayananSurat = [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" }, { name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" } { name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
], ],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "polsek"] dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "agama", "alamat", "pekerjaan", "polsek"]
}, },
{ {
id: "skkematian", id: "skkematian",
@@ -55,7 +55,7 @@ export const categoryPelayananSurat = [
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }, { name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" } { name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
], ],
dataText: ["nama almarhum", "nik", "tempat tanggal lahir", "alamat", "tanggal kematian", "waktu kematian", "penyebab kematian"] dataText: ["nik pelapor", "nama pelapor", "pekerjaan pelapor", "alamat pelapor", "hubungan pelapor dengan almarhum", "nama almarhum", "nik almarhum", "tempat tanggal lahir almarhum", "alamat almarhum", "agama almarhum", "tanggal kematian", "waktu kematian", "tempat kematian", "penyebab kematian"]
}, },
{ {
id: "skpenghasilan", id: "skpenghasilan",
@@ -65,7 +65,7 @@ export const categoryPelayananSurat = [
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" }, { name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" } { name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
], ],
dataText: ["nama", "nik", "alamat", "pekerjaan", "jenis usaha", "penghasilan", "alasan permohonan"] dataText: ["nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "penghasilan", "alasan permohonan"]
}, },
{ {
id: "sktempatusaha", id: "sktempatusaha",
@@ -76,7 +76,7 @@ export const categoryPelayananSurat = [
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }, { name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" } { name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
], ],
dataText: ["nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"] dataText: ["nik", "nama pemilik", "tempat tanggal lahir", "alamat pemilik", "nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
}, },
{ {
id: "sktidakmampu", id: "sktidakmampu",
@@ -104,6 +104,6 @@ export const categoryPelayananSurat = [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" }, { name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" } { name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
], ],
dataText: ["nama anak", "nama ayah", "status ayah", "nama ibu", "status ibu"] dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "nama ayah", "status ayah", "nama ibu", "status ibu"]
} }
]; ];

View File

@@ -1,3 +1,4 @@
import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat"; import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal"; import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
@@ -77,7 +78,9 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
const [host, setHost] = useState<User | null>(null); const [host, setHost] = useState<User | null>(null);
const [noSurat, setNoSurat] = useState(""); const [noSurat, setNoSurat] = useState("");
const [openedPreview, setOpenedPreview] = useState(false); const [openedPreview, setOpenedPreview] = useState(false);
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState("");
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -128,8 +131,22 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
} }
} }
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
}
}, [viewImg]);
return ( return (
<> <>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
onClose={() => {
setOpenedPreviewFile(false)
}}
folder="syarat-dokumen"
fileName={viewImg}
/>
{/* MODAL KONFIRMASI */} {/* MODAL KONFIRMASI */}
<Modal <Modal
@@ -246,7 +263,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
> >
{syaratDokumen?.map((v: any) => ( {syaratDokumen?.map((v: any) => (
<List.Item key={v.id}> <List.Item key={v.id}>
<Anchor href="https://mantine.dev/" target="_blank"> <Anchor onClick={() => { setViewImg(v.value) }}>
{v.jenis} {v.jenis}
</Anchor> </Anchor>
</List.Item> </List.Item>

View File

@@ -1,3 +1,4 @@
import ModalFile from "@/components/ModalFile";
import notification from "@/components/notificationGlobal"; import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
@@ -10,13 +11,12 @@ import {
Flex, Flex,
Grid, Grid,
Group, Group,
Image,
Modal, Modal,
Stack, Stack,
Table, Table,
Text, Text,
Textarea, Textarea,
Title, Title
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { import {
@@ -73,9 +73,7 @@ export default function DetailPengaduanPage() {
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) { function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak"); const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [imageSrc, setImageSrc] = useState<string | null>(null); const [openedPreview, setOpenedPreview] = useState(false);
const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
useDisclosure(false);
const [keterangan, setKeterangan] = useState(""); const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null); const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
@@ -173,14 +171,12 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
{/* MODAL GAMBAR */} {/* MODAL GAMBAR */}
<Modal <ModalFile
opened={openedModalImage} open={openedPreview && !_.isEmpty(data?.image)}
onClose={closeModalImage} onClose={() => setOpenedPreview(false)}
title="Gambar Pengaduan" folder="pengaduan"
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} fileName={data?.image}
> />
<Image src={imageSrc!} />
</Modal>
<Card <Card
radius="md" radius="md"
@@ -266,7 +262,7 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
{ {
data?.image != null && data?.image != "" data?.image != null && data?.image != ""
? ?
<Anchor href="#" onClick={() => { }}> <Anchor href="#" onClick={() => { setOpenedPreview(true) }}>
Lihat Gambar Lihat Gambar
</Anchor> </Anchor>
: :

View File

@@ -57,7 +57,7 @@ const DashboardRoute = new Elysia({
const kenaikanPengaduan = const kenaikanPengaduan =
dataPengaduanYesterday === 0 dataPengaduanYesterday === 0
? dataPengaduanToday > 0 ? dataPengaduanToday > 0
? 100 ? dataPengaduanToday * 100
: 0 : 0
: ((dataPengaduanToday - dataPengaduanYesterday) / dataPengaduanYesterday) * 100; : ((dataPengaduanToday - dataPengaduanYesterday) / dataPengaduanYesterday) * 100;
@@ -87,7 +87,7 @@ const DashboardRoute = new Elysia({
const kenaikanPelayanan = const kenaikanPelayanan =
dataPelayananYesterday === 0 dataPelayananYesterday === 0
? dataPelayananToday > 0 ? dataPelayananToday > 0
? 100 ? dataPelayananToday * 100
: 0 : 0
: ((dataPelayananToday - dataPelayananYesterday) / dataPelayananYesterday) * 100; : ((dataPelayananToday - dataPelayananYesterday) / dataPelayananYesterday) * 100;
@@ -143,6 +143,7 @@ const DashboardRoute = new Elysia({
select: { select: {
id: true, id: true,
status: true, status: true,
noPengajuan: true,
updatedAt: true, updatedAt: true,
CategoryPelayanan: { CategoryPelayanan: {
select: { select: {
@@ -155,6 +156,7 @@ const DashboardRoute = new Elysia({
const dataPelayananFix = dataPelayanan.map((item) => { const dataPelayananFix = dataPelayanan.map((item) => {
return { return {
id: item.id, id: item.id,
noPengajuan: item.noPengajuan,
title: item.CategoryPelayanan.name, title: item.CategoryPelayanan.name,
status: item.status, status: item.status,
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt), updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),

View File

@@ -436,13 +436,13 @@ const PelayananRoute = new Elysia({
jenis: t.String({ jenis: t.String({
minLength: 1, minLength: 1,
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.", description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "alamat", "pekerjaan", "keperluan"], examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "jenis harus diisi" error: "jenis harus diisi"
}), }),
value: t.String({ value: t.String({
minLength: 1, minLength: 1,
description: "Isi atau nilai dari jenis field terkait.", description: "Isi atau nilai dari jenis field terkait.",
examples: ["Budi Santoso", "Jl. Mawar No. 10", "Karyawan Swasta"], examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"],
error: "value harus diisi" error: "value harus diisi"
}), }),
}), }),
@@ -450,6 +450,14 @@ const PelayananRoute = new Elysia({
description: "Kumpulan data text dinamis sesuai kategori layanan.", description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [ examples: [
[ [
{ jenis: "nama", value: "Budi Santoso" },
{ jenis: "jenis kelamin", value: "Laki-laki" },
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ jenis: "negara", value: "Indonesia" },
{ jenis: "agama", value: "Islam" },
{ jenis: "status perkawinan", value: "Belum menikah" },
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
{ jenis: "jenis usaha", value: "usaha makanan" }, { jenis: "jenis usaha", value: "usaha makanan" },
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" }, { jenis: "alamat usaha", value: "Jl. Melati No. 21" },
] ]