amalia/20-nov-25 #33

Merged
amaliadwiy merged 4 commits from amalia/20-nov-25 into main 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 {
ActionIcon,
Anchor,
Button,
Divider,
FileInput,
Flex,
Group,
Input,
@@ -10,18 +12,23 @@ import {
Stack,
Table,
Title,
Tooltip,
Tooltip
} from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react";
import { useState } from "react";
import useSWR from "swr";
import ModalFile from "./ModalFile";
import notification from "./notificationGlobal";
import _ from "lodash";
export default function DesaSetting() {
const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(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("/", () =>
apiFetch.api["configuration-desa"].list.get(),
);
@@ -39,7 +46,31 @@ export default function DesaSetting() {
async function handleEdit() {
try {
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) {
mutate();
close();
@@ -67,11 +98,8 @@ export default function DesaSetting() {
}
}
function chooseEdit({
data,
}: {
data: { id: string; value: string; name: string };
}) {
function chooseEdit({ data }: { data: { id: string; value: string; name: string }; }) {
setDataEdit(data);
open();
}
@@ -100,18 +128,35 @@ export default function DesaSetting() {
opened={opened}
onClose={close}
title={"Edit"}
centered
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</Input.Wrapper>
{
dataEdit.name == "TTD"
?
(
<Input.Wrapper label={dataEdit.name}>
<FileInput
clearable
placeholder="Upload TTD"
accept="image/*"
onChange={(e) => { setImg(e) }}
/>
</Input.Wrapper>
)
:
(
<Input.Wrapper label={dataEdit.name}>
<Input
value={dataEdit.value}
onChange={(e) =>
onValidation({ kat: "value", value: e.target.value })
}
/>
</Input.Wrapper>
)
}
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
@@ -119,7 +164,7 @@ export default function DesaSetting() {
<Button
variant="filled"
onClick={handleEdit}
disabled={btnDisable}
disabled={btnDisable || (dataEdit.name == "TTD" && !img)}
loading={btnLoading}
>
Simpan
@@ -127,6 +172,14 @@ export default function DesaSetting() {
</Group>
</Stack>
</Modal>
<ModalFile
open={openedPreview && !_.isEmpty(viewImg)}
onClose={() => setOpenedPreview(false)}
folder="syarat-dokumen"
fileName={viewImg}
/>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
@@ -147,7 +200,17 @@ export default function DesaSetting() {
{list?.map((v: any) => (
<Table.Tr key={v.id}>
<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>
<Tooltip label="Edit Setting">
<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 { useRef } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
import SKKelahiran from "./surat/SKKelahiran";
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
import SKKematian from "./surat/SKKematian";
import SKPenghasilan from "./surat/SKPenghasilan";
import SKTempatUsaha from "./surat/SKTempatUsaha";
import SKTidakMampu from "./surat/SKTidakMampu";
import SKUsaha from "./surat/SKUsaha";
import SKYatim from "./surat/SKYatimPiatu";
@@ -115,8 +120,17 @@ export default function ModalSurat({ open, onClose, surat }: { open: boolean, on
? <SKTidakMampu data={data.data} />
: data.data.surat.idCategory == "skyatimpiatu"
? <SKYatim data={data.data} />
: <></>
: data.data.surat.idCategory == "skdomisiliorganisasi"
? <SKDomisiliOrganisasi data={data.data} />
: data.data.surat.idCategory == "skbedabiodata"
? <SKBedaBiodataDiri data={data.data} />
: data.data.surat.idCategory == "sktempatusaha"
? <SKTempatUsaha data={data.data} />
: data.data.surat.idCategory == "skbelumkawin"
? <SKBelumKawin data={data.data} />
: data.data.surat.idCategory == "skkematian"
? <SKKematian data={data.data} />
: <></>
: <></>
}
</div>

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>
{/* DATA PEJABAT */}
<div style={{ marginLeft: "20px" }}>
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Alamat" value={data.setting.desaAlamat} />
@@ -32,7 +32,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div style={{ marginLeft: "20px" }}>
<div>
<Row label="Nama" value={getValue("nama")} />
<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" }}>
{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}
</div>
</div>

View File

@@ -1,11 +1,41 @@
import _ from "lodash";
import { useEffect, useState } from "react";
import notification from "../notificationGlobal";
export default function SKUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>("");
const getValue = (jenis: string) =>
_.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
);
const 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 (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
@@ -18,7 +48,7 @@ export default function SKUsaha({ data }: { data: any }) {
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<div style={{ textAlign: "center", margin: "15px 0" }}>
<b><u>SURAT KETERANGAN USAHA</u></b><br />
Nomor: {data.surat.noSurat}
</div>
@@ -102,13 +132,15 @@ export default function SKUsaha({ data }: { data: any }) {
</div>
{/* 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" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br /><br /><br />
{data.setting.perbekelNama} <br />
<br /><br />
<img src={viewImg} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>

View File

@@ -4,16 +4,10 @@ import KategoriPengaduan from "@/components/KategoriPengaduan";
import ProfileUser from "@/components/ProfileUser";
import UserSetting from "@/components/UserSetting";
import {
Button,
Card,
Container,
Divider,
Flex,
Grid,
NavLink,
Stack,
Table,
Title,
NavLink
} from "@mantine/core";
import {
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> {
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`);
export async function catFile(config: Config, folder: string, fileName: string): Promise<ArrayBuffer> {
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`);
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> {
@@ -159,7 +163,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
const text = await res.text();
if (!res.ok) throw new Error(`Upload failed: ${text}`);
if (!res.ok) return 'gagal'
return `✅ Uploaded ${file.name} successfully`;
}

View File

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