Merge pull request 'amalia/20-nov-25' (#33) from amalia/20-nov-25 into main

Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/33
This commit is contained in:
2025-11-20 17:32:19 +08:00
14 changed files with 726 additions and 52 deletions

View File

@@ -1,8 +1,10 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
ActionIcon, ActionIcon,
Anchor,
Button, Button,
Divider, Divider,
FileInput,
Flex, Flex,
Group, Group,
Input, Input,
@@ -10,18 +12,23 @@ import {
Stack, Stack,
Table, Table,
Title, Title,
Tooltip, Tooltip
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react"; import { IconEdit } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import ModalFile from "./ModalFile";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
import _ from "lodash";
export default function DesaSetting() { export default function DesaSetting() {
const [btnDisable, setBtnDisable] = useState(false); const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(false); const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [img, setImg] = useState<any>()
const [openedPreview, setOpenedPreview] = useState(false);
const [viewImg, setViewImg] = useState("");
const { data, mutate, isLoading } = useSWR("/", () => const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api["configuration-desa"].list.get(), apiFetch.api["configuration-desa"].list.get(),
); );
@@ -39,7 +46,31 @@ export default function DesaSetting() {
async function handleEdit() { async function handleEdit() {
try { try {
setBtnLoading(true); setBtnLoading(true);
const res = await apiFetch.api["configuration-desa"].edit.post(dataEdit);
let finalData = { ...dataEdit }; // ← buffer data terbaru
if (dataEdit.name === "TTD") {
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img });
if (resImg.status === 200) {
finalData = {
...finalData,
value: resImg.data?.filename || ""
};
setDataEdit(finalData); // update state
} else {
return notification({
title: "Error",
message: "Failed to upload image",
type: "error",
});
}
}
const res = await apiFetch.api["configuration-desa"].edit.post(finalData);
if (res.status === 200) { if (res.status === 200) {
mutate(); mutate();
close(); close();
@@ -67,11 +98,8 @@ export default function DesaSetting() {
} }
} }
function chooseEdit({
data, function chooseEdit({ data }: { data: { id: string; value: string; name: string }; }) {
}: {
data: { id: string; value: string; name: string };
}) {
setDataEdit(data); setDataEdit(data);
open(); open();
} }
@@ -100,18 +128,35 @@ export default function DesaSetting() {
opened={opened} opened={opened}
onClose={close} onClose={close}
title={"Edit"} title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
> >
<Stack gap="ld"> <Stack gap="ld">
<Input.Wrapper label={dataEdit.name}> {
<Input dataEdit.name == "TTD"
value={dataEdit.value} ?
onChange={(e) => (
onValidation({ kat: "value", value: e.target.value }) <Input.Wrapper label={dataEdit.name}>
} <FileInput
/> clearable
</Input.Wrapper> placeholder="Upload TTD"
accept="image/*"
onChange={(e) => { setImg(e) }}
/>
</Input.Wrapper>
)
:
(
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</Input.Wrapper>
)
}
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Batal Batal
@@ -119,7 +164,7 @@ export default function DesaSetting() {
<Button <Button
variant="filled" variant="filled"
onClick={handleEdit} onClick={handleEdit}
disabled={btnDisable} disabled={btnDisable || (dataEdit.name == "TTD" && !img)}
loading={btnLoading} loading={btnLoading}
> >
Simpan Simpan
@@ -127,6 +172,14 @@ export default function DesaSetting() {
</Group> </Group>
</Stack> </Stack>
</Modal> </Modal>
<ModalFile
open={openedPreview && !_.isEmpty(viewImg)}
onClose={() => setOpenedPreview(false)}
folder="syarat-dokumen"
fileName={viewImg}
/>
<Stack gap={"md"}> <Stack gap={"md"}>
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
@@ -147,7 +200,17 @@ export default function DesaSetting() {
{list?.map((v: any) => ( {list?.map((v: any) => (
<Table.Tr key={v.id}> <Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Table.Td>{v.name}</Table.Td>
<Table.Td>{v.value}</Table.Td> <Table.Td>
{
v.name == "TTD"
?
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
Lihat
</Anchor>
:
v.value
}
</Table.Td>
<Table.Td> <Table.Td>
<Tooltip label="Edit Setting"> <Tooltip label="Edit Setting">
<ActionIcon <ActionIcon

View File

@@ -0,0 +1,66 @@
import { Flex, Image, Loader, Modal } from "@mantine/core";
import { useEffect, useState } from "react";
import notification from "./notificationGlobal";
export default function ModalFile({ open, onClose, folder, fileName }: { open: boolean, onClose: () => void, folder: string, fileName: string }) {
const [viewImg, setViewImg] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
if (open && fileName) {
loadImage();
}
}, [open, fileName]);
const loadImage = async () => {
try {
setViewImg("");
setLoading(true);
const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
} finally {
setLoading(false);
}
};
return (
<Modal
opened={open}
onClose={onClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="lg"
withCloseButton
removeScrollProps={{ allowPinchZoom: true }}
title="File"
>
{loading && (
<Flex justify="center" align="center" h={200}>
<Loader />
</Flex>
)}
{viewImg && (
<Image
radius="md"
h={300}
fit="contain"
src={viewImg}
/>
)}
</Modal>
);
}

View File

@@ -6,9 +6,14 @@ import html2canvas from "html2canvas";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import { useRef } from "react"; import { useRef } from "react";
import useSWR from "swr"; import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
import SKKelahiran from "./surat/SKKelahiran"; import SKKelahiran from "./surat/SKKelahiran";
import SKKelakuanBaik from "./surat/SKKelakuanBaik"; import SKKelakuanBaik from "./surat/SKKelakuanBaik";
import SKKematian from "./surat/SKKematian";
import SKPenghasilan from "./surat/SKPenghasilan"; import SKPenghasilan from "./surat/SKPenghasilan";
import SKTempatUsaha from "./surat/SKTempatUsaha";
import SKTidakMampu from "./surat/SKTidakMampu"; import SKTidakMampu from "./surat/SKTidakMampu";
import SKUsaha from "./surat/SKUsaha"; import SKUsaha from "./surat/SKUsaha";
import SKYatim from "./surat/SKYatimPiatu"; import SKYatim from "./surat/SKYatimPiatu";
@@ -115,8 +120,17 @@ export default function ModalSurat({ open, onClose, surat }: { open: boolean, on
? <SKTidakMampu data={data.data} /> ? <SKTidakMampu data={data.data} />
: data.data.surat.idCategory == "skyatimpiatu" : data.data.surat.idCategory == "skyatimpiatu"
? <SKYatim data={data.data} /> ? <SKYatim data={data.data} />
: <></> : data.data.surat.idCategory == "skdomisiliorganisasi"
? <SKDomisiliOrganisasi data={data.data} />
: data.data.surat.idCategory == "skbedabiodata"
? <SKBedaBiodataDiri data={data.data} />
: data.data.surat.idCategory == "sktempatusaha"
? <SKTempatUsaha data={data.data} />
: data.data.surat.idCategory == "skbelumkawin"
? <SKBelumKawin data={data.data} />
: data.data.surat.idCategory == "skkematian"
? <SKKematian data={data.data} />
: <></>
: <></> : <></>
} }
</div> </div>

View File

@@ -0,0 +1,129 @@
import _ from "lodash";
export default function SKBedaBiodataDiri({ data }: { data: any }) {
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
return (
<div style={{ lineHeight: "1.25" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "15px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center" }}>
<b><u>SURAT KETERANGAN BEDA BIODATA DIRI</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan + " " + data.setting.desaNama}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "15px" }}>
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang bersangkutan:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>, meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa dokumen, sebagai berikut:
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>1. Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td><td style={{ width: "10px" }}>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>3. Nama Orang Tua</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama orang tua")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena <b>kesalahan penulisan/pencatatan administratif</b>, namun yang bersangkutan adalah <b>orang yang sama</b>.
<br />
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "15px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "30px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br />
{data.setting.perbekelNama} <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import _ from "lodash";
export default function SKBelumKawin({ data }: { data: any }) {
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN BELUM KAWIN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan} {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan dari yang bersangkutan dan data administrasi kependudukan yang ada di Desa {data.setting.desaNama},
yang bersangkutan benar sampai saat ini belum pernah menikah, baik secara adat, agama, maupun hukum negara.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,93 @@
import _ from "lodash";
export default function SKDomisiliOrganisasi({ data }: { data: any }) {
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN DOMISILI ORGANISASI</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Alamat Kantor</td>
<td>:</td>
<td>{data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama Organisasi</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Jenis Organisasi</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Nomor Telepon</td><td>:</td><td>{getValue("negara")}</td></tr>
<tr><td>Nama Pimpinan</td><td>:</td><td>{getValue("agama")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}.
Dan sampai saat ini masih aktif melakukan kegiatan sesuai dengan bidangnya.<br />
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br />
{data.setting.perbekelNama} <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,89 @@
import _ from "lodash";
export default function SKKematian({ data }: { data: any }) {
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN KEMATIAN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Hubungan dengan almarhum/almarhumah</td><td>:</td><td>{getValue("hubungan dengan almarhum")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Melaporkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Telah meninggal dunia pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Tanggal Kematian</td><td style={{ width: "10px" }}>:</td><td>{getValue("tanggal kematian")}</td></tr>
<tr><td>Waktu Kematian</td><td>:</td><td>{getValue("waktu kematian")}</td></tr>
<tr><td>Tempat Kematian</td><td>:</td><td>{getValue("tempat kematian")}</td></tr>
<tr><td>Penyebab Kematian</td><td>:</td><td>{getValue("penyebab kematian")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,90 @@
import _ from "lodash";
export default function SKTempatUsaha({ data }: { data: any }) {
const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
return (
<div style={{ lineHeight: "1.5" }}>
{/* TITLE */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b><br />
Nomor: {data.surat.noSurat}
</div>
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya:
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
<Row label="Alamat" value={data.setting.desaAlamat} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama")} />
<Row label="Tempat/Tanggal Lahir" value={getValue("tempat tanggal lahir")} />
<Row label="Alamat Pemilik Usaha" value={getValue("alamat")} />
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
<br />
<div>Benar yang bersangkutan memiliki tempat usaha dengan keterangan seperti berikut:</div>
<div>
<Row label="Nama Usaha" value={getValue("nama usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status tempat usaha")} />
<Row label="Luas Tempat Usaha" value={getValue("luas tempat usaha")} />
<Row label="Jumlah Karyawan" value={getValue("jumlah karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan <b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
</p>
<div>
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
<Row label="Pada tanggal" value={data.surat.createdAt} />
</div>
<br /><br />
{/* TANDA TANGAN */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
{data.setting.desaNama}, {data.surat.createdAt} <br /><br /><br />
<u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
</div>
);
}
function Row({ label, value }: { label: string, value: string }) {
return (
<div style={{ display: "flex", marginBottom: "4px" }}>
<div style={{ width: "180px" }}>{label}</div>
<div style={{ width: "10px" }}>:</div>
<div>{value}</div>
</div>
);
}

View File

@@ -19,7 +19,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
</div> </div>
{/* DATA PEJABAT */} {/* DATA PEJABAT */}
<div style={{ marginLeft: "20px" }}> <div>
<Row label="Nama" value={data.setting.perbekelNama} /> <Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Alamat" value={data.setting.desaAlamat} /> <Row label="Alamat" value={data.setting.desaAlamat} />
@@ -32,7 +32,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
<div>Dengan ini menerangkan bahwa:</div> <div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */} {/* DATA WARGA */}
<div style={{ marginLeft: "20px" }}> <div>
<Row label="Nama" value={getValue("nama")} /> <Row label="Nama" value={getValue("nama")} />
<Row label="Tempat Tgl Lahir" value={getValue("tempat tanggal lahir")} /> <Row label="Tempat Tgl Lahir" value={getValue("tempat tanggal lahir")} />
@@ -61,7 +61,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
<div style={{ textAlign: "center" }}> <div style={{ textAlign: "center" }}>
{data.setting.desaNama}, {data.surat.createdAt} <br /><br /><br /> {data.setting.desaNama}, {data.surat.createdAt} <br /><br /><br />
<b><u>{data.setting.perbekelNama}</u></b><br /> <u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama} {data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div> </div>
</div> </div>

View File

@@ -1,11 +1,41 @@
import _ from "lodash"; import _ from "lodash";
import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKUsaha({ data }: { data: any }) { export default function SKUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || "" data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
); );
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=syarat-dokumen&fileName=' + data.setting.perbekelTTD;
// Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url);
} catch (err) {
console.error("Gagal load gambar:", err);
}
};
useEffect(() => {
loadImage();
}, [data]);
return ( return (
<div style={{ lineHeight: "1.3" }}> <div style={{ lineHeight: "1.3" }}>
{/* HEADER */} {/* HEADER */}
@@ -18,7 +48,7 @@ export default function SKUsaha({ data }: { data: any }) {
</div> </div>
{/* JUDUL */} {/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}> <div style={{ textAlign: "center", margin: "15px 0" }}>
<b><u>SURAT KETERANGAN USAHA</u></b><br /> <b><u>SURAT KETERANGAN USAHA</u></b><br />
Nomor: {data.surat.noSurat} Nomor: {data.surat.noSurat}
</div> </div>
@@ -102,13 +132,15 @@ export default function SKUsaha({ data }: { data: any }) {
</div> </div>
{/* TANDA TANGAN */} {/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end", width: "100%" }}> <div style={{ marginTop: "20px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}> <div style={{ textAlign: "center" }}>
<br /><br /> <br /><br />
Kepala Desa / Lurah {data.setting.desaNama} Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br /> <br /><br />
{data.setting.perbekelNama} <br /> <img src={viewImg} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP} NIP. {data.setting.perbekelNIP}
</div> </div>
</div> </div>

View File

@@ -4,16 +4,10 @@ import KategoriPengaduan from "@/components/KategoriPengaduan";
import ProfileUser from "@/components/ProfileUser"; import ProfileUser from "@/components/ProfileUser";
import UserSetting from "@/components/UserSetting"; import UserSetting from "@/components/UserSetting";
import { import {
Button,
Card, Card,
Container, Container,
Divider,
Flex,
Grid, Grid,
NavLink, NavLink
Stack,
Table,
Title,
} from "@mantine/core"; } from "@mantine/core";
import { import {
IconBuildingBank, IconBuildingBank,

View File

@@ -0,0 +1,12 @@
import { v4 as uuidv4 } from "uuid";
import { mimeToExtension } from "./mimetypeToExtension";
export function renameFile({ oldFile, newName }: { oldFile: File; newName: string }) {
const ext = mimeToExtension(oldFile.type)
const nameFix = newName == 'random' ? `${uuidv4()}.${ext}` : newName
return new File([oldFile], nameFix, {
type: oldFile.type,
lastModified: oldFile.lastModified,
});
}

View File

@@ -128,11 +128,15 @@ export async function listFiles(config: Config): Promise<{ name: string }[]> {
} }
} }
export async function catFile(config: Config, fileName: string): Promise<string> { export async function catFile(config: Config, folder: string, fileName: string): Promise<ArrayBuffer> {
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`); const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`);
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
const content = await (await fetchWithAuth(config, downloadUrl)).text();
return content // Download file sebagai binary, BUKAN text
const fileResponse = await fetchWithAuth(config, downloadUrl);
const buffer = await fileResponse.arrayBuffer();
return buffer;
} }
export async function uploadFile(config: Config, file: File): Promise<string> { export async function uploadFile(config: Config, file: File): Promise<string> {
@@ -159,7 +163,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
const text = await res.text(); const text = await res.text();
if (!res.ok) throw new Error(`Upload failed: ${text}`); if (!res.ok) return 'gagal'
return `✅ Uploaded ${file.name} successfully`; return `✅ Uploaded ${file.name} successfully`;
} }

View File

@@ -6,7 +6,8 @@ import { mimeToExtension } from "../lib/mimetypeToExtension"
import { generateNoPengaduan } from "../lib/no-pengaduan" import { generateNoPengaduan } from "../lib/no-pengaduan"
import { normalizePhoneNumber } from "../lib/normalizePhone" import { normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma" import { prisma } from "../lib/prisma"
import { catFile, defaultConfigSF, testConnection, uploadFile, uploadFileBase64 } from "../lib/seafile" import { renameFile } from "../lib/rename-file"
import { catFile, defaultConfigSF, uploadFile, uploadFileBase64 } from "../lib/seafile"
const PengaduanRoute = new Elysia({ const PengaduanRoute = new Elysia({
prefix: "pengaduan", prefix: "pengaduan",
@@ -523,20 +524,27 @@ Respon:
return { success: false, message: "File tidak ditemukan" }; return { success: false, message: "File tidak ditemukan" };
} }
// Rename file
const renamedFile = renameFile({ oldFile: file, newName: 'random' });
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer) // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer(); // const buffer = await file.arrayBuffer();
const result = await uploadFile(defaultConfigSF, file); const result = await uploadFile(defaultConfigSF, renamedFile);
if (result == 'gagal') {
return { success: false, message: "Upload gagal" };
}
return { return {
success: true, success: true,
message: "Upload berhasil", message: "Upload berhasil",
filename: file.name, filename: renamedFile.name,
size: file.size, size: renamedFile.size,
seafileResult: result seafileResult: result
}; };
}, { }, {
body: t.Object({ body: t.Object({
file: t.File({ format: "binary" }) file: t.Any()
}), }),
detail: { detail: {
summary: "Upload File", summary: "Upload File",
@@ -715,14 +723,10 @@ Respon:
} }
}) })
.get("/image", async ({ query, set }) => { .get("/image", async ({ query, set }) => {
const { fileName } = query const { fileName, folder } = query;
const connect = await testConnection(defaultConfigSF) const hasil = await catFile(defaultConfigSF, folder, fileName);
console.log({ connect })
const hasil = await catFile(defaultConfigSF, fileName)
console.log('hasilnya', hasil)
// Tentukan tipe MIME berdasarkan ekstensi
const ext = fileName.split(".").pop()?.toLowerCase(); const ext = fileName.split(".").pop()?.toLowerCase();
const mime = const mime =
ext === "jpg" || ext === "jpeg" ext === "jpg" || ext === "jpeg"
@@ -732,16 +736,21 @@ Respon:
: "application/octet-stream"; : "application/octet-stream";
set.headers["Content-Type"] = mime; set.headers["Content-Type"] = mime;
set.headers["Content-Length"] = hasil.byteLength.toString();
return new Response(hasil); return new Response(hasil);
}, { }, {
query: t.Object({ query: t.Object({
fileName: t.String(), fileName: t.String(),
folder: t.String()
}), }),
detail: { detail: {
summary: "Gambar Pengaduan Warga", summary: "View Gambar",
description: `tool untuk mendapatkan gambar pengaduan warga`, description: "tool untuk mendapatkan gambar",
} }
}) })
; ;
export default PengaduanRoute export default PengaduanRoute