Compare commits
208 Commits
amalia/20-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d6abc163fb | |||
| 9c08980bf1 | |||
| a2af3fbe36 | |||
| ec8722ffba | |||
| b0752dac8d | |||
| a8d3a3a9ff | |||
| f86703e7d1 | |||
| d8bb33cc93 | |||
| 5807e98069 | |||
| 59b4f1d73f | |||
| 8bd552ac22 | |||
| 5d48d06513 | |||
| 3da163ea1d | |||
| 57e4f34eb6 | |||
| 3348cbe8e3 | |||
| 727984a076 | |||
| a7a0ad7e37 | |||
| 9fed41cbe8 | |||
| fc387fe8e6 | |||
| 80df579499 | |||
| 5bbbc15c27 | |||
| 82765f6ef0 | |||
| e8b5720118 | |||
| 01334ec573 | |||
| 98ad9b0d72 | |||
| c0471f47f3 | |||
| 3d641d2035 | |||
| 694115dbfb | |||
| 7de5078868 | |||
| 7a3faa5719 | |||
| ea5072d9ab | |||
| e8bb4f5a41 | |||
| d63bf024d3 | |||
| 46f7dbf7bb | |||
| 1adea29990 | |||
| 2a5b6e7b7c | |||
| 2117612337 | |||
| 8f33ec2ffa | |||
| 411f61ec15 | |||
| 476319945e | |||
| 8480cec6ae | |||
| 4ca5e4c4f3 | |||
| 75758bcbe6 | |||
| 2d336ea467 | |||
| 112e931bad | |||
| 487395bdb3 | |||
| 3944e1ee82 | |||
| a9b34547f0 | |||
| 211aac3d5f | |||
| 73a2a4367c | |||
| a01f394e43 | |||
| 7dde0a4eb9 | |||
| 6debbf8c64 | |||
| c074e2bc0a | |||
| bab832b87f | |||
| 8bafb88086 | |||
| 777f2c04f1 | |||
| a81f6c4255 | |||
| 0f1b0196e7 | |||
| da86f5f10a | |||
| 91a3dfdb5d | |||
| 3904527c2a | |||
| ff0413be5a | |||
| fda2b0977a | |||
| 55fbf4836d | |||
| c897063eb5 | |||
| 84161db7f2 | |||
| a1766538b2 | |||
| 7c6e4ac9eb | |||
| d8cf7833a9 | |||
| a13e51a724 | |||
| c585d2481d | |||
| 18b541116a | |||
| e2d523d535 | |||
| 18d3b40700 | |||
| 719ba0186b | |||
| baf00b1ba8 | |||
| 026f74cc44 | |||
| dfc5c9144f | |||
| 2badded9c3 | |||
| f91e2dd87b | |||
| 6fb6ab9750 | |||
| 5524f72712 | |||
| 11a78d7371 | |||
| 93de9ebe9a | |||
| 3baba059ab | |||
| 12eb71b96d | |||
| dcd072034c | |||
| 4cef5148ad | |||
| ee27813da7 | |||
| 7a6ea5b13d | |||
| 29f6ecfd23 | |||
| 8d28e7ae6a | |||
| d6882d4b3a | |||
| b7f0f4da48 | |||
| 286c989bcf | |||
| 031e408640 | |||
| c797d1fc46 | |||
| 6f6905a414 | |||
| 91e5f6a77e | |||
| 3f567b57b2 | |||
| c98cfd21ce | |||
| fdf7b0a13f | |||
| d76a702d2d | |||
| 6bc6a9d357 | |||
| dee32b8cfd | |||
| ff0b0273bf | |||
| f8dcffa9c5 | |||
| 20e3056e04 | |||
| 84c9f405d6 | |||
| 22597c0159 | |||
| 0f9af404e1 | |||
| 676edaa22b | |||
| 7f6f495eaa | |||
| b5af41b07d | |||
| 6428f5084e | |||
| bfc292ec6c | |||
| 3b71976863 | |||
| 5680466c98 | |||
| 270f3687a3 | |||
| f5cc45937c | |||
| 5b4164b151 | |||
| 225c58b346 | |||
| b8b3aed86e | |||
| fc530399dd | |||
| 281e34ea69 | |||
| f928fc504f | |||
| 4fb98d0480 | |||
| bfb33e2105 | |||
| 2579714000 | |||
| d69189cf7d | |||
| 20e24a03aa | |||
| c256f4b729 | |||
| 9430ad3728 | |||
| c6c3ba95f8 | |||
|
|
3c58230c3a | ||
| d22b4b973f | |||
| 700fbe3bd7 | |||
| 9b7a61e134 | |||
| 2d376663bb | |||
| 7c669f3494 | |||
| 0ed9dc6ddd | |||
| b9984c6337 | |||
| 48a7d43713 | |||
| 6a52d10faa | |||
| 2b94684570 | |||
| cc7c8eb704 | |||
| 4996da4189 | |||
| c32cce838f | |||
| 5af9b720ca | |||
| 35618bb438 | |||
| eee8aadb1a | |||
| 1f95c7d7d8 | |||
| 23df516aad | |||
| 70175cedc6 | |||
| f52f5f87ca | |||
| ea17357638 | |||
| 9c7c9d8595 | |||
| 5ecf264155 | |||
| 4cc28c4311 | |||
| c25e5eeba0 | |||
| 574603e290 | |||
| ba76eb5e59 | |||
| 6ae83ec19c | |||
| ba0414a99c | |||
| 4dd66dbd9a | |||
| fa201274d4 | |||
| dff1aa61c5 | |||
| 90b8fdf573 | |||
| 239b1bdc1b | |||
| f99da3b2a6 | |||
| 3866f71e2d | |||
| d57ff92aa9 | |||
| f901cff3b1 | |||
| fe3ebf4bd3 | |||
| 075cb12417 | |||
| cd7e602254 | |||
| f9b84f89eb | |||
| cca1840922 | |||
| c622565bb7 | |||
| d7e77da16a | |||
| decf6dd972 | |||
| 5b72f1a9cc | |||
| acb5ae7cd1 | |||
| 6bdb0246c9 | |||
| 3f68f212cd | |||
|
|
e0236a907f | ||
| e4189d40e9 | |||
| 94e7604afb | |||
| a253d40d19 | |||
| 26c7357ca3 | |||
| 15c5140902 | |||
| c5b1452955 | |||
| e1431fafb2 | |||
| ad7b40523c | |||
| 10db3f922e | |||
| 0a3afb7b9c | |||
| c72ef5a755 | |||
| 4c047324bc | |||
| e4a03e3a8f | |||
| 41af733c6e | |||
|
|
436016641b | ||
|
|
6fbddb3806 | ||
|
|
eb1eaa11ea | ||
|
|
54ae3b746d | ||
|
|
7781882531 | ||
| 558d8aaafb | |||
| d7267abdb3 |
146
bak/ModalSurat.tsx.txt
Normal file
146
bak/ModalSurat.tsx.txt
Normal file
@@ -0,0 +1,146 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { ActionIcon, Flex, Modal } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconDownload, IconX } from "@tabler/icons-react";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
import { useRef } from "react";
|
||||
import useSWR from "swr";
|
||||
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
|
||||
import SKBelumKawin from "./surat/SKBelumKawin";
|
||||
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
|
||||
import SKKelahiran from "./surat/SKKelahiran";
|
||||
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
|
||||
import SKKematian from "./surat/SKKematian";
|
||||
import SKPenghasilan from "./surat/SKPenghasilan";
|
||||
import SKTempatUsaha from "./surat/SKTempatUsaha";
|
||||
import SKTidakMampu from "./surat/SKTidakMampu";
|
||||
import SKUsaha from "./surat/SKUsaha";
|
||||
import SKYatim from "./surat/SKYatimPiatu";
|
||||
|
||||
export default function ModalSurat({
|
||||
open,
|
||||
onClose,
|
||||
surat,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
surat: string;
|
||||
}) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
color: "#000",
|
||||
fontSize: "14px",
|
||||
fontFamily: "Times New Roman",
|
||||
};
|
||||
const hiddenRef = useRef<any>(null);
|
||||
const { data, mutate, isLoading } = useSWR("surat", () =>
|
||||
apiFetch.api.surat.detail.get({
|
||||
query: {
|
||||
id: surat,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
const downloadPDF = async () => {
|
||||
const element = hiddenRef.current;
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL("image/jpeg", 1.0);
|
||||
|
||||
const pdf = new jsPDF("p", "mm", "a4");
|
||||
const pageWidth = 210; // A4 width mm
|
||||
const pageHeight = 297; // A4 height mm
|
||||
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
|
||||
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
||||
|
||||
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onClose()}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
styles={{
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px 16px",
|
||||
},
|
||||
title: {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
|
||||
|
||||
<Flex gap={8}>
|
||||
<ActionIcon size={32} variant="default">
|
||||
<IconDownload size={20} onClick={downloadPDF} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon size={32} variant="default" onClick={onClose}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{data && data.data ? (
|
||||
data.data.surat.idCategory == "skusaha" ? (
|
||||
<SKUsaha data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelahiran" ? (
|
||||
<SKKelahiran data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelakuanbaik" ? (
|
||||
<SKKelakuanBaik data={data.data} />
|
||||
) : data.data.surat.idCategory == "skpenghasilan" ? (
|
||||
<SKPenghasilan data={data.data} />
|
||||
) : data.data.surat.idCategory == "sktidakmampu" ? (
|
||||
<SKTidakMampu data={data.data} />
|
||||
) : data.data.surat.idCategory == "skyatimpiatu" ? (
|
||||
<SKYatim data={data.data} />
|
||||
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
|
||||
<SKDomisiliOrganisasi data={data.data} />
|
||||
) : data.data.surat.idCategory == "skbedabiodata" ? (
|
||||
<SKBedaBiodataDiri data={data.data} />
|
||||
) : data.data.surat.idCategory == "sktempatusaha" ? (
|
||||
<SKTempatUsaha data={data.data} />
|
||||
) : data.data.surat.idCategory == "skbelumkawin" ? (
|
||||
<SKBelumKawin data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkematian" ? (
|
||||
<SKKematian data={data.data} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
310
bak/listPermission.json.txt
Normal file
310
bak/listPermission.json.txt
Normal file
@@ -0,0 +1,310 @@
|
||||
{
|
||||
"menus": [
|
||||
{
|
||||
"key": "dashboard",
|
||||
"label": "Dashboard",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "dashboard.view",
|
||||
"label": "Melihat Dashboard",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan",
|
||||
"label": "Pengaduan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.antrian",
|
||||
"label": "Detail pengaduan dengan status antrian",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.antrian.tolak",
|
||||
"label": "Menolak pengaduan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.antrian.terima",
|
||||
"label": "Menerima pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.diterima",
|
||||
"label": "Detail pengaduan dengan status diterima",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.diterima.dikerjakan",
|
||||
"label": "Menegerjakan pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.dikerjakan",
|
||||
"label": "Detail pengaduan dengan status dikerjakan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.dikerjakan.selesai",
|
||||
"label": "Menyelesaikan pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pelayanan",
|
||||
"label": "Pelayanan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.antrian",
|
||||
"label": "Detail pelayanan dengan status antrian",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.antrian.tolak",
|
||||
"label": "Menolak pelayanan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.antrian.terima",
|
||||
"label": "Menerima pelayanan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.diterima",
|
||||
"label": "Detail pelayanan dengan status diterima",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.diterima.tolak",
|
||||
"label": "Menolak pelayanan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.diterima.setujui",
|
||||
"label": "Menyetujui pelayanan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "warga",
|
||||
"label": "Warga",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "warga.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting",
|
||||
"label": "Setting",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.profile",
|
||||
"label": "Profile",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.profile.view",
|
||||
"label": "View",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.profile.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.profile.password",
|
||||
"label": "Ubah Password",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.user",
|
||||
"label": "User",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.user.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role",
|
||||
"label": "User Role",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.user_role.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan",
|
||||
"label": "Kategori Pengaduan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan",
|
||||
"label": "Kategori Pelayanan Surat",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.detail",
|
||||
"label": "View Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.desa",
|
||||
"label": "Desa",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.desa.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.desa.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "api_key",
|
||||
"label": "API Key",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "api_key.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "credential",
|
||||
"label": "Credential",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "credential.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
250
bak/mcp_route.ts.txt
Normal file
250
bak/mcp_route.ts.txt
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Elysia } from "elysia";
|
||||
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||
|
||||
var tools = [] as any[];
|
||||
const OPENAPI_URL = process.env.BUN_PUBLIC_BASE_URL + "/docs/json";
|
||||
const FILTER_TAG = "mcp";
|
||||
|
||||
if (!process.env.BUN_PUBLIC_BASE_URL) {
|
||||
throw new Error("BUN_PUBLIC_BASE_URL environment variable is not set");
|
||||
}
|
||||
|
||||
// =====================
|
||||
// MCP Protocol Types
|
||||
// =====================
|
||||
type JSONRPCRequest = {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: any;
|
||||
};
|
||||
|
||||
type JSONRPCResponse = {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
result?: any;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
};
|
||||
|
||||
// =====================
|
||||
// Tool Executor
|
||||
// =====================
|
||||
export async function executeTool(
|
||||
tool: any,
|
||||
args: Record<string, any> = {},
|
||||
baseUrl: string
|
||||
) {
|
||||
const x = tool["x-props"] || {};
|
||||
|
||||
const method = (x.method || "GET").toUpperCase();
|
||||
const path = x.path || `/${tool.name}`;
|
||||
const url = `${baseUrl}${path}`;
|
||||
|
||||
const opts: RequestInit = {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
};
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
opts.body = JSON.stringify(args || {});
|
||||
}
|
||||
|
||||
const res = await fetch(url, opts);
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
const data = contentType.includes("application/json")
|
||||
? await res.json()
|
||||
: await res.text();
|
||||
|
||||
return {
|
||||
success: res.ok,
|
||||
status: res.status,
|
||||
method,
|
||||
path,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
// =====================
|
||||
// MCP Handler (Async)
|
||||
// =====================
|
||||
async function handleMCPRequestAsync(
|
||||
request: JSONRPCRequest
|
||||
): Promise<JSONRPCResponse> {
|
||||
const { id, method, params } = request;
|
||||
|
||||
switch (method) {
|
||||
case "initialize":
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
||||
},
|
||||
};
|
||||
|
||||
case "tools/list":
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||
name,
|
||||
description,
|
||||
inputSchema,
|
||||
"x-props": x,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
case "tools/call": {
|
||||
const toolName = params?.name;
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const baseUrl =
|
||||
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
||||
const data = result.data.data;
|
||||
const isObject = typeof data === "object" && data !== null;
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
isObject
|
||||
? { type: "json", data: data }
|
||||
: { type: "text", text: JSON.stringify(data || result.data || result) },
|
||||
],
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32603, message: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case "ping":
|
||||
return { jsonrpc: "2.0", id, result: {} };
|
||||
|
||||
default:
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Method '${method}' not found` },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Elysia MCP Server
|
||||
// =====================
|
||||
export const MCPRoute = new Elysia({
|
||||
tags: ["MCP Server"]
|
||||
})
|
||||
.post("/mcp", async ({ request, set }) => {
|
||||
if (!tools.length) {
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Content-Type"] = "application/json";
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (!Array.isArray(body)) {
|
||||
const res = await handleMCPRequestAsync(body);
|
||||
return res;
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
body.map((req) => handleMCPRequestAsync(req))
|
||||
);
|
||||
return results;
|
||||
} catch (error: any) {
|
||||
set.status = 400;
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: "Parse error",
|
||||
data: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
// Tools list (debug)
|
||||
.get("/mcp/tools", async ({ set }) => {
|
||||
if (!tools.length) {
|
||||
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return {
|
||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||
name,
|
||||
description,
|
||||
inputSchema,
|
||||
"x-props": x,
|
||||
})),
|
||||
};
|
||||
})
|
||||
|
||||
// MCP status
|
||||
.get("/mcp/status", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "active", timestamp: Date.now() };
|
||||
})
|
||||
|
||||
// Health check
|
||||
.get("/health", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
||||
})
|
||||
.get("/mcp/init", async ({ set }) => {
|
||||
|
||||
const _tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
tools = _tools;
|
||||
return {
|
||||
success: true,
|
||||
message: "MCP initialized",
|
||||
tools: tools.length,
|
||||
};
|
||||
})
|
||||
|
||||
// CORS
|
||||
.options("/mcp", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] =
|
||||
"Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
})
|
||||
.options("/mcp/tools", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] =
|
||||
"Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
});
|
||||
381
bak/mcp_tool_convert.ts.txt
Normal file
381
bak/mcp_tool_convert.ts.txt
Normal file
@@ -0,0 +1,381 @@
|
||||
import _ from "lodash";
|
||||
|
||||
interface McpTool {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: any;
|
||||
"x-props": {
|
||||
method: string;
|
||||
path: string;
|
||||
operationId?: string;
|
||||
tag?: string;
|
||||
deprecated?: boolean;
|
||||
summary?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions.
|
||||
*/
|
||||
export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): McpTool[] {
|
||||
const tools: McpTool[] = [];
|
||||
|
||||
if (!openApiJson || typeof openApiJson !== "object") {
|
||||
console.warn("Invalid OpenAPI JSON");
|
||||
return tools;
|
||||
}
|
||||
|
||||
const paths = openApiJson.paths || {};
|
||||
|
||||
if (Object.keys(paths).length === 0) {
|
||||
console.warn("No paths found in OpenAPI spec");
|
||||
return tools;
|
||||
}
|
||||
|
||||
for (const [path, methods] of Object.entries(paths)) {
|
||||
if (!path || typeof path !== "string") continue;
|
||||
if (path.startsWith("/mcp")) continue;
|
||||
|
||||
if (!methods || typeof methods !== "object") continue;
|
||||
|
||||
for (const [method, operation] of Object.entries<any>(methods)) {
|
||||
const validMethods = ["get", "post", "put", "delete", "patch", "head", "options"];
|
||||
if (!validMethods.includes(method.toLowerCase())) continue;
|
||||
|
||||
if (!operation || typeof operation !== "object") continue;
|
||||
|
||||
const tags: string[] = Array.isArray(operation.tags) ? operation.tags : [];
|
||||
|
||||
if (!tags.length || !tags.some(t =>
|
||||
typeof t === "string" && t.toLowerCase().includes(filterTag)
|
||||
)) continue;
|
||||
|
||||
try {
|
||||
const tool = createToolFromOperation(path, method, operation, tags);
|
||||
if (tool) {
|
||||
tools.push(tool);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error creating tool for ${method.toUpperCase()} ${path}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat MCP tool dari operation OpenAPI
|
||||
*/
|
||||
function createToolFromOperation(
|
||||
path: string,
|
||||
method: string,
|
||||
operation: any,
|
||||
tags: string[]
|
||||
): McpTool | null {
|
||||
try {
|
||||
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||
const name = cleanToolName(rawName);
|
||||
|
||||
if (!name || name === "unnamed_tool") {
|
||||
console.warn(`Invalid tool name for ${method} ${path}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const description =
|
||||
operation.description ||
|
||||
operation.summary ||
|
||||
`Execute ${method.toUpperCase()} ${path}`;
|
||||
|
||||
// ✅ Extract schema berdasarkan method
|
||||
let schema;
|
||||
if (method.toLowerCase() === "get") {
|
||||
// ✅ Untuk GET, ambil dari parameters (query/path)
|
||||
schema = extractParametersSchema(operation.parameters || []);
|
||||
} else {
|
||||
// ✅ Untuk POST/PUT/etc, ambil dari requestBody
|
||||
schema = extractRequestBodySchema(operation);
|
||||
}
|
||||
|
||||
const inputSchema = createInputSchema(schema);
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
"x-props": {
|
||||
method: method.toUpperCase(),
|
||||
path,
|
||||
operationId: operation.operationId,
|
||||
tag: tags[0],
|
||||
deprecated: operation.deprecated || false,
|
||||
summary: operation.summary,
|
||||
},
|
||||
inputSchema,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to create tool from operation:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract schema dari parameters (untuk GET requests)
|
||||
*/
|
||||
function extractParametersSchema(parameters: any[]): any {
|
||||
if (!Array.isArray(parameters) || parameters.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const properties: any = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const param of parameters) {
|
||||
if (!param || typeof param !== "object") continue;
|
||||
|
||||
// ✅ Support path, query, dan header parameters
|
||||
if (["path", "query", "header"].includes(param.in)) {
|
||||
const paramName = param.name;
|
||||
if (!paramName || typeof paramName !== "string") continue;
|
||||
|
||||
properties[paramName] = {
|
||||
type: param.schema?.type || "string",
|
||||
description: param.description || `${param.in} parameter: ${paramName}`,
|
||||
};
|
||||
|
||||
// ✅ Copy field tambahan dari schema
|
||||
if (param.schema) {
|
||||
const allowedFields = ["examples", "example", "default", "enum", "pattern", "minLength", "maxLength", "minimum", "maximum", "format"];
|
||||
for (const field of allowedFields) {
|
||||
if (param.schema[field] !== undefined) {
|
||||
properties[paramName][field] = param.schema[field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (param.required === true) {
|
||||
required.push(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(properties).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
required,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract schema dari requestBody (untuk POST/PUT/etc requests)
|
||||
*/
|
||||
function extractRequestBodySchema(operation: any): any {
|
||||
if (!operation.requestBody?.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = operation.requestBody.content;
|
||||
|
||||
const contentTypes = [
|
||||
"application/json",
|
||||
"multipart/form-data",
|
||||
"application/x-www-form-urlencoded",
|
||||
"text/plain",
|
||||
];
|
||||
|
||||
for (const contentType of contentTypes) {
|
||||
if (content[contentType]?.schema) {
|
||||
return content[contentType].schema;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [_, value] of Object.entries<any>(content)) {
|
||||
if (value?.schema) {
|
||||
return value.schema;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Buat input schema yang valid untuk MCP
|
||||
*/
|
||||
function createInputSchema(schema: any): any {
|
||||
const defaultSchema = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
};
|
||||
|
||||
if (!schema || typeof schema !== "object") {
|
||||
return defaultSchema;
|
||||
}
|
||||
|
||||
try {
|
||||
const properties: any = {};
|
||||
const required: string[] = [];
|
||||
const originalRequired = Array.isArray(schema.required) ? schema.required : [];
|
||||
|
||||
if (schema.properties && typeof schema.properties === "object") {
|
||||
for (const [key, prop] of Object.entries<any>(schema.properties)) {
|
||||
if (!key || typeof key !== "string") continue;
|
||||
|
||||
try {
|
||||
const cleanProp = cleanProperty(prop);
|
||||
if (cleanProp) {
|
||||
properties[key] = cleanProp;
|
||||
|
||||
// ✅ PERBAIKAN: Check optional flag dengan benar
|
||||
const isOptional = prop?.optional === true || prop?.optional === "true";
|
||||
const isInRequired = originalRequired.includes(key);
|
||||
|
||||
// ✅ Hanya masukkan ke required jika memang required DAN bukan optional
|
||||
if (isInRequired && !isOptional) {
|
||||
required.push(key);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error cleaning property ${key}:`, error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "object",
|
||||
properties,
|
||||
required,
|
||||
additionalProperties: false,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating input schema:", error);
|
||||
return defaultSchema;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan property dari field custom
|
||||
*/
|
||||
function cleanProperty(prop: any): any | null {
|
||||
if (!prop || typeof prop !== "object") {
|
||||
return { type: "string" };
|
||||
}
|
||||
|
||||
try {
|
||||
const cleaned: any = {
|
||||
type: prop.type || "string",
|
||||
};
|
||||
|
||||
const allowedFields = [
|
||||
"description",
|
||||
"examples",
|
||||
"example",
|
||||
"default",
|
||||
"enum",
|
||||
"pattern",
|
||||
"minLength",
|
||||
"maxLength",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"format",
|
||||
"multipleOf",
|
||||
"exclusiveMinimum",
|
||||
"exclusiveMaximum",
|
||||
];
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (prop[field] !== undefined && prop[field] !== null) {
|
||||
cleaned[field] = prop[field];
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.properties && typeof prop.properties === "object") {
|
||||
cleaned.properties = {};
|
||||
for (const [key, value] of Object.entries(prop.properties)) {
|
||||
const cleanedNested = cleanProperty(value);
|
||||
if (cleanedNested) {
|
||||
cleaned.properties[key] = cleanedNested;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(prop.required)) {
|
||||
cleaned.required = prop.required.filter((r: any) => typeof r === "string");
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.items) {
|
||||
cleaned.items = cleanProperty(prop.items);
|
||||
}
|
||||
|
||||
if (Array.isArray(prop.oneOf)) {
|
||||
cleaned.oneOf = prop.oneOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(prop.anyOf)) {
|
||||
cleaned.anyOf = prop.anyOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
if (Array.isArray(prop.allOf)) {
|
||||
cleaned.allOf = prop.allOf.map(cleanProperty).filter(Boolean);
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning property:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bersihkan nama tool
|
||||
*/
|
||||
function cleanToolName(name: string): string {
|
||||
if (!name || typeof name !== "string") {
|
||||
return "unnamed_tool";
|
||||
}
|
||||
|
||||
try {
|
||||
return name
|
||||
.replace(/[{}]/g, "")
|
||||
.replace(/[^a-zA-Z0-9_]/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_|_$/g, "")
|
||||
.replace(/^(get|post|put|delete|patch|api)_/i, "")
|
||||
.replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "")
|
||||
.replace(/(^_|_$)/g, "")
|
||||
|| "unnamed_tool";
|
||||
} catch (error) {
|
||||
console.error("Error cleaning tool name:", error);
|
||||
return "unnamed_tool";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
|
||||
*/
|
||||
export async function getMcpTools(url: string, filterTag: string): Promise<McpTool[]> {
|
||||
try {
|
||||
|
||||
console.log(`Fetching OpenAPI spec from: ${url}`);
|
||||
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const openApiJson = await response.json();
|
||||
const tools = convertOpenApiToMcpTools(openApiJson, filterTag);
|
||||
|
||||
console.log(`✅ Successfully generated ${tools.length} MCP tools`);
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MCP tools:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
24
bun.lock
24
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "jenna-mcp",
|
||||
@@ -21,6 +22,9 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"add": "^2.0.6",
|
||||
"dayjs": "^1.11.19",
|
||||
"echarts": "^6.0.0",
|
||||
"echarts-for-react": "^3.0.5",
|
||||
"elysia": "^1.4.15",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
@@ -289,6 +293,10 @@
|
||||
|
||||
"ecc-jsbn": ["ecc-jsbn@0.1.2", "", { "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" } }, "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw=="],
|
||||
|
||||
"echarts": ["echarts@6.0.0", "", { "dependencies": { "tslib": "2.3.0", "zrender": "6.0.0" } }, "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ=="],
|
||||
|
||||
"echarts-for-react": ["echarts-for-react@3.0.5", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "size-sensor": "^1.0.1" }, "peerDependencies": { "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", "react": "^15.0.0 || >=16.0.0" } }, "sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg=="],
|
||||
|
||||
"editor": ["editor@1.0.0", "", {}, "sha512-SoRmbGStwNYHgKfjOrX2L0mUvp9bUVv0uPppZSOMAntEbcFtoC3MKF5b3T6HQPXKIV+QGY3xPO3JK5it5lVkuw=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
@@ -645,6 +653,8 @@
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"size-sensor": ["size-sensor@1.0.2", "", {}, "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="],
|
||||
@@ -687,7 +697,7 @@
|
||||
|
||||
"tough-cookie": ["tough-cookie@2.5.0", "", { "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" } }, "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
"tslib": ["tslib@2.3.0", "", {}, "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="],
|
||||
|
||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||
|
||||
@@ -743,6 +753,8 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
|
||||
|
||||
"zrender": ["zrender@6.0.0", "", { "dependencies": { "tslib": "2.3.0" } }, "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg=="],
|
||||
|
||||
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.7", "", { "dependencies": { "@scalar/openapi-types": "0.2.0", "@unhead/schema": "^1.11.11", "nanoid": "^5.1.5", "type-fest": "^4.20.0", "zod": "^3.23.8" } }, "sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw=="],
|
||||
|
||||
"body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
@@ -769,12 +781,22 @@
|
||||
|
||||
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"request/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"request/qs": ["qs@6.5.3", "", {}, "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="],
|
||||
|
||||
"request/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
|
||||
|
||||
"use-callback-ref/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.2.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
|
||||
|
||||
3
kirim.sh
Normal file
3
kirim.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
curl -X POST https://cld-dkr-prod-jenna-mcp.wibudev.com/api/pengaduan/upload-file-form-data \
|
||||
-H "Accept: application/json" \
|
||||
-F "file=@image.png"
|
||||
@@ -28,6 +28,9 @@
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"add": "^2.0.6",
|
||||
"dayjs": "^1.11.19",
|
||||
"echarts": "^6.0.0",
|
||||
"echarts-for-react": "^3.0.5",
|
||||
"elysia": "^1.4.15",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^3.0.3",
|
||||
|
||||
@@ -9,11 +9,13 @@ datasource db {
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
User User[]
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
permissions Json?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
User User[]
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -108,7 +110,8 @@ model CategoryPelayanan {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
syaratDokumen Json[]
|
||||
dataText String[]
|
||||
dataText String[] @default([])
|
||||
dataPelengkap Json[] @default([])
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -184,6 +187,7 @@ model SuratPelayanan {
|
||||
Warga Warga @relation(fields: [idWarga], references: [id])
|
||||
idWarga String
|
||||
noSurat String
|
||||
file String?
|
||||
dateExpired DateTime? @db.Date
|
||||
status Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { categoryPelayananSurat } from "@/lib/categoryPelayananSurat";
|
||||
import { confDesa } from "@/lib/configurationDesa";
|
||||
import permissionConfig from "@/lib/listPermission.json"; // JSON yang kita buat
|
||||
import { prisma } from "@/server/lib/prisma";
|
||||
|
||||
const category = [
|
||||
@@ -29,14 +30,6 @@ const role = [
|
||||
{
|
||||
id: "developer",
|
||||
name: "developer"
|
||||
},
|
||||
{
|
||||
id: "admin",
|
||||
name: "admin"
|
||||
},
|
||||
{
|
||||
id: "pelaksana",
|
||||
name: "pelaksana"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -51,11 +44,30 @@ const user = [
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const allKeys: string[] = [];
|
||||
|
||||
function collectKeys(items: any[]) {
|
||||
items.forEach((item) => {
|
||||
allKeys.push(item.key);
|
||||
if (item.children) collectKeys(item.children);
|
||||
});
|
||||
}
|
||||
|
||||
collectKeys(permissionConfig.menus);
|
||||
|
||||
|
||||
for (const r of role) {
|
||||
await prisma.role.upsert({
|
||||
where: { id: r.id },
|
||||
create: r,
|
||||
update: r
|
||||
create: {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
permissions: allKeys as any,
|
||||
},
|
||||
update: {
|
||||
name: r.name,
|
||||
permissions: allKeys as any,
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`✅ Role ${r.name} seeded successfully`)
|
||||
|
||||
@@ -10,10 +10,12 @@ import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
|
||||
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
|
||||
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
|
||||
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
|
||||
import UpdateDataSurat from "./pages/darmasaba/update_data_surat";
|
||||
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
|
||||
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
|
||||
import Surat from "./pages/darmasaba/surat";
|
||||
import Home from "./pages/Home";
|
||||
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||
@@ -68,6 +70,10 @@ export default function AppRoutes() {
|
||||
path="/darmasaba/surat-keterangan-domisili-organisasi"
|
||||
element={<FormSuratKeteranganDomisiliOrganisasi />}
|
||||
/>
|
||||
<Route
|
||||
path="/darmasaba/update-data-surat"
|
||||
element={<UpdateDataSurat />}
|
||||
/>
|
||||
<Route
|
||||
path="/darmasaba/surat-keterangan-belum-kawin"
|
||||
element={<FormSuratKeteranganBelumKawin />}
|
||||
@@ -84,6 +90,7 @@ export default function AppRoutes() {
|
||||
path="/darmasaba/surat-keterangan-kelakuan-baik"
|
||||
element={<FormSuratKeteranganKelakuanBaik />}
|
||||
/>
|
||||
<Route path="/darmasaba/surat" element={<Surat />} />
|
||||
</Route>
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ const clientRoutes = {
|
||||
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
|
||||
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
|
||||
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
|
||||
"/darmasaba/update-data-surat": "/darmasaba/update-data-surat",
|
||||
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
|
||||
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
|
||||
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
|
||||
"/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik",
|
||||
"/darmasaba/surat": "/darmasaba/surat",
|
||||
"/": "/",
|
||||
"/scr": "/scr",
|
||||
"/scr/dashboard": "/scr/dashboard",
|
||||
|
||||
44
src/components/BreadCrumbs.tsx
Normal file
44
src/components/BreadCrumbs.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ActionIcon, Anchor, Breadcrumbs, Card, Group } from "@mantine/core";
|
||||
import { IconChevronLeft } from "@tabler/icons-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function BreadCrumbs({ dataLink, back, linkBack }: { dataLink: { title: string, link: string, active: boolean }[], back?: boolean, linkBack?: string }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="md"
|
||||
p="sm"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
{
|
||||
back &&
|
||||
<ActionIcon variant="outline" aria-label="Settings" radius={"lg"} onClick={() => window.history.back()}>
|
||||
<IconChevronLeft size={20} stroke={1.5} />
|
||||
</ActionIcon>
|
||||
}
|
||||
<Breadcrumbs>
|
||||
{
|
||||
dataLink.map((item, index) => (
|
||||
<Anchor
|
||||
c={item.active ? "gray.0" : "gray.5"}
|
||||
onClick={() => item.active || item.link == "#" ? null : navigate(item.link)}
|
||||
key={index}
|
||||
>
|
||||
{item.title}
|
||||
</Anchor>
|
||||
))
|
||||
}
|
||||
</Breadcrumbs>
|
||||
</Group>
|
||||
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
101
src/components/DashboardCountData.tsx
Normal file
101
src/components/DashboardCountData.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconFileCertificate,
|
||||
IconMessageReport,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function DashboardCountData() {
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.dashboard.count.get(),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconMessageReport size={28} />}
|
||||
label="Pengaduan Hari Ini"
|
||||
value={String(data?.data?.pengaduan?.today)}
|
||||
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
|
||||
color={"gray"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconFileCertificate size={28} />}
|
||||
label="Pengajuan Surat Hari Ini"
|
||||
value={String(data?.data?.pelayanan?.today)}
|
||||
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
|
||||
color="gray"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconUsers size={28} />}
|
||||
label="Warga"
|
||||
value={String(data?.data?.warga)}
|
||||
color="blue"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
change,
|
||||
color,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="md"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
|
||||
}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Group gap={6}>
|
||||
{icon}
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Text fw={600} size="xl" c="gray.0">
|
||||
{value}
|
||||
</Text>
|
||||
{change && (
|
||||
<Text size="sm" c={color}>
|
||||
{change}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
77
src/components/DashboardGrafik.tsx
Normal file
77
src/components/DashboardGrafik.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Card, Divider, Flex, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconChartBar } from "@tabler/icons-react";
|
||||
import type { EChartsOption } from "echarts";
|
||||
import EChartsReact from "echarts-for-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function DashboardGrafik() {
|
||||
const [options, setOptions] = useState<EChartsOption>({});
|
||||
const { data, mutate, isLoading } = useSWR("grafik-dashboard", async () => {
|
||||
return apiFetch.api.dashboard.grafik.get().then((res) => res.data);
|
||||
});
|
||||
|
||||
const loadData = () => {
|
||||
if (!data) return;
|
||||
const option: EChartsOption = {
|
||||
darkMode: true,
|
||||
animation: true,
|
||||
legend: {
|
||||
textStyle: { color: "#fff" },
|
||||
},
|
||||
tooltip: {},
|
||||
dataset: {
|
||||
dimensions: data.dimensions,
|
||||
source: data.source,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
minInterval: 1,
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
color: ["#1abc9c", "#10816aff"],
|
||||
series: [{ type: "bar" }, { type: "bar" }],
|
||||
};
|
||||
|
||||
setOptions(option);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) loadData();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Flex direction={"column"}>
|
||||
<Title order={4} c="gray.0">
|
||||
Grafik Pengaduan dan Pelayanan Surat
|
||||
</Title>
|
||||
<Text size="sm">7 Hari Terakhir</Text>
|
||||
</Flex>
|
||||
<IconChartBar size={20} color="gray" />
|
||||
</Flex>
|
||||
<Divider my="xs" />
|
||||
<Stack gap="sm">
|
||||
<EChartsReact style={{ height: 400 }} option={options} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
210
src/components/DashboardLastData.tsx
Normal file
210
src/components/DashboardLastData.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function DashboardLastData() {
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("last-update", async () => {
|
||||
const res = await apiFetch.api.dashboard["last-update"].get();
|
||||
return res.data;
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex justify="flex-start" gap="md">
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
w={"50%"}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Flex
|
||||
align="center"
|
||||
pb={"sm"}
|
||||
justify="space-between"
|
||||
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
|
||||
>
|
||||
<Title order={4} c="gray.0">
|
||||
Last update pengaduan
|
||||
</Title>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
radius="md"
|
||||
onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Flex>
|
||||
<Stack gap="sm" mt="md" align="stretch" justify="center">
|
||||
{data &&
|
||||
Array.isArray(data.pengaduan) &&
|
||||
data.pengaduan.length > 0 ? (
|
||||
data.pengaduan.map((item: any, index: number) => (
|
||||
<PengaduanSection
|
||||
key={index}
|
||||
id={item.id}
|
||||
nomer={item.noPengaduan}
|
||||
judul={item.title}
|
||||
status={item.status}
|
||||
updated={item.updatedAt}
|
||||
kategori="pengaduan"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text c="dimmed" ta={"center"}>
|
||||
Tidak ada data
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
w={"50%"}
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Flex
|
||||
align="center"
|
||||
pb={"sm"}
|
||||
justify="space-between"
|
||||
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
|
||||
>
|
||||
<Title order={4} c="gray.0">
|
||||
Last update pelayanan surat
|
||||
</Title>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)
|
||||
}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</Flex>
|
||||
<Stack gap="sm" mt="md" align="stretch" justify="center">
|
||||
{data &&
|
||||
Array.isArray(data.pelayanan) &&
|
||||
data.pelayanan.length > 0 ? (
|
||||
data.pelayanan.map((item: any, index: number) => (
|
||||
<PengaduanSection
|
||||
key={index}
|
||||
id={item.id}
|
||||
nomer={item.noPengajuan}
|
||||
judul={item.title}
|
||||
status={item.status}
|
||||
updated={item.updatedAt}
|
||||
kategori="pelayanan"
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text c="dimmed" ta={"center"}>
|
||||
Tidak ada data
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function PengaduanSection({
|
||||
id,
|
||||
nomer,
|
||||
judul,
|
||||
status,
|
||||
updated,
|
||||
kategori,
|
||||
}: {
|
||||
id: string;
|
||||
nomer: string;
|
||||
judul: string;
|
||||
status: string;
|
||||
updated: string;
|
||||
kategori: "pengaduan" | "pelayanan";
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap="xs"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
kategori == "pelayanan"
|
||||
? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}`
|
||||
: `/scr/dashboard/pengaduan/detail?id=${id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
align="center"
|
||||
pb={"sm"}
|
||||
justify="space-between"
|
||||
gap="md"
|
||||
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
|
||||
>
|
||||
<Flex direction={"column"}>
|
||||
<Text size="md" c="gray.2" lineClamp={1}>
|
||||
{judul}
|
||||
</Text>
|
||||
<Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
#{nomer} ∙ {updated}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Tooltip label={status}>
|
||||
<Badge
|
||||
size="xs"
|
||||
circle
|
||||
color={
|
||||
status === "diterima"
|
||||
? "green"
|
||||
: status === "ditolak"
|
||||
? "red"
|
||||
: status === "selesai"
|
||||
? "blue"
|
||||
: status === "dikerjakan"
|
||||
? "gray"
|
||||
: "yellow"
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -12,21 +12,26 @@ import {
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import ModalFile from "./ModalFile";
|
||||
import notification from "./notificationGlobal";
|
||||
import _ from "lodash";
|
||||
|
||||
export default function DesaSetting() {
|
||||
export default function DesaSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(false);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [img, setImg] = useState<any>()
|
||||
const [img, setImg] = useState<any>();
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [viewImg, setViewImg] = useState("");
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
@@ -50,12 +55,19 @@ export default function DesaSetting() {
|
||||
let finalData = { ...dataEdit }; // ← buffer data terbaru
|
||||
|
||||
if (dataEdit.name === "TTD") {
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img });
|
||||
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({
|
||||
file: dataEdit.value,
|
||||
folder: "lainnya",
|
||||
});
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: img,
|
||||
folder: "lainnya",
|
||||
});
|
||||
|
||||
if (resImg.status === 200) {
|
||||
finalData = {
|
||||
...finalData,
|
||||
value: resImg.data?.filename || ""
|
||||
value: resImg.data?.filename || "",
|
||||
};
|
||||
|
||||
setDataEdit(finalData); // update state
|
||||
@@ -68,7 +80,6 @@ export default function DesaSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const res = await apiFetch.api["configuration-desa"].edit.post(finalData);
|
||||
|
||||
if (res.status === 200) {
|
||||
@@ -98,8 +109,11 @@ 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();
|
||||
}
|
||||
@@ -131,31 +145,27 @@ export default function DesaSetting() {
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
{
|
||||
dataEdit.name == "TTD"
|
||||
?
|
||||
(
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<FileInput
|
||||
clearable
|
||||
placeholder="Upload TTD"
|
||||
accept="image/*"
|
||||
onChange={(e) => { setImg(e) }}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)
|
||||
:
|
||||
(
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input
|
||||
value={dataEdit.value}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "value", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)
|
||||
}
|
||||
{dataEdit.name == "TTD" ? (
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<FileInput
|
||||
clearable
|
||||
placeholder="Upload TTD"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
setImg(e);
|
||||
}}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
) : (
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input
|
||||
value={dataEdit.value}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "value", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)}
|
||||
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
@@ -176,7 +186,7 @@ export default function DesaSetting() {
|
||||
<ModalFile
|
||||
open={openedPreview && !_.isEmpty(viewImg)}
|
||||
onClose={() => setOpenedPreview(false)}
|
||||
folder="syarat-dokumen"
|
||||
folder="lainnya"
|
||||
fileName={viewImg}
|
||||
/>
|
||||
|
||||
@@ -197,27 +207,43 @@ export default function DesaSetting() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
{list.length > 0 && list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
v.name == "TTD"
|
||||
?
|
||||
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
|
||||
{v.name == "TTD" ? (
|
||||
v.value ? (
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={() => {
|
||||
setViewImg(v.value);
|
||||
setOpenedPreview(true);
|
||||
}}
|
||||
underline="always"
|
||||
>
|
||||
Lihat
|
||||
</Anchor>
|
||||
:
|
||||
v.value
|
||||
}
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
) : (
|
||||
v.value
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Tooltip label="Edit Setting">
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.desa.edit")
|
||||
? "Edit Setting"
|
||||
: "Edit Setting - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes("setting.desa.edit")}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
38
src/components/FullScreenLoading.tsx
Normal file
38
src/components/FullScreenLoading.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Center, Loader, Overlay, Stack, Text } from "@mantine/core";
|
||||
|
||||
type FullScreenLoadingProps = {
|
||||
visible: boolean;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export default function FullScreenLoading({
|
||||
visible,
|
||||
text = "Memproses data...",
|
||||
}: FullScreenLoadingProps) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Overlay fixed blur={6} backgroundOpacity={0.3} zIndex={10000}>
|
||||
<Center h="100%">
|
||||
<Stack align="center" justify="center">
|
||||
<Loader size="lg" />
|
||||
<Text size="sm" c="dimmed">
|
||||
{text}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
const overlayStyle: React.CSSProperties = {
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 9999,
|
||||
backdropFilter: "blur(6px)",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.6)",
|
||||
};
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
flexDirection: "column",
|
||||
};
|
||||
@@ -4,25 +4,27 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
List,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
TagsInput,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import { IconEye, IconTrash } from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPelayananSurat() {
|
||||
export default function KategoriPelayananSurat({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [openedDetail, { open: openDetail, close: closeDetail }] =
|
||||
@@ -39,13 +41,13 @@ export default function KategoriPelayananSurat() {
|
||||
const [dataChoose, setDataChoose] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
syaratDokumen: [{ key: "", name: "", desc: "" }],
|
||||
dataPelengkap: [{ key: "", name: "", desc: "" }],
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
syaratDokumen: [{ key: "", name: "", desc: "" }],
|
||||
dataPelengkap: [{ key: "", name: "", desc: "" }],
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
@@ -55,8 +57,8 @@ export default function KategoriPelayananSurat() {
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const cleanedDataText = dataTambah.dataText
|
||||
.map((v) => v.trim())
|
||||
const cleanedDataText = dataTambah.dataPelengkap
|
||||
.map((v) => v.name.trim())
|
||||
.filter((v) => v !== "");
|
||||
const cleanedSyarat = dataTambah.syaratDokumen
|
||||
.map((item) => ({
|
||||
@@ -77,8 +79,8 @@ export default function KategoriPelayananSurat() {
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
syaratDokumen: [{ name: "", desc: "" }],
|
||||
dataText: [""],
|
||||
syaratDokumen: [{ key: "", name: "", desc: "" }],
|
||||
dataPelengkap: [{ key: "", name: "", desc: "" }],
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
@@ -107,8 +109,8 @@ export default function KategoriPelayananSurat() {
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const cleanedDataText = dataChoose.dataText
|
||||
.map((v) => v.trim())
|
||||
const cleanedDataText = dataChoose.dataPelengkap
|
||||
.map((v) => v.name.trim())
|
||||
.filter((v) => v !== "");
|
||||
const cleanedSyarat = dataChoose.syaratDokumen
|
||||
.map((item) => ({
|
||||
@@ -186,7 +188,10 @@ export default function KategoriPelayananSurat() {
|
||||
function handleAddSyarat() {
|
||||
setDataChoose({
|
||||
...dataChoose,
|
||||
syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }],
|
||||
syaratDokumen: [
|
||||
...dataChoose.syaratDokumen,
|
||||
{ key: "", name: "", desc: "" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,7 +204,7 @@ export default function KategoriPelayananSurat() {
|
||||
|
||||
function handleEditSyarat(
|
||||
index: number,
|
||||
data: { name: string; desc: string },
|
||||
data: { key: string; name: string; desc: string },
|
||||
) {
|
||||
setDataChoose({
|
||||
...dataChoose,
|
||||
@@ -212,7 +217,7 @@ export default function KategoriPelayananSurat() {
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
{/* <Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
@@ -228,15 +233,85 @@ export default function KategoriPelayananSurat() {
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<TagsInput
|
||||
label="Data Pelengkap"
|
||||
placeholder="Tambah data pelengkap"
|
||||
splitChars={[","]}
|
||||
value={dataChoose.dataText}
|
||||
onChange={(value) =>
|
||||
setDataChoose({ ...dataChoose, dataText: value })
|
||||
}
|
||||
/>
|
||||
<Flex direction={"column"} gap={"md"}>
|
||||
<Group>
|
||||
<Text size="sm" c={"white"}>
|
||||
Data Pelengkap
|
||||
</Text>
|
||||
<Tooltip label="Tambah Data Pelengkap">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="blue"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={handleAddSyarat}
|
||||
>
|
||||
<IconPlus size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{dataChoose?.dataPelengkap?.map((v: any, i: number) => (
|
||||
<Grid
|
||||
key={i}
|
||||
style={{
|
||||
borderBottom: "1px solid gray",
|
||||
paddingBottom: "10px",
|
||||
}}
|
||||
>
|
||||
<Grid.Col
|
||||
span={1}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Tooltip label="Delete Syarat Dokumen">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
handleDeleteSyarat(i);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={5}>
|
||||
<Input.Wrapper label="Label">
|
||||
<Input
|
||||
value={v.name}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
key: v.key,
|
||||
name: e.target.value,
|
||||
desc: v.desc,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Input.Wrapper label="Deskripsi">
|
||||
<Input
|
||||
value={v.desc}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
key: v.key,
|
||||
name: v.name,
|
||||
desc: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex direction={"column"} gap={"md"}>
|
||||
<Group>
|
||||
<Text size="sm" c={"white"}>
|
||||
@@ -285,11 +360,12 @@ export default function KategoriPelayananSurat() {
|
||||
</Tooltip>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={5}>
|
||||
<Input.Wrapper label="Nama">
|
||||
<Input.Wrapper label="Label">
|
||||
<Input
|
||||
value={v.name}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
key: v.key,
|
||||
name: e.target.value,
|
||||
desc: v.desc,
|
||||
})
|
||||
@@ -303,6 +379,7 @@ export default function KategoriPelayananSurat() {
|
||||
value={v.desc}
|
||||
onChange={(e) =>
|
||||
handleEditSyarat(i, {
|
||||
key: v.key,
|
||||
name: v.name,
|
||||
desc: e.target.value,
|
||||
})
|
||||
@@ -323,10 +400,10 @@ export default function KategoriPelayananSurat() {
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Modal> */}
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
{/* <Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
@@ -342,15 +419,6 @@ export default function KategoriPelayananSurat() {
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<TagsInput
|
||||
label="Data Pelengkap"
|
||||
placeholder="Tambah data pelengkap"
|
||||
splitChars={[","]}
|
||||
value={dataTambah.dataText}
|
||||
onChange={(value) =>
|
||||
setDataTambah({ ...dataTambah, dataText: value })
|
||||
}
|
||||
/>
|
||||
<Flex direction={"column"} gap={"md"}>
|
||||
<Group>
|
||||
<Text size="sm" c={"white"}>
|
||||
@@ -367,7 +435,7 @@ export default function KategoriPelayananSurat() {
|
||||
...dataTambah,
|
||||
syaratDokumen: [
|
||||
...dataTambah.syaratDokumen,
|
||||
{ name: "", desc: "" },
|
||||
{ key: "", name: "", desc: "" },
|
||||
],
|
||||
});
|
||||
}}
|
||||
@@ -460,7 +528,7 @@ export default function KategoriPelayananSurat() {
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Modal> */}
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
@@ -519,8 +587,8 @@ export default function KategoriPelayananSurat() {
|
||||
Data Pelengkap
|
||||
</Text>
|
||||
<List>
|
||||
{dataChoose?.dataText?.map((v: any) => (
|
||||
<List.Item key={v.id}>{v}</List.Item>
|
||||
{dataChoose?.dataPelengkap?.map((v: any) => (
|
||||
<List.Item key={v.id}>{v.name}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Flex>
|
||||
@@ -533,15 +601,17 @@ export default function KategoriPelayananSurat() {
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pelayanan Surat
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{/* {permissions.includes("setting.kategori_pelayanan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)} */}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -553,7 +623,7 @@ export default function KategoriPelayananSurat() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list?.map((v: any) => (
|
||||
{list.length > 0 && list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
@@ -572,7 +642,15 @@ export default function KategoriPelayananSurat() {
|
||||
<IconEye size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Edit Kategori">
|
||||
{/* <Tooltip
|
||||
label={
|
||||
permissions.includes(
|
||||
"setting.kategori_pelayanan.edit",
|
||||
)
|
||||
? "Edit Kategori"
|
||||
: "Edit Kategori - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -581,11 +659,24 @@ export default function KategoriPelayananSurat() {
|
||||
setDataChoose(v);
|
||||
open();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pelayanan.edit",
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
</Tooltip> */}
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes(
|
||||
"setting.kategori_pelayanan.delete",
|
||||
)
|
||||
? "Hapus Kategori"
|
||||
: "Hapus Kategori - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -595,6 +686,11 @@ export default function KategoriPelayananSurat() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pelayanan.delete",
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -15,11 +15,16 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPengaduan() {
|
||||
export default function KategoriPengaduan({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
@@ -293,15 +298,17 @@ export default function KategoriPengaduan() {
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pengaduan
|
||||
</Title>
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{permissions.includes("setting.kategori_pengaduan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -318,17 +325,38 @@ export default function KategoriPengaduan() {
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit Kategori">
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes(
|
||||
"setting.kategori_pengaduan.edit",
|
||||
)
|
||||
? "Edit Kategori"
|
||||
: "Edit Kategori - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pengaduan.edit",
|
||||
) || v.id == "lainnya"
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete Kategori">
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes(
|
||||
"setting.kategori_pengaduan.delete",
|
||||
)
|
||||
? "Hapus Kategori"
|
||||
: "Hapus Kategori - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -338,6 +366,11 @@ export default function KategoriPengaduan() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pengaduan.delete",
|
||||
) || v.id == "lainnya"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -1,66 +1,102 @@
|
||||
import { detectFileType } from "@/server/lib/detect-type-of-file";
|
||||
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);
|
||||
export default function ModalFile({
|
||||
open,
|
||||
onClose,
|
||||
folder,
|
||||
fileName,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
folder: string;
|
||||
fileName: string;
|
||||
}) {
|
||||
const [viewFile, setViewFile] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [typeFile, setTypeFile] = useState<string>("");
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && fileName) {
|
||||
loadImage();
|
||||
useEffect(() => {
|
||||
if (open && fileName) {
|
||||
loadImage();
|
||||
}
|
||||
}, [open, fileName]);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewFile("");
|
||||
setLoading(true);
|
||||
|
||||
// detect type of file
|
||||
const { ext, type } = detectFileType(fileName);
|
||||
setTypeFile(type || "");
|
||||
|
||||
// load file
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=" + folder + "&fileName=" + fileName;
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok) {
|
||||
setError(true);
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}, [open, fileName]);
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
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);
|
||||
setViewFile(url);
|
||||
} catch (err) {
|
||||
setError(true);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
onClose();
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
|
||||
|
||||
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}
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={onClose}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="xl"
|
||||
withCloseButton
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
title="File"
|
||||
>
|
||||
{loading && (
|
||||
<Flex justify="center" align="center" h={200}>
|
||||
<Loader />
|
||||
</Flex>
|
||||
)}
|
||||
{viewFile && (
|
||||
<>
|
||||
{typeFile == "pdf" ? (
|
||||
<embed
|
||||
src={viewFile}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
height="950"
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
) : (
|
||||
<Image radius="md" h={300} fit="contain" src={viewFile} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { ActionIcon, Flex, Modal } from "@mantine/core";
|
||||
import { Flex, Modal, Progress, Stack, Text } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconDownload, IconX } from "@tabler/icons-react";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
|
||||
import SKBelumKawin from "./surat/SKBelumKawin";
|
||||
@@ -18,123 +17,190 @@ import SKTidakMampu from "./surat/SKTidakMampu";
|
||||
import SKUsaha from "./surat/SKUsaha";
|
||||
import SKYatim from "./surat/SKYatimPiatu";
|
||||
|
||||
export default function ModalSurat({ open, onClose, surat }: { open: boolean, onClose: () => void, surat: string }) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
color: "#000",
|
||||
fontSize: "14px",
|
||||
fontFamily: "Times New Roman",
|
||||
};
|
||||
const hiddenRef = useRef<any>(null);
|
||||
const { data, mutate, isLoading } = useSWR("surat", () =>
|
||||
apiFetch.api.surat.detail.get({
|
||||
query: {
|
||||
id: surat,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
export default function ModalSurat({
|
||||
open,
|
||||
onClose,
|
||||
surat,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: (val: { success: boolean, data: string }) => void;
|
||||
surat: string;
|
||||
}) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
color: "#000",
|
||||
fontSize: "14px",
|
||||
fontFamily: "Times New Roman",
|
||||
};
|
||||
const [uploading, setUploading] = useState<{ text: "Menyiapkan" | "Mengupload" | "Selesai" | "Gagal", value: number }>({ text: "Menyiapkan", value: 10 })
|
||||
const hiddenRef = useRef<any>(null);
|
||||
const { data, mutate, isLoading } = useSWR("surat", () =>
|
||||
apiFetch.api.surat.detail.get({
|
||||
query: {
|
||||
id: surat,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
const downloadPDF = async () => {
|
||||
const element = hiddenRef.current;
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
const uploadPdf = async () => {
|
||||
try {
|
||||
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
|
||||
setUploading({ text: "Mengupload", value: 75 });
|
||||
const element = hiddenRef.current;
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
|
||||
const imgData = canvas.toDataURL("image/jpeg", 1.0);
|
||||
const imgData = canvas.toDataURL("image/jpeg", 1.0);
|
||||
|
||||
const pdf = new jsPDF("p", "mm", "a4");
|
||||
const pageWidth = 210; // A4 width mm
|
||||
const pageHeight = 297; // A4 height mm
|
||||
const pdf = new jsPDF("p", "mm", "a4");
|
||||
const pageWidth = 210; // A4 width mm
|
||||
const pageHeight = 297; // A4 height mm
|
||||
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
|
||||
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
||||
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
||||
|
||||
pdf.save("surat-keterangan-usaha.pdf");
|
||||
};
|
||||
// ⬇️ ambil sebagai Blob
|
||||
const pdfBlob = pdf.output("blob");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onClose()}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
styles={{
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px 16px",
|
||||
},
|
||||
title: {
|
||||
width: "100%",
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
Preview Surat
|
||||
</div>
|
||||
const pdfFile = new File(
|
||||
[pdfBlob],
|
||||
`${data?.data?.surat?.nameCategory}.pdf`,
|
||||
{
|
||||
type: "application/pdf",
|
||||
lastModified: Date.now(),
|
||||
}
|
||||
);
|
||||
|
||||
<Flex gap={8}>
|
||||
<ActionIcon size={32} variant="default">
|
||||
<IconDownload size={20} onClick={downloadPDF} />
|
||||
</ActionIcon>
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: pdfFile,
|
||||
folder: "surat",
|
||||
});
|
||||
|
||||
<ActionIcon size={32} variant="default" onClick={onClose}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{
|
||||
data && data.data
|
||||
? data.data.surat.idCategory == "skusaha"
|
||||
? <SKUsaha data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelahiran"
|
||||
? <SKKelahiran data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelakuanbaik"
|
||||
? <SKKelakuanBaik data={data.data} />
|
||||
: data.data.surat.idCategory == "skpenghasilan"
|
||||
? <SKPenghasilan data={data.data} />
|
||||
: data.data.surat.idCategory == "sktidakmampu"
|
||||
? <SKTidakMampu data={data.data} />
|
||||
: data.data.surat.idCategory == "skyatimpiatu"
|
||||
? <SKYatim data={data.data} />
|
||||
: data.data.surat.idCategory == "skdomisiliorganisasi"
|
||||
? <SKDomisiliOrganisasi data={data.data} />
|
||||
: data.data.surat.idCategory == "skbedabiodata"
|
||||
? <SKBedaBiodataDiri data={data.data} />
|
||||
: data.data.surat.idCategory == "sktempatusaha"
|
||||
? <SKTempatUsaha data={data.data} />
|
||||
: data.data.surat.idCategory == "skbelumkawin"
|
||||
? <SKBelumKawin data={data.data} />
|
||||
: data.data.surat.idCategory == "skkematian"
|
||||
? <SKKematian data={data.data} />
|
||||
: <></>
|
||||
: <></>
|
||||
}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const resUpdate = await apiFetch.api.surat.update.post({
|
||||
id: surat,
|
||||
filename: resImg.data?.filename!,
|
||||
});
|
||||
|
||||
if (resUpdate?.data?.success) {
|
||||
setUploading({ text: "Selesai", value: 100 });
|
||||
setTimeout(() => {
|
||||
onClose({ success: true, data: resUpdate.data?.link });
|
||||
}, 1000)
|
||||
} else {
|
||||
setUploading({ text: "Gagal", value: 100 });
|
||||
setTimeout(() => {
|
||||
onClose({ success: false, data: "" });
|
||||
}, 1000)
|
||||
}
|
||||
} else {
|
||||
setUploading({ text: "Gagal", value: 100 });
|
||||
setTimeout(() => {
|
||||
onClose({ success: false, data: "" });
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error uploading PDF:", error);
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
uploadPdf();
|
||||
}, 3000);
|
||||
}
|
||||
}, [surat, open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => { }}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
closeOnClickOutside={false}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
styles={{
|
||||
header: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "12px 16px",
|
||||
},
|
||||
title: {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
|
||||
|
||||
{/* <Flex gap={8} align="center">
|
||||
<Loader color="blue" size="xs" />
|
||||
<Text size="sm">{uploading.text}</Text>
|
||||
</Flex> */}
|
||||
</Flex>
|
||||
<Stack
|
||||
align="stretch"
|
||||
justify="center"
|
||||
gap="xs"
|
||||
>
|
||||
<Text size="sm" ta="center">{uploading.text} - Harap menunggu sampai selesai</Text>
|
||||
<Progress radius="md" value={uploading.value} animated size="lg" />
|
||||
</Stack>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{data && data.data ? (
|
||||
data.data.surat.idCategory == "skusaha" ? (
|
||||
<SKUsaha data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelahiran" ? (
|
||||
<SKKelahiran data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelakuanbaik" ? (
|
||||
<SKKelakuanBaik data={data.data} />
|
||||
) : data.data.surat.idCategory == "skpenghasilan" ? (
|
||||
<SKPenghasilan data={data.data} />
|
||||
) : data.data.surat.idCategory == "sktidakmampu" ? (
|
||||
<SKTidakMampu data={data.data} />
|
||||
) : data.data.surat.idCategory == "skyatimpiatu" ? (
|
||||
<SKYatim data={data.data} />
|
||||
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
|
||||
<SKDomisiliOrganisasi data={data.data} />
|
||||
) : data.data.surat.idCategory == "skbedabiodata" ? (
|
||||
<SKBedaBiodataDiri data={data.data} />
|
||||
) : data.data.surat.idCategory == "sktempatusaha" ? (
|
||||
<SKTempatUsaha data={data.data} />
|
||||
) : data.data.surat.idCategory == "skbelumkawin" ? (
|
||||
<SKBelumKawin data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkematian" ? (
|
||||
<SKKematian data={data.data} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
45
src/components/NotFoundPengajuanSurat.tsx
Normal file
45
src/components/NotFoundPengajuanSurat.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Button, Center, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
export function DataNotFound({
|
||||
onRetry,
|
||||
backTo,
|
||||
}: {
|
||||
onRetry?: () => void;
|
||||
backTo?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Center mih={320}>
|
||||
<Stack align="center" gap="sm">
|
||||
<IconSearch size={64} opacity={0.5} />
|
||||
|
||||
<Title order={4}>Data Pengajuan Tidak Ditemukan</Title>
|
||||
|
||||
<Text size="sm" c="dimmed" ta="center" maw={380}>
|
||||
Kami tidak dapat menemukan data pengajuan dengan nomor pengajuan yg
|
||||
diinputkan. Silakan periksa kembali data Anda.
|
||||
</Text>
|
||||
|
||||
<Group mt="md">
|
||||
{/* {onRetry && (
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onClick={onRetry}
|
||||
>
|
||||
Cari Ulang
|
||||
</Button>
|
||||
)} */}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
// leftSection={<IconArrowLeft size={16} />}
|
||||
onClick={backTo}
|
||||
>
|
||||
Cari Kembali
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
68
src/components/PermissionRole.tsx
Normal file
68
src/components/PermissionRole.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { groupPermissions } from "@/lib/groupPermission";
|
||||
import { Anchor, Flex, Stack, Text } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Node {
|
||||
label: string;
|
||||
children: any;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
function RenderNode({ node }: { node: Node }) {
|
||||
const sub = Object.values(node.children || {});
|
||||
|
||||
return (
|
||||
<Stack pl="md" gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">- {node.label}</Text>
|
||||
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode key={i} node={child} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderNode2({ node }: { node: Node }) {
|
||||
const sub = Object.values(node.children || {});
|
||||
|
||||
return (
|
||||
<Flex direction={"row"} wrap={"wrap"} gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">{node.label},</Text>
|
||||
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode2 key={i} node={child} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PermissionRole({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: string[];
|
||||
}) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
if (!permissions?.length) return <Text c="dimmed">-</Text>;
|
||||
|
||||
const groups = groupPermissions(permissions);
|
||||
const rootNodes = Object.values(groups);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{showAll
|
||||
? rootNodes.map((node: any, idx) => (
|
||||
<RenderNode key={idx} node={node} />
|
||||
))
|
||||
: rootNodes
|
||||
.slice(0, 2)
|
||||
.map((node: any, idx) => <RenderNode2 key={idx} node={node} />)}
|
||||
<Anchor size="xs" onClick={() => setShowAll(!showAll)}>
|
||||
{showAll ? "View less" : "View more"}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
183
src/components/PermissionTree.tsx
Normal file
183
src/components/PermissionTree.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import permissionConfig from "@/lib/listPermission.json";
|
||||
import {
|
||||
ActionIcon,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
Group,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Node {
|
||||
label: string;
|
||||
key: string;
|
||||
children?: Node[];
|
||||
}
|
||||
|
||||
export default function PermissionTree({
|
||||
selected,
|
||||
onChange,
|
||||
}: {
|
||||
selected: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
}) {
|
||||
// Ambil semua child dari node
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
|
||||
|
||||
function toggleNode(label: string) {
|
||||
setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
|
||||
}
|
||||
|
||||
function getAllChildKeys(node: Node): string[] {
|
||||
let result: string[] = [];
|
||||
if (node.children) {
|
||||
node.children.forEach((c) => {
|
||||
result.push(c.key);
|
||||
result = [...result, ...getAllChildKeys(c)];
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Dapatkan parentKey, jika ada
|
||||
function getParentKey(key: string) {
|
||||
const split = key.split(".");
|
||||
if (split.length <= 1) return null;
|
||||
split.pop();
|
||||
return split.join(".");
|
||||
}
|
||||
|
||||
// Update parent ke atas secara rekursif
|
||||
function updateParent(next: string[], parentKey: string | null): string[] {
|
||||
if (!parentKey) return next;
|
||||
|
||||
const allChildKeys = findAllChildKeysFromKey(parentKey);
|
||||
|
||||
const selectedChild = allChildKeys.filter((c) => next.includes(c));
|
||||
|
||||
if (selectedChild.length === 0) {
|
||||
// Semua child uncheck → parent uncheck
|
||||
next = next.filter((x) => x !== parentKey);
|
||||
} else if (selectedChild.length === allChildKeys.length) {
|
||||
// Semua child check → parent check
|
||||
if (!next.includes(parentKey)) {
|
||||
next.push(parentKey);
|
||||
}
|
||||
} else {
|
||||
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
|
||||
if (!next.includes(parentKey)) {
|
||||
next.push(parentKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Rekursif naik ke atas
|
||||
return updateParent(next, getParentKey(parentKey));
|
||||
}
|
||||
|
||||
// dapatkan child dari string key
|
||||
function findAllChildKeysFromKey(parentKey: string) {
|
||||
const list: string[] = [];
|
||||
|
||||
function traverse(nodes: Node[]) {
|
||||
nodes.forEach((n) => {
|
||||
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
|
||||
list.push(n.key);
|
||||
}
|
||||
if (n.children) traverse(n.children);
|
||||
});
|
||||
}
|
||||
|
||||
traverse(permissionConfig.menus);
|
||||
return list;
|
||||
}
|
||||
|
||||
const RenderMenu = ({ menu }: { menu: Node }) => {
|
||||
const hasChild = menu.children && menu.children.length > 0;
|
||||
const open = openNodes[menu.label] ?? false;
|
||||
const childKeys = getAllChildKeys(menu);
|
||||
const isChecked = selected.includes(menu.key);
|
||||
const isIndeterminate =
|
||||
!isChecked &&
|
||||
selected.some(
|
||||
(x) => typeof x === "string" && x.startsWith(menu.key + "."),
|
||||
);
|
||||
|
||||
function handleCheck() {
|
||||
let next = [...selected];
|
||||
|
||||
if (childKeys.length > 0) {
|
||||
// klik parent
|
||||
if (!isChecked) {
|
||||
next = [...new Set([...next, menu.key, ...childKeys])];
|
||||
} else {
|
||||
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
|
||||
}
|
||||
|
||||
next = updateParent(next, getParentKey(menu.key));
|
||||
onChange(next);
|
||||
return;
|
||||
}
|
||||
|
||||
// klik child
|
||||
if (isChecked) {
|
||||
next = next.filter((x) => x !== menu.key);
|
||||
} else {
|
||||
next.push(menu.key);
|
||||
}
|
||||
|
||||
next = updateParent(next, getParentKey(menu.key));
|
||||
onChange(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Group gap="xs">
|
||||
{menu.children && menu.children.length > 0 ? (
|
||||
<ActionIcon variant="subtle" onClick={() => toggleNode(menu.label)}>
|
||||
{openNodes[menu.label] ? (
|
||||
<IconChevronDown size={16} />
|
||||
) : (
|
||||
<IconChevronRight size={16} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<div style={{ width: 28 }} />
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
label={menu.label}
|
||||
checked={isChecked}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{menu.children && (
|
||||
<Collapse in={open}>
|
||||
<Stack gap={4} pl="md">
|
||||
{menu.children.map((child) => (
|
||||
<RenderMenu key={child.key} menu={child} />
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text size="sm">Hak Akses</Text>
|
||||
{permissionConfig.menus
|
||||
.filter(
|
||||
(menu: Node) =>
|
||||
!menu.key.startsWith("api") && !menu.key.startsWith("credential"),
|
||||
)
|
||||
.map((menu: Node) => (
|
||||
<RenderMenu key={menu.key} menu={menu} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -9,10 +9,15 @@ import {
|
||||
Stack,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function ProfileUser() {
|
||||
export default function ProfileUser({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [openedPassword, setOpenedPassword] = useState(false);
|
||||
const [pwdBaru, setPwdBaru] = useState("");
|
||||
@@ -126,12 +131,17 @@ export default function ProfileUser() {
|
||||
Profile Pengguna
|
||||
</Title>
|
||||
<Group gap="md">
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
{permissions.includes("setting.profile.edit") && (
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{permissions.includes("setting.profile.password") && (
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
|
||||
49
src/components/SuccessPengajuanSurat.tsx
Normal file
49
src/components/SuccessPengajuanSurat.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Badge, Button, Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconCheck } from "@tabler/icons-react";
|
||||
|
||||
type SuccessPengajuanProps = {
|
||||
noPengajuan: string;
|
||||
onClose?: () => void;
|
||||
category?: "create" | "update";
|
||||
};
|
||||
|
||||
export default function SuccessPengajuan({
|
||||
noPengajuan,
|
||||
onClose,
|
||||
category,
|
||||
}: SuccessPengajuanProps) {
|
||||
return (
|
||||
<Center h="100vh">
|
||||
<Card shadow="md" radius="md" p="xl" withBorder maw={520} w="100%">
|
||||
<Stack align="center" gap="md">
|
||||
<IconCheck size={56} color="green" />
|
||||
|
||||
<Title order={3} ta="center">
|
||||
{category == "create"
|
||||
? "Pengajuan Berhasil Dibuat"
|
||||
: "Pengajuan Berhasil Diupdate"}
|
||||
</Title>
|
||||
|
||||
<Text ta="center" size="sm" c="dimmed">
|
||||
{category == "create"
|
||||
? "Pengajuan layanan surat sudah dibuat dengan nomor:"
|
||||
: "Pengajuan layanan surat sudah diupdate dengan nomor:"}
|
||||
</Text>
|
||||
|
||||
<Badge size="xl" variant="light" color="green">
|
||||
{noPengajuan}
|
||||
</Badge>
|
||||
|
||||
<Text ta="center" size="sm">
|
||||
Nomor ini akan digunakan untuk mengakses dan memantau status
|
||||
pengajuan surat Anda.
|
||||
</Text>
|
||||
|
||||
<Button fullWidth mt="md" onClick={onClose}>
|
||||
Selesai
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
472
src/components/UserRoleSetting.tsx
Normal file
472
src/components/UserRoleSetting.tsx
Normal file
@@ -0,0 +1,472 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import listMenu from "../lib/listPermission.json";
|
||||
import notification from "./notificationGlobal";
|
||||
import PermissionRole from "./PermissionRole";
|
||||
import PermissionTree from "./PermissionTree";
|
||||
|
||||
interface MenuNode {
|
||||
key: string;
|
||||
label: string;
|
||||
default: boolean;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
export default function UserRoleSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const {
|
||||
data: dataRole,
|
||||
mutate: mutateRole,
|
||||
isLoading: isLoadingRole,
|
||||
} = useSWR("user-role", () => apiFetch.api.user.role.get());
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("role-list", () =>
|
||||
apiFetch.api.user.role.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const listRole = dataRole?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
permissions: false,
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user["role-create"].post(
|
||||
dataTambah as any,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your role have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create role",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create role",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your role have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit role",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit role",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user["role-delete"].post({
|
||||
id: dataDelete,
|
||||
});
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeDelete();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your role have been deleted",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete role",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to delete role",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; name: string; permissions: [] };
|
||||
}) {
|
||||
setDataEdit({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
permissions: data.permissions ? data.permissions : [],
|
||||
});
|
||||
open();
|
||||
}
|
||||
|
||||
function onValidation({
|
||||
kat,
|
||||
value,
|
||||
aksi,
|
||||
}: {
|
||||
kat: "name" | "permission";
|
||||
value: string | null;
|
||||
aksi: "edit" | "tambah";
|
||||
}) {
|
||||
if (value == null || value.length < 1) {
|
||||
setBtnDisable(true);
|
||||
setError({ ...error, [kat]: true });
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
if (aksi === "edit") {
|
||||
setDataEdit({ ...dataEdit, [kat]: value });
|
||||
} else {
|
||||
setDataTambah({ ...dataTambah, [kat]: value });
|
||||
}
|
||||
}
|
||||
|
||||
function buildOrderList(menus: MenuNode[]): string[] {
|
||||
const list: string[] = [];
|
||||
|
||||
const traverse = (nodes: MenuNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
list.push(node.key);
|
||||
if (node.children) traverse(node.children);
|
||||
});
|
||||
};
|
||||
|
||||
traverse(menus);
|
||||
return list;
|
||||
}
|
||||
|
||||
function sortByJsonOrder(arrayData: string[]): string[] {
|
||||
const orderList = buildOrderList(listMenu.menus);
|
||||
|
||||
return arrayData.sort((a, b) => {
|
||||
return orderList.indexOf(a) - orderList.indexOf(b);
|
||||
});
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(false);
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Edit */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Edit"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size={"lg"}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Nama Role">
|
||||
<Input
|
||||
value={dataEdit.name}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "edit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<PermissionTree
|
||||
selected={dataEdit.permissions}
|
||||
onChange={(permissions) => {
|
||||
setDataEdit({
|
||||
...dataEdit,
|
||||
permissions: sortByJsonOrder(permissions) as never[],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleEdit}
|
||||
disabled={
|
||||
btnDisable ||
|
||||
dataEdit.name.length < 1 ||
|
||||
dataEdit.permissions?.length < 1
|
||||
}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Tambah */}
|
||||
<Modal
|
||||
opened={openedTambah}
|
||||
onClose={closeTambah}
|
||||
title={"Tambah"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size={"lg"}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper
|
||||
label="Nama Role"
|
||||
description=""
|
||||
error={error.name ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={dataTambah.name}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
value: e.target.value,
|
||||
aksi: "tambah",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<PermissionTree
|
||||
selected={dataTambah.permissions}
|
||||
onChange={(permissions) => {
|
||||
setDataTambah({
|
||||
...dataTambah,
|
||||
permissions: sortByJsonOrder(permissions) as never[],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeTambah}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={handleCreate}
|
||||
disabled={
|
||||
btnDisable ||
|
||||
dataTambah.name.length < 1 ||
|
||||
dataTambah.permissions.length < 1
|
||||
}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Modal Delete */}
|
||||
<Modal
|
||||
opened={openedDelete}
|
||||
onClose={closeDelete}
|
||||
title={"Delete"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="md" color="gray.6">
|
||||
Apakah anda yakin ingin menghapus role ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={closeDelete}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleDelete}
|
||||
loading={btnLoading}
|
||||
>
|
||||
Hapus
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<Stack gap={"md"}>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar Role
|
||||
</Title>
|
||||
{permissions.includes("setting.user_role.tambah") && (
|
||||
<Tooltip label="Tambah Role">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Role</Table.Th>
|
||||
<Table.Th>Permission</Table.Th>
|
||||
<Table.Th>Aksi</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list.length > 0 ? (
|
||||
list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td w={"150"}>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<PermissionRole permissions={v.permissions} />
|
||||
</Table.Td>
|
||||
<Table.Td w={"100"}>
|
||||
<Group>
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.user_role.edit")
|
||||
? "Edit Role"
|
||||
: "Edit Role - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={
|
||||
!permissions.includes("setting.user_role.edit") ||
|
||||
v.id == "developer"
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.user_role.delete")
|
||||
? "Delete Role"
|
||||
: "Delete Role - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.user_role.delete",
|
||||
) || v.id == "developer"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5} align="center">
|
||||
Data Role Tidak Ditemukan
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -16,11 +16,16 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function UserSetting() {
|
||||
export default function UserSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
@@ -106,19 +111,19 @@ export default function UserSetting() {
|
||||
async function handleEdit() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.pengaduan.category.update.post(dataEdit);
|
||||
const res = await apiFetch.api.user.update.post(dataEdit);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
close();
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your category have been saved",
|
||||
message: "Your data have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
message: "Failed to edit user",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
@@ -126,7 +131,7 @@ export default function UserSetting() {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit category",
|
||||
message: "Failed to edit user2",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
@@ -221,9 +226,10 @@ export default function UserSetting() {
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
<Input.Wrapper label="Edit Kategori">
|
||||
<Input.Wrapper label="Nama">
|
||||
<Input
|
||||
value={dataEdit.name}
|
||||
error={error.name ? "Field is required" : ""}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "name",
|
||||
@@ -233,6 +239,51 @@ export default function UserSetting() {
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Select
|
||||
label="Role"
|
||||
placeholder="Pilih Role"
|
||||
data={listRole.map((r: any) => ({
|
||||
value: r.id,
|
||||
label: r.name,
|
||||
}))}
|
||||
value={dataEdit.roleId || null}
|
||||
error={error.roleId ? "Field is required" : ""}
|
||||
onChange={(_value, option) => {
|
||||
onValidation({
|
||||
kat: "roleId",
|
||||
value: option?.value,
|
||||
aksi: "edit",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input.Wrapper label="Phone" description="">
|
||||
<Input
|
||||
value={dataEdit.phone}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "phone",
|
||||
value: e.target.value,
|
||||
aksi: "edit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Input.Wrapper
|
||||
label="Email"
|
||||
description=""
|
||||
error={error.email ? "Field is required" : ""}
|
||||
>
|
||||
<Input
|
||||
value={dataEdit.email}
|
||||
onChange={(e) =>
|
||||
onValidation({
|
||||
kat: "email",
|
||||
value: e.target.value,
|
||||
aksi: "edit",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
@@ -390,15 +441,17 @@ export default function UserSetting() {
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar User
|
||||
</Title>
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{permissions.includes("setting.user.tambah") && (
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -413,26 +466,42 @@ export default function UserSetting() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{list.length > 0 ? (
|
||||
{list && Array.isArray(list) && list.length > 0 ? (
|
||||
list?.map((v: any) => (
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>{v.phone}</Table.Td>
|
||||
<Table.Td>{v.email}</Table.Td>
|
||||
<Table.Td>{v.roleId}</Table.Td>
|
||||
<Table.Td>{v.nameRole}</Table.Td>
|
||||
<Table.Td>
|
||||
<Group>
|
||||
<Tooltip label="Edit User">
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.user.edit")
|
||||
? "Edit User"
|
||||
: "Edit User - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={
|
||||
!permissions.includes("setting.user.edit") ||
|
||||
v.roleId == "developer"
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Delete User">
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.user.delete")
|
||||
? "Delete User"
|
||||
: "Delete User - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -442,6 +511,10 @@ export default function UserSetting() {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes("setting.user.delete") ||
|
||||
v.roleId == "developer"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -1,129 +1,244 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKBedaBiodataDiri({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<b><u>SURAT KETERANGAN BEDA BIODATA DIRI</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* 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>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* 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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.25" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "15px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN BEDA BIODATA DIRI</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>
|
||||
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang
|
||||
bersangkutan:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>,
|
||||
meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa
|
||||
dokumen, sebagai berikut:
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>1. {getValue("data_dokumen")}</td>
|
||||
<td style={{ width: "10px" }}></td>
|
||||
<td>{/* {getValue("nama")} */}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen A</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("dokumen_a")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen B</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("dokumen_b")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* <div style={{ marginTop: "15px" }}>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("tempat_lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen A</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("dokumen_a")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen B</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tertulis pada dokumen b")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>3. Nama Orang Tua</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama orang tua")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen A</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tertulis pada dokumen a")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tertulis pada dokumen B</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tertulis pada dokumen b")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div> */}
|
||||
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Perbedaan tersebut terjadi karena{" "}
|
||||
<b>kesalahan penulisan/pencatatan administratif</b>, namun yang
|
||||
bersangkutan adalah <b>orang yang sama</b>.
|
||||
<br />
|
||||
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk
|
||||
dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "0px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,168 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKBelumKawin({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN BELUM KAWIN</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* 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>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* 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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN BELUM KAWIN</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan}{" "}
|
||||
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
|
||||
Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan keterangan dari yang bersangkutan dan data administrasi
|
||||
kependudukan yang ada di Desa {data.setting.desaNama}, yang bersangkutan
|
||||
benar sampai saat ini belum pernah menikah, baik secara adat, agama,
|
||||
maupun hukum negara.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
|
||||
digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
<br />
|
||||
Pemohon
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<u>{getValue("nama")}</u> <br />
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
<br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +1,165 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKDomisiliOrganisasi({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN DOMISILI ORGANISASI</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* 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>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* 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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN DOMISILI ORGANISASI</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat Kantor</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaAlamat}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama Organisasi</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama_organisasi")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Organisasi</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_organisasi")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_organisasi")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nomor Telepon</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("no_telepon")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nama Pimpinan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama_pimpinan")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan{" "}
|
||||
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
|
||||
Kabupaten {data.setting.desaKabupaten}. Dan sampai saat ini masih aktif
|
||||
melakukan kegiatan sesuai dengan bidangnya.
|
||||
<br />
|
||||
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
|
||||
digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,113 +1,246 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKelahiran({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.2" }}>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b>PEMERINTAH KABUPATEN/KOTA {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN KELAHIRAN</u></b><br />
|
||||
Nomor : {data.surat.noSurat}
|
||||
</div>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div>
|
||||
Yang bertanda tangan di bawah ini, {data.setting.perbekelJabatan}
|
||||
{` ${data.setting.desaNama}, Kecamatan ${data.setting.desaKecamatan}, Kabupaten/Kota ${data.setting.desaKabupaten}`}
|
||||
, dengan ini menerangkan bahwa:
|
||||
</div>
|
||||
|
||||
{/* DATA KELAHIRAN ANAK */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Telah lahir seorang anak pada:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Tanggal Lahir</td><td>:</td><td>{getValue("tanggal lahir anak")}</td></tr>
|
||||
<tr><td>Pukul</td><td>:</td><td>{getValue("pukul lahir anak")}</td></tr>
|
||||
<tr><td>Tempat Kelahiran</td><td>:</td><td>{getValue("tempat lahir anak")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin anak")}</td></tr>
|
||||
<tr><td>Anak ke</td><td>:</td><td>{getValue("anak ke")}</td></tr>
|
||||
<tr><td>Nama Anak</td><td>:</td><td>{getValue("nama anak")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA IBU */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dari seorang ibu bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Lengkap Ibu</td><td>:</td><td>{getValue("nama ibu")}</td></tr>
|
||||
<tr><td>NIK</td><td>:</td><td>{getValue("nik ibu")}</td></tr>
|
||||
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ibu")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ibu")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ibu")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA AYAH */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan seorang ayah bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Lengkap Ayah</td><td>:</td><td>{getValue("nama ayah")}</td></tr>
|
||||
<tr><td>NIK</td><td>:</td><td>{getValue("nik ayah")}</td></tr>
|
||||
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ayah")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ayah")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ayah")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA PELAPOR */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan laporan dari:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Nama Pelapor</td><td>:</td><td>{getValue("nama pelapor")}</td></tr>
|
||||
<tr><td>Hubungan dengan Anak</td><td>:</td><td>{getValue("hubungan pelapor")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat pelapor")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PENUTUP */}
|
||||
<div style={{ marginTop: "20px", textAlign: "justify" }}>
|
||||
Demikian Surat Keterangan Kelahiran ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TEMPAT TANGGAL */}
|
||||
<table style={{ width: "100%", marginTop: "20px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "200px" }}>Dikeluarkan di</td><td>:</td><td>{data.setting.desaNama}</td></tr>
|
||||
<tr><td>Pada tanggal</td><td>:</td><td>{data.surat.createdAt}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ marginTop: "40px", width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.2" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b>
|
||||
PEMERINTAH KABUPATEN/KOTA {_.upperCase(data.setting.desaKabupaten)}
|
||||
</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN KELAHIRAN</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor : {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div>
|
||||
Yang bertanda tangan di bawah ini, {data.setting.perbekelJabatan}
|
||||
{` ${data.setting.desaNama}, Kecamatan ${data.setting.desaKecamatan}, Kabupaten/Kota ${data.setting.desaKabupaten}`}
|
||||
, dengan ini menerangkan bahwa:
|
||||
</div>
|
||||
|
||||
{/* DATA KELAHIRAN ANAK */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Telah lahir seorang anak pada:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "200px" }}>Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tanggal_lahir_anak")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pukul</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pukul_lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat Kelahiran</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tempat_lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Anak ke</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("anak_ke")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Nama Anak</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama_anak")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA IBU */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dari seorang ibu bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "200px" }}>Nama Lengkap Ibu</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama_ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik_ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat & Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir_ibu")}, ${getValue("tanggal_lahir_ibu")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan_ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_ibu")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA AYAH */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan seorang ayah bernama:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "200px" }}>Nama Lengkap Ayah</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama_ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik_ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat & Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir_ayah")}, ${getValue("tanggal_lahir_ayah")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan_ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_ayah")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DATA PELAPOR */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan laporan dari:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "200px" }}>Nama Pelapor</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hubungan dengan Anak</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("hubungan_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_pelapor")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PENUTUP */}
|
||||
<div style={{ marginTop: "20px", textAlign: "justify" }}>
|
||||
Demikian Surat Keterangan Kelahiran ini dibuat dengan sebenarnya agar
|
||||
dapat digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TEMPAT TANGGAL */}
|
||||
<table style={{ width: "100%", marginTop: "20px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "200px" }}>Dikeluarkan di</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>:</td>
|
||||
<td>{data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,114 +1,155 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKelakuanBaik({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "30px" }}>
|
||||
<b style={{ fontSize: "18px" }}>SURAT KETERANGAN KELAKUAN BAIK</b><br />
|
||||
(PENGANTAR SKCK)<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini menerangkan dengan sebenarnya bahwa:
|
||||
</div>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* IDENTITAS PENDUDUK */}
|
||||
<table style={{ width: "100%", marginBottom: "15px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama lengkap</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tgl Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tempat tanggal lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ISI */}
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Adalah benar penduduk yang berdomisili di wilayah kami dan selama tinggal di lingkungan
|
||||
Desa {data.setting.desaNama}, berkelakuan baik, tidak pernah terlibat perbuatan melanggar hukum,
|
||||
serta dikenal sopan dan aktif dalam kegiatan kemasyarakatan.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat keterangan ini diberikan sebagai pengantar permohonan penerbitan Surat Keterangan
|
||||
Catatan Kepolisian (SKCK) ke Polsek/Polres {getValue("polsek")}.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat ini berlaku selama 6 (enam) bulan sejak tanggal diterbitkan, kecuali terdapat perubahan
|
||||
data yang mendasar.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL */}
|
||||
<table style={{ width: "100%", marginBottom: "40px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>:</td>
|
||||
<td>{data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama}<br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "30px" }}>
|
||||
<b style={{ fontSize: "18px" }}>SURAT KETERANGAN KELAKUAN BAIK</b>
|
||||
<br />
|
||||
(PENGANTAR SKCK)
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* PEMBUKA */}
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini menerangkan dengan sebenarnya bahwa:
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS PENDUDUK */}
|
||||
<table style={{ width: "100%", marginBottom: "15px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama lengkap</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tgl Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* ISI */}
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Adalah benar penduduk yang berdomisili di wilayah kami dan selama
|
||||
tinggal di lingkungan Desa {data.setting.desaNama}, berkelakuan baik,
|
||||
tidak pernah terlibat perbuatan melanggar hukum, serta dikenal sopan dan
|
||||
aktif dalam kegiatan kemasyarakatan.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat keterangan ini diberikan sebagai pengantar permohonan penerbitan
|
||||
Surat Keterangan Catatan Kepolisian (SKCK) ke Polsek/Polres{" "}
|
||||
{getValue("polsek")}.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
|
||||
Surat ini berlaku selama 6 (enam) bulan sejak tanggal diterbitkan,
|
||||
kecuali terdapat perubahan data yang mendasar.
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "justify", marginBottom: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk
|
||||
dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL */}
|
||||
<table style={{ width: "100%", marginBottom: "40px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>:</td>
|
||||
<td>{data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br /> <br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u>
|
||||
<br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,89 +1,203 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKematian({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN KEMATIAN</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* 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>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
<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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN KEMATIAN</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_pelapor")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hubungan dengan almarhum/almarhumah</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("hubungan_pelapor")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Melaporkan bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama_almarhum")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nik_almarhum")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin_almarhum")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir_almarhum")}, ${getValue("tanggal_lahir_almarhum")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama_almarhum")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_almarhum")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Telah meninggal dunia pada:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Tanggal Kematian</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("tanggal_kematian")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Waktu Kematian</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("waktu_kematian")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat Kematian</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("tempat_kematian")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Penyebab Kematian</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("penyebab_kematian")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
|
||||
digunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
<br />
|
||||
Pemohon
|
||||
<br />
|
||||
<br />
|
||||
<br />
|
||||
<br /> <br />
|
||||
<u>{getValue("nama_pelapor")}</u> <br />
|
||||
</div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,112 +1,179 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKPenghasilan({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b><u>SURAT KETERANGAN PENGHASILAN</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
{/* IDENTITAS */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Nama</td><td>:</td><td>{getValue("nama")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
|
||||
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
{/* PENGHASILAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan keterangan yang bersangkutan, orang tersebut memiliki penghasilan rata-rata:
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Penghasilan</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>
|
||||
Rp {getValue("penghasilan")}
|
||||
{" "}
|
||||
({getValue("penghasilan terbilang")}) per bulan
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* KEPERLUAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan permohonan")}</b>.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL & TANDA TANGAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN PENGHASILAN</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat / Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* PENGHASILAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Berdasarkan keterangan yang bersangkutan, orang tersebut memiliki
|
||||
penghasilan rata-rata:
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Penghasilan</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>Rp. {getValue("penghasilan")} per bulan</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* KEPERLUAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan")}</b>
|
||||
.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat
|
||||
dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
{/* TANGGAL & TANDA TANGAN */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "40px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /> <br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,90 +1,141 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKTempatUsaha({ data }: { data: any }) {
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
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>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* ISI */}
|
||||
<div>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
Yang bertanda tangan dibawah ini, saya:
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* 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>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
<br />
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
<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>
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* ISI */}
|
||||
<div>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
Yang bertanda tangan dibawah ini, saya:
|
||||
</div>
|
||||
|
||||
{/* DATA PEJABAT */}
|
||||
<div>
|
||||
<Row label="Nama" value={data.setting.perbekelNama} />
|
||||
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
|
||||
<Row label="Alamat" value={data.setting.desaAlamat} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
{/* DATA WARGA */}
|
||||
<div>
|
||||
<Row label="Nama Pemilik Usaha" value={getValue("nama_pemilik")} />
|
||||
<Row
|
||||
label="Tempat/Tanggal Lahir"
|
||||
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
|
||||
/>
|
||||
<Row
|
||||
label="Alamat Pemilik Usaha"
|
||||
value={getValue("alamat_pemilik")}
|
||||
/>
|
||||
<Row label="Nomor KTP" value={getValue("nik")} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Benar yang bersangkutan memiliki tempat usaha dengan keterangan
|
||||
seperti berikut:
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Row label="Nama Usaha" value={getValue("nama_usaha")} />
|
||||
<Row label="Bidang Usaha" value={getValue("bidang_usaha")} />
|
||||
<Row label="Alamat Usaha" value={getValue("alamat_usaha")} />
|
||||
<Row label="Status Tempat Usaha" value={getValue("status_tempat")} />
|
||||
<Row label="Luas Tempat Usaha" value={getValue("luas_usaha") + " m2"} />
|
||||
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
|
||||
</div>
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Surat keterangan ini dibuat untuk keperluan{" "}
|
||||
<b>{getValue("tujuan")}.</b>
|
||||
</p>
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat
|
||||
dipergunakan sebagaimana mestinya.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
|
||||
<Row label="Pada tanggal" value={data.surat.createdAt} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u>
|
||||
<br />
|
||||
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string, value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,81 +1,120 @@
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKTidakMampu({ data }: { data: any }) {
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TIDAK MAMPU</b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
{/* ISI */}
|
||||
<div>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
Yang bertanda tangan dibawah ini, saya
|
||||
</div>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* DATA PEJABAT */}
|
||||
<div>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
<Row label="Nama" value={data.setting.perbekelNama} />
|
||||
<Row label="Alamat" value={data.setting.desaAlamat} />
|
||||
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
{/* DATA WARGA */}
|
||||
<div>
|
||||
|
||||
<Row label="Nama" value={getValue("nama")} />
|
||||
<Row label="Tempat Tgl Lahir" value={getValue("tempat tanggal lahir")} />
|
||||
<Row label="Alamat" value={getValue("alamat")} />
|
||||
<Row label="NIK" value={getValue("nik")} />
|
||||
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan termasuk keluarga tidak mampu.
|
||||
Surat keterangan ini dipergunakan untuk
|
||||
<b>{getValue("alasan permohonan")}.</b>
|
||||
</p>
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Demikian surat keterangan ini kami buat dengan sebenar-benarnya untuk dapat dipergunakan
|
||||
sebagaimana mestinya.
|
||||
</p>
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{data.setting.desaNama}, {data.surat.createdAt} <br /><br /><br />
|
||||
|
||||
<u>{data.setting.perbekelNama}</u><br />
|
||||
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TIDAK MAMPU</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* ISI */}
|
||||
<div>
|
||||
<div style={{ marginBottom: "10px" }}>
|
||||
Yang bertanda tangan dibawah ini, saya
|
||||
</div>
|
||||
|
||||
{/* DATA PEJABAT */}
|
||||
<div>
|
||||
<Row label="Nama" value={data.setting.perbekelNama} />
|
||||
<Row label="Alamat" value={data.setting.desaAlamat} />
|
||||
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
{/* DATA WARGA */}
|
||||
<div>
|
||||
<Row label="NIK" value={getValue("nik")} />
|
||||
<Row label="Nama" value={getValue("nama")} />
|
||||
<Row
|
||||
label="Tempat Tgl Lahir"
|
||||
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
|
||||
/>
|
||||
<Row label="Alamat" value={getValue("alamat")} />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan
|
||||
termasuk keluarga tidak mampu. Surat keterangan ini dipergunakan untuk
|
||||
<b>{getValue("alasan")}.</b>
|
||||
</p>
|
||||
|
||||
<p style={{ textAlign: "justify" }}>
|
||||
Demikian surat keterangan ini kami buat dengan sebenar-benarnya untuk
|
||||
dapat dipergunakan sebagaimana mestinya.
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u>
|
||||
<br />
|
||||
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string, value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,148 +3,217 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKUsaha({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=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);
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "15px 0" }}>
|
||||
<b><u>SURAT KETERANGAN USAHA</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan dengan sesungguhnya bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
|
||||
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
|
||||
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
|
||||
<tr><td>Warga Negara</td><td>:</td><td>{getValue("negara")}</td></tr>
|
||||
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
|
||||
<tr><td>Status</td><td>:</td><td>{getValue("status perkawinan")}</td></tr>
|
||||
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
|
||||
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DOMISILI */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Bahwa orang tersebut di atas benar-benar penduduk:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Desa / Kelurahan</td><td style={{ width: "10px" }}>:</td><td>{data.setting.desaNama}</td></tr>
|
||||
<tr><td>Kecamatan</td><td>:</td><td>{data.setting.desaKecamatan}</td></tr>
|
||||
<tr><td>Kabupaten</td><td>:</td><td>{data.setting.desaKabupaten}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* USAHA */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan yang bersangkutan benar memiliki usaha:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr><td style={{ width: "160px" }}>Jenis Usaha</td><td style={{ width: "10px" }}>:</td><td>{getValue("jenis usaha")}</td></tr>
|
||||
<tr><td>Alamat Usaha</td><td>:</td><td>{getValue("alamat usaha")}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div style={{ marginTop: "20px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
|
||||
<br /><br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br /><br />
|
||||
<img src={viewImg} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}
|
||||
<br />
|
||||
Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* JUDUL */}
|
||||
<div style={{ textAlign: "center", margin: "15px 0" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN USAHA</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
{/* YANG BERTANDA TANGAN */}
|
||||
<div style={{ marginTop: "15px" }}>
|
||||
Yang bertanda tangan di bawah ini:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dengan ini menerangkan dengan sesungguhnya bahwa:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Nama</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat / Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Warga Negara</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("negara")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agama</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("agama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("status_perkawinan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* DOMISILI */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Bahwa orang tersebut di atas benar-benar penduduk:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Desa / Kelurahan</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kecamatan</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKecamatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Kabupaten</td>
|
||||
<td>:</td>
|
||||
<td>{data.setting.desaKabupaten}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* USAHA */}
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dan yang bersangkutan benar memiliki usaha:
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "160px" }}>Jenis Usaha</td>
|
||||
<td style={{ width: "10px" }}>:</td>
|
||||
<td>{getValue("jenis_usaha")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat Usaha</td>
|
||||
<td>:</td>
|
||||
<td>{getValue("alamat_usaha")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan
|
||||
sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
Dikeluarkan di {data.setting.desaNama} <br />
|
||||
Pada tanggal {data.surat.createdAt}
|
||||
</div>
|
||||
|
||||
{/* TANDA TANGAN */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<br />
|
||||
Kepala Desa / Lurah {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,163 +1,214 @@
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKYatim({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// Fetch manual agar mendapatkan Response asli
|
||||
const res = await fetch(urlApi);
|
||||
if (!res.ok)
|
||||
return notification({
|
||||
title: "Error",
|
||||
message: "Failed to load image sign",
|
||||
type: "error",
|
||||
});
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
|
||||
<b>DESA {_.upperCase(data.setting.desaNama)}</b><br />
|
||||
Alamat: {data.setting.desaAlamat}. Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
<div style={{ textAlign: "center", marginTop: "15px" }}>
|
||||
<b><u>SURAT KETERANGAN YATIM / PIATU / YATIM PIATU</u></b><br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN PENANDATANGAN */}
|
||||
<div>Yang bertanda tangan di bawah ini:</div>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>: {data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat Kantor</td>
|
||||
<td>: {data.setting.desaAlamat}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN IDENTITAS ANAK */}
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>: {getValue("tempat tanggal lahir")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>: {getValue("jenis kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>: {getValue("alamat")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>: {getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>: {getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* KETERANGAN ORANG TUA */}
|
||||
<div>
|
||||
Benar bahwa yang bersangkutan adalah <b>anak (Yatim / Piatu / Yatim Piatu)</b>,
|
||||
dengan keterangan sebagai berikut:
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div><b>1. Nama Ayah</b></div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ayah</td>
|
||||
<td>: {getValue("nama ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status ayah")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div><b>2. Nama Ibu</b></div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ibu</td>
|
||||
<td>: {getValue("nama ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status ibu")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Dengan demikian, berdasarkan keterangan pihak keluarga dan data di Kantor Desa,
|
||||
maka benar bahwa yang bersangkutan adalah
|
||||
<b> anak (Yatim / Piatu / Yatim Piatu).</b>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* TANGGAL & TEMPAT */}
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td>: {data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>: {data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br /><br />
|
||||
|
||||
{/* TTD */}
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br /><br /><br /><br />
|
||||
{data.setting.perbekelNama} <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
useShallowEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.3" }}>
|
||||
{/* HEADER */}
|
||||
<div style={{ textAlign: "center", marginBottom: "10px" }}>
|
||||
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
|
||||
<br />
|
||||
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
|
||||
<br />
|
||||
<b>DESA {_.upperCase(data.setting.desaNama)}</b>
|
||||
<br />
|
||||
Alamat: {data.setting.desaAlamat}. Kode Pos: {data.setting.desaPos}
|
||||
</div>
|
||||
);
|
||||
|
||||
<div style={{ textAlign: "center", marginTop: "15px" }}>
|
||||
<b>
|
||||
<u>SURAT KETERANGAN YATIM / PIATU / YATIM PIATU</u>
|
||||
</b>
|
||||
<br />
|
||||
Nomor: {data.surat.noSurat}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN PENANDATANGAN */}
|
||||
<div>Yang bertanda tangan di bawah ini:</div>
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {data.setting.perbekelNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jabatan</td>
|
||||
<td>: {data.setting.perbekelJabatan}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat Kantor</td>
|
||||
<td>: {data.setting.desaAlamat}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* BAGIAN IDENTITAS ANAK */}
|
||||
<div>Dengan ini menerangkan bahwa:</div>
|
||||
|
||||
<table style={{ width: "100%", marginTop: "5px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>NIK</td>
|
||||
<td>: {getValue("nik")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama</td>
|
||||
<td>: {getValue("nama")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tempat/Tanggal Lahir</td>
|
||||
<td>
|
||||
: {`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Jenis Kelamin</td>
|
||||
<td>: {getValue("jenis_kelamin")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Alamat</td>
|
||||
<td>: {getValue("alamat")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
<td>: {getValue("pekerjaan")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* KETERANGAN ORANG TUA */}
|
||||
<div>
|
||||
Benar bahwa yang bersangkutan adalah{" "}
|
||||
<b>anak (Yatim / Piatu / Yatim Piatu)</b>, dengan keterangan sebagai
|
||||
berikut:
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<b>1. Nama Ayah</b>
|
||||
</div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ayah</td>
|
||||
<td>: {getValue("nama_ayah")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status_ayah")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<b>2. Nama Ibu</b>
|
||||
</div>
|
||||
<table style={{ width: "100%" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Nama Ibu</td>
|
||||
<td>: {getValue("nama_ibu")}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>: {getValue("status_ibu")}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Dengan demikian, berdasarkan keterangan pihak keluarga dan data di
|
||||
Kantor Desa, maka benar bahwa yang bersangkutan adalah
|
||||
<b> anak (Yatim / Piatu / Yatim Piatu).</b>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
Surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan
|
||||
sebagaimana mestinya.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
{/* TANGGAL & TEMPAT */}
|
||||
<table style={{ width: "100%", marginTop: "10px" }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{ width: "180px" }}>Dikeluarkan di</td>
|
||||
<td>: {data.setting.desaNama}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pada tanggal</td>
|
||||
<td>: {data.surat.createdAt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
{/* TTD */}
|
||||
<div
|
||||
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
|
||||
>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
Kepala Desa {data.setting.desaNama}
|
||||
<br />
|
||||
<br />
|
||||
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
|
||||
<br />
|
||||
<u>{data.setting.perbekelNama}</u> <br />
|
||||
NIP. {data.setting.perbekelNIP}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,16 @@ import Auth from "./server/routes/auth_route";
|
||||
import ConfigurationDesaRoute from "./server/routes/configuration_desa_route";
|
||||
import CredentialRoute from "./server/routes/credential_route";
|
||||
import DarmasabaRoute from "./server/routes/darmasaba_route";
|
||||
import DashboardRoute from "./server/routes/dashboard_route";
|
||||
import LayananRoute from "./server/routes/layanan_route";
|
||||
import { MCPRoute } from "./server/routes/mcp_route";
|
||||
import PelayananRoute from "./server/routes/pelayanan_surat_route";
|
||||
import PengaduanRoute from "./server/routes/pengaduan_route";
|
||||
import SendWaRoute from "./server/routes/send_wa_route";
|
||||
import SuratRoute from "./server/routes/surat_route";
|
||||
import TestPengaduanRoute from "./server/routes/test_pengaduan";
|
||||
import UserRoute from "./server/routes/user_route";
|
||||
import WargaRoute from "./server/routes/warga_route";
|
||||
import SuratRoute from "./server/routes/surat_route";
|
||||
|
||||
const Docs = new Elysia({
|
||||
tags: ["docs"],
|
||||
@@ -31,6 +33,7 @@ const Api = new Elysia({
|
||||
prefix: "/api",
|
||||
tags: ["api"],
|
||||
})
|
||||
.use(DashboardRoute)
|
||||
.use(PengaduanRoute)
|
||||
.use(PelayananRoute)
|
||||
.use(ConfigurationDesaRoute)
|
||||
@@ -43,7 +46,8 @@ const Api = new Elysia({
|
||||
.use(CredentialRoute)
|
||||
.use(UserRoute)
|
||||
.use(LayananRoute)
|
||||
.use(AduanRoute);
|
||||
.use(AduanRoute)
|
||||
.use(SendWaRoute);
|
||||
|
||||
const app = new Elysia()
|
||||
.use(Api)
|
||||
|
||||
@@ -1,109 +1,384 @@
|
||||
import { enumAgama, enumJenisKelamin, enumStatusHidup, enumStatusPerkawinan, enumStatusTempatUsaha } from "./valueEnum";
|
||||
|
||||
export const categoryPelayananSurat = [
|
||||
{
|
||||
id: "skbedabiodata",
|
||||
name: "Surat Keterangan Beda Biodata Diri",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||
{ name: "dokumen yang beda", desc: "Fotokopi dokumen bersangkutan yang terdapat perbedaan biodata diri, misalnya: Sertifikat Tanah, Ijazah, Polis Asuransi, dan lainnya." }
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing",
|
||||
required: true, satuan: null
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga",
|
||||
required: true, satuan: null
|
||||
},
|
||||
{
|
||||
key: "dokumen_beda",
|
||||
name: "Dokumen Pendukung",
|
||||
desc: "Fotokopi dokumen yang terdapat perbedaan biodata (ijazah, sertifikat, dll)",
|
||||
required: true, satuan: null
|
||||
}
|
||||
],
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "dokumen", "tertulis pada dokumen a", "tertulis pada dokumen b"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "dokumen",
|
||||
name: "Nama Dokumen",
|
||||
desc: "Jenis dokumen yang mengalami perbedaan biodata",
|
||||
type: "text",
|
||||
required: true, satuan: null
|
||||
},
|
||||
{
|
||||
key: "data_dokumen",
|
||||
name: "Data Dokumen",
|
||||
desc: "Data dokumen yg berbeda",
|
||||
type: "text",
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "dokumen_a", name: "Data pada Dokumen A", desc: "Data biodata pada dokumen pertama", type: "text", required: true, satuan: null },
|
||||
{ key: "dokumen_b", name: "Data pada Dokumen B", desc: "Data biodata pada dokumen kedua", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skbelumkawin",
|
||||
name: "Surat Keterangan Belum Kawin",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau Kartu Keluarga", required: true, satuan: null },
|
||||
{ key: "akta_cerai", name: "Akta Cerai", desc: "Fotokopi akta cerai (jika berstatus janda/duda)", required: false, satuan: null }
|
||||
],
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "status perkawinan"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama pemohon",
|
||||
type: "enum",
|
||||
options: enumAgama,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "skdomisiliorganisasi",
|
||||
name: "Surat Keterangan Domisili Organisasi",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
|
||||
{ name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "skt_organisasi", name: "SKT Organisasi", desc: "Fotokopi SKT Organisasi", required: true, satuan: null },
|
||||
{ key: "susunan_pengurus", name: "Susunan Pengurus", desc: "Susunan pengurus organisasi", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nama organisasi", "alamat organisasi", "nama pemohon", "jabatan pemohon", "kontak", "penanggung jawab", "tanggal berdiri"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text", required: true, satuan: null },
|
||||
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis organisasi", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat", type: "text", required: true, satuan: null },
|
||||
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text", required: true, satuan: null },
|
||||
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan", type: "text", required: true, satuan: null },
|
||||
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skkelahiran",
|
||||
name: "Surat Keterangan Kelahiran",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "surat lahir", desc: "Fotokopi Surat Keterangan Lahir dari Bidan/Dokter (jika ada)" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "surat_lahir", name: "Surat Keterangan Lahir", desc: "Surat keterangan lahir dari bidan/dokter (jika ada)", required: false, satuan: null }
|
||||
],
|
||||
dataText: ["nama ayah", "nama ibu", "nama anak", "tanggal lahir anak", "pukul lahir anak", "tempat lahir anak", "jenis kelamin anak", "anak ke", "nik ibu", "tempat tanggal lahir ibu", "pekerjaan ibu", "alamat ibu", "nik ayah", "tempat tanggal lahir ayah", "pekerjaan ayah", "alamat ayah", "nama pelapor", "hubungan pelapor", "alamat pelapor"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date", required: true, satuan: null },
|
||||
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin Anak",
|
||||
desc: "Jenis kelamin anak",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran", type: "number", required: true, satuan: null },
|
||||
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number", required: true, satuan: null },
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu", type: "date", required: true, satuan: null },
|
||||
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu", type: "text", required: true, satuan: null },
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text", required: true, satuan: null },
|
||||
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number", required: true, satuan: null },
|
||||
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah", type: "date", required: true, satuan: null },
|
||||
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah", type: "text", required: true, satuan: null },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan anak", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skkelakuanbaik",
|
||||
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "polsek"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama",
|
||||
type: "enum",
|
||||
options: enumAgama,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
|
||||
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skkematian",
|
||||
name: "Surat Keterangan Kematian",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null },
|
||||
{ key: "surat_kematian", name: "Surat Keterangan Kematian", desc: "Surat keterangan kematian dari rumah sakit/dokter (jika ada)", required: false, satuan: null }
|
||||
],
|
||||
dataText: ["nama almarhum", "nik", "tempat tanggal lahir", "alamat", "tanggal kematian", "waktu kematian", "penyebab kematian"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "NIK pelapor", type: "number", required: true, satuan: null },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan almarhum", type: "text", required: true, satuan: null },
|
||||
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama almarhum", type: "text", required: true, satuan: null },
|
||||
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "NIK almarhum", type: "number", required: true, satuan: null },
|
||||
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir", desc: "Tempat lahir almarhum", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir", desc: "Tanggal lahir almarhum", type: "date", required: true, satuan: null },
|
||||
{ key: "alamat_almarhum", name: "Alamat", desc: "Alamat terakhir", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "agama_almarhum",
|
||||
name: "Agama Almarhum",
|
||||
desc: "Agama almarhum",
|
||||
type: "enum",
|
||||
options: enumAgama,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date", required: true, satuan: null },
|
||||
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text", required: true, satuan: null },
|
||||
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skpenghasilan",
|
||||
name: "Surat Keterangan Penghasilan",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
|
||||
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_ortu_kk", name: "KTP Orang Tua / KK", desc: "Fotokopi KTP orang tua/KK", required: true, satuan: null },
|
||||
{ key: "surat_pernyataan", name: "Surat Pernyataan Penghasilan", desc: "Surat pernyataan penghasilan bermaterai", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nama", "nik", "alamat", "pekerjaan", "jenis usaha", "penghasilan", "alasan permohonan"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number", required: true, satuan: "/Bulan" },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sktempatusaha",
|
||||
name: "Surat Keterangan Tempat Usaha",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
|
||||
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
|
||||
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null },
|
||||
{ key: "dokumen_lahan", name: "Dokumen Lahan", desc: "SPPT/Sertifikat/surat sewa tempat usaha", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "NIK pemilik", type: "number", required: true, satuan: null },
|
||||
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik", type: "text", required: true, satuan: null },
|
||||
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text", required: true, satuan: null },
|
||||
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang usaha", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "status_tempat",
|
||||
name: "Status Tempat Usaha",
|
||||
desc: "Status kepemilikan tempat usaha",
|
||||
type: "enum",
|
||||
options: enumStatusTempatUsaha,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number", required: true, satuan: "m²" },
|
||||
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah karyawan", type: "number", required: true, satuan: null },
|
||||
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sktidakmampu",
|
||||
name: "Surat Keterangan Tidak Mampu",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nik", "nama", "tempat tanggal lahir", "alamat", "alasan permohonan"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "NIK pemohon", type: "number", required: true, satuan: null },
|
||||
{ key: "nama Lengkap", name: "Nama", desc: "Nama pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat pemohon", type: "text", required: true, satuan: null },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan permohonan", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skusaha",
|
||||
name: "Surat Keterangan Usaha",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
|
||||
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
|
||||
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemilik usaha", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemilik usaha",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama",
|
||||
type: "enum",
|
||||
options: enumAgama,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{
|
||||
key: "status_perkawinan",
|
||||
name: "Status Perkawinan",
|
||||
desc: "Status perkawinan",
|
||||
type: "enum",
|
||||
options: enumStatusPerkawinan,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
|
||||
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha", type: "text", required: true, satuan: null },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skyatimpiatu",
|
||||
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
|
||||
syaratDokumen: [
|
||||
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
|
||||
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
|
||||
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
|
||||
],
|
||||
dataText: ["nama anak", "nama ayah", "status ayah", "nama ibu", "status ibu"]
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "NIK anak", type: "number", required: true, satuan: null },
|
||||
{ key: "nama", name: "Nama", desc: "Nama anak", type: "text", required: true, satuan: null },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin anak",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text", required: false, satuan: null },
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "status_ayah",
|
||||
name: "Status Ayah",
|
||||
desc: "Status ayah",
|
||||
type: "enum",
|
||||
options: enumStatusHidup,
|
||||
required: true, satuan: null
|
||||
},
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu", type: "text", required: true, satuan: null },
|
||||
{
|
||||
key: "status_ibu",
|
||||
name: "Status Ibu",
|
||||
desc: "Status ibu",
|
||||
type: "enum",
|
||||
options: enumStatusHidup,
|
||||
required: true, satuan: null
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
59
src/lib/groupPermission.ts
Normal file
59
src/lib/groupPermission.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import config from "@/lib/listPermission.json";
|
||||
|
||||
export interface PermissionNode {
|
||||
key: string;
|
||||
label: string;
|
||||
children?: PermissionNode[];
|
||||
}
|
||||
|
||||
interface Grouped {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
children: Grouped;
|
||||
actions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/* --- Build lookup table --- */
|
||||
const permissionMap: Record<string, string[]> = {};
|
||||
|
||||
function walk(nodes: PermissionNode[], path: string[] = []) {
|
||||
nodes.forEach((n) => {
|
||||
const full = [...path, n.label];
|
||||
permissionMap[n.key] = full;
|
||||
if (n.children) walk(n.children, full);
|
||||
});
|
||||
}
|
||||
|
||||
walk(config.menus);
|
||||
|
||||
/* --- Convert keys → hierarchical grouped --- */
|
||||
export function groupPermissions(keys: string[]) {
|
||||
const tree: Grouped = {};
|
||||
|
||||
keys.forEach((key) => {
|
||||
const path = permissionMap[key];
|
||||
if (!path) return;
|
||||
|
||||
let pointer = tree;
|
||||
|
||||
path.forEach((label, idx) => {
|
||||
if (!pointer[label]) {
|
||||
pointer[label] = {
|
||||
label,
|
||||
children: {},
|
||||
actions: []
|
||||
};
|
||||
}
|
||||
|
||||
// last item = actual permission action
|
||||
if (idx === path.length - 1) {
|
||||
pointer[label].actions.push(label);
|
||||
}
|
||||
|
||||
pointer = pointer[label].children;
|
||||
});
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
300
src/lib/listPermission.json
Normal file
300
src/lib/listPermission.json
Normal file
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"menus": [
|
||||
{
|
||||
"key": "dashboard",
|
||||
"label": "Dashboard",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "dashboard.view",
|
||||
"label": "Melihat Dashboard",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan",
|
||||
"label": "Pengaduan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.antrian",
|
||||
"label": "Detail pengaduan dengan status antrian",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.antrian.tolak",
|
||||
"label": "Menolak pengaduan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.antrian.terima",
|
||||
"label": "Menerima pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.diterima",
|
||||
"label": "Detail pengaduan dengan status diterima",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.diterima.dikerjakan",
|
||||
"label": "Menegerjakan pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pengaduan.dikerjakan",
|
||||
"label": "Detail pengaduan dengan status dikerjakan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pengaduan.dikerjakan.selesai",
|
||||
"label": "Menyelesaikan pengaduan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pelayanan",
|
||||
"label": "Pelayanan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.antrian",
|
||||
"label": "Detail pelayanan dengan status antrian",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.antrian.tolak",
|
||||
"label": "Menolak pelayanan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.antrian.terima",
|
||||
"label": "Menerima pelayanan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.diterima",
|
||||
"label": "Detail pelayanan dengan status diterima",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "pelayanan.diterima.tolak",
|
||||
"label": "Menolak pelayanan",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "pelayanan.diterima.setujui",
|
||||
"label": "Menyetujui pelayanan",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "warga",
|
||||
"label": "Warga",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "warga.view",
|
||||
"label": "Melihat List & Detail",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting",
|
||||
"label": "Setting",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.profile",
|
||||
"label": "Profile",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.profile.view",
|
||||
"label": "View",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.profile.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.profile.password",
|
||||
"label": "Ubah Password",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.user",
|
||||
"label": "User",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.user.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role",
|
||||
"label": "User Role",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.user_role.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.user_role.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan",
|
||||
"label": "Kategori Pengaduan",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.tambah",
|
||||
"label": "Tambah",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pengaduan.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan",
|
||||
"label": "Kategori Pelayanan Surat",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.detail",
|
||||
"label": "View Detail",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.kategori_pelayanan.delete",
|
||||
"label": "Delete",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "setting.desa",
|
||||
"label": "Desa",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "setting.desa.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"key": "setting.desa.edit",
|
||||
"label": "Edit",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "api_key",
|
||||
"label": "API Key",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "api_key.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "credential",
|
||||
"label": "Credential",
|
||||
"default": true,
|
||||
"children": [
|
||||
{
|
||||
"key": "credential.view",
|
||||
"label": "View List",
|
||||
"default": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
31
src/lib/valueEnum.ts
Normal file
31
src/lib/valueEnum.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const enumJenisKelamin = [
|
||||
{ label: "Laki-laki", value: "Laki-laki" },
|
||||
{ label: "Perempuan", value: "Perempuan" }
|
||||
];
|
||||
|
||||
export const enumAgama = [
|
||||
{ label: "Islam", value: "Islam" },
|
||||
{ label: "Kristen", value: "Kristen" },
|
||||
{ label: "Katolik", value: "Katolik" },
|
||||
{ label: "Hindu", value: "Hindu" },
|
||||
{ label: "Buddha", value: "Buddha" },
|
||||
{ label: "Konghucu", value: "Konghucu" }
|
||||
];
|
||||
|
||||
export const enumStatusHidup = [
|
||||
{ label: "Hidup", value: "Hidup" },
|
||||
{ label: "Meninggal", value: "Meninggal" }
|
||||
];
|
||||
|
||||
export const enumStatusPerkawinan = [
|
||||
{ label: "Belum Kawin", value: "Belum Kawin" },
|
||||
{ label: "Kawin", value: "Kawin" },
|
||||
{ label: "Cerai Hidup", value: "Cerai Hidup" },
|
||||
{ label: "Cerai Mati", value: "Cerai Mati" }
|
||||
];
|
||||
|
||||
export const enumStatusTempatUsaha = [
|
||||
{ label: "Milik Sendiri", value: "Milik Sendiri" },
|
||||
{ label: "Sewa", value: "Sewa" },
|
||||
{ label: "Pinjam", value: "Pinjam" }
|
||||
];
|
||||
@@ -1,20 +1,53 @@
|
||||
import clientRoutes from "@/clientRoutes";
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Center,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
TextInput
|
||||
} from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
import apiFetch from "../lib/apiFetch";
|
||||
import clientRoutes from "@/clientRoutes";
|
||||
|
||||
export default function Login() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
function navigateToRoute(akses: string) {
|
||||
switch (akses) {
|
||||
case "dashboard":
|
||||
window.location.href = clientRoutes["/scr/dashboard/dashboard-home"];
|
||||
break;
|
||||
case "pengaduan":
|
||||
window.location.href = clientRoutes["/scr/dashboard/pengaduan/list"];
|
||||
break;
|
||||
case "warga":
|
||||
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
|
||||
break;
|
||||
case "credential":
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/credential/credential"];
|
||||
break;
|
||||
case "setting":
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/setting/detail-setting"];
|
||||
break;
|
||||
case "api_key":
|
||||
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
|
||||
break;
|
||||
case "pelayanan":
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
|
||||
break;
|
||||
default:
|
||||
window.location.href = clientRoutes["/scr/dashboard"];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -25,7 +58,7 @@ export default function Login() {
|
||||
|
||||
if (response.data?.token) {
|
||||
localStorage.setItem("token", response.data.token);
|
||||
window.location.href = clientRoutes["/scr/dashboard"];
|
||||
navigateToRoute(response.data.akses || "dashboard");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,25 +73,73 @@ export default function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
<Text>Login</Text>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<Group justify="right">
|
||||
<Button onClick={handleSubmit} disabled={loading}>
|
||||
<Center
|
||||
h="100vh"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
radius="lg"
|
||||
p="xl"
|
||||
w={420}
|
||||
style={{
|
||||
background: "rgba(20, 20, 20, 0.75)",
|
||||
backdropFilter: "blur(12px)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||
boxShadow: "0 20px 60px rgba(0,0,0,0.6)",
|
||||
}}
|
||||
>
|
||||
<Stack>
|
||||
<Text
|
||||
size="xl"
|
||||
fw={700}
|
||||
ta="center"
|
||||
c="white"
|
||||
>
|
||||
Welcome Back
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
size="sm"
|
||||
ta="center"
|
||||
c="dimmed"
|
||||
>
|
||||
Sign in to continue to your dashboard
|
||||
</Text>
|
||||
|
||||
<TextInput
|
||||
label="Email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
mt="md"
|
||||
radius="md"
|
||||
size="md"
|
||||
variant="gradient"
|
||||
gradient={{ from: "teal", to: "cyan", deg: 45 }}
|
||||
style={{
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
707
src/pages/darmasaba/surat.tsx
Normal file
707
src/pages/darmasaba/surat.tsx
Normal file
@@ -0,0 +1,707 @@
|
||||
import FullScreenLoading from "@/components/FullScreenLoading";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { fromSlug, toSlug } from "@/server/lib/slug_converter";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
FileInput,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconBuildingCommunity,
|
||||
IconCategory,
|
||||
IconFiles,
|
||||
IconInfoCircle,
|
||||
IconNotes,
|
||||
IconPhone,
|
||||
IconUpload
|
||||
} from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/id";
|
||||
import React, { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
type DataItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
kategoriId: string;
|
||||
nama: string;
|
||||
phone: string;
|
||||
dataPelengkap: DataItem[];
|
||||
syaratDokumen: DataItem[];
|
||||
};
|
||||
|
||||
type ErrorState = Record<string, string | null>;
|
||||
|
||||
export default function FormSurat() {
|
||||
const [errors, setErrors] = useState<ErrorState>({});
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [noPengajuan, setNoPengajuan] = useState("");
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const jenisSurat = query.get("jenis");
|
||||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||||
apiFetch.api.pelayanan.category.get(),
|
||||
);
|
||||
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
||||
const [dataSurat, setDataSurat] = useState<any>({});
|
||||
const [formSurat, setFormSurat] = useState<FormSurat>({
|
||||
nama: "",
|
||||
phone: "",
|
||||
kategoriId: "",
|
||||
dataPelengkap: [],
|
||||
syaratDokumen: [],
|
||||
});
|
||||
|
||||
const listCategory = data?.data || [];
|
||||
|
||||
function onResetAll() {
|
||||
setNoPengajuan("");
|
||||
setSubmitLoading(false);
|
||||
setFormSurat({
|
||||
nama: "",
|
||||
phone: "",
|
||||
kategoriId: "",
|
||||
dataPelengkap: [],
|
||||
syaratDokumen: [],
|
||||
});
|
||||
}
|
||||
|
||||
function onGetJenisSurat() {
|
||||
try {
|
||||
if (!jenisSurat || jenisSurat == "null") {
|
||||
setJenisSuratFix({ name: "", id: "" });
|
||||
} else {
|
||||
const namaJenis = fromSlug(jenisSurat);
|
||||
const data = listCategory.find((item: any) => item.name.toUpperCase() == namaJenis.toUpperCase());
|
||||
if (!data) return;
|
||||
setJenisSuratFix(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDetailJenisSurat() {
|
||||
try {
|
||||
const get: any = await apiFetch.api.pelayanan.category.detail.get({
|
||||
query: {
|
||||
id: jenisSuratFix.id,
|
||||
},
|
||||
});
|
||||
|
||||
setDataSurat(get.data);
|
||||
setFormSurat({
|
||||
kategoriId: jenisSuratFix.id,
|
||||
nama: "",
|
||||
phone: "",
|
||||
dataPelengkap: (get.data?.dataPelengkap || []).map(
|
||||
(item: { key: string, required: boolean }) => ({
|
||||
key: item.key,
|
||||
value: "",
|
||||
required: item.required
|
||||
}),
|
||||
),
|
||||
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||||
(item: { key: string, required: boolean }) => ({
|
||||
key: item.key,
|
||||
value: "",
|
||||
required: item.required
|
||||
}),
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (listCategory.length > 0) {
|
||||
onGetJenisSurat();
|
||||
}
|
||||
}, [jenisSurat, listCategory]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (jenisSuratFix.id != "") {
|
||||
getDetailJenisSurat();
|
||||
}
|
||||
}, [jenisSuratFix.id]);
|
||||
|
||||
function onChecking() {
|
||||
const hasError = Object.values(errors).some((v) => v);
|
||||
|
||||
if (hasError) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Masih ada form yang belum valid",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
const isFormKosong = Object.values(formSurat).some((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(
|
||||
(item) =>
|
||||
(typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
|
||||
);
|
||||
}
|
||||
return typeof value === "string" && value.trim() === "";
|
||||
});
|
||||
|
||||
if (isFormKosong) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Silahkan lengkapi form surat",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
setSubmitLoading(true);
|
||||
// 🔥 CLONE state SEKALI
|
||||
let finalFormSurat = structuredClone(formSurat);
|
||||
|
||||
// 2️⃣ Upload satu per satu
|
||||
for (const itemUpload of finalFormSurat.syaratDokumen) {
|
||||
const updImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: itemUpload.value,
|
||||
folder: "syarat-dokumen",
|
||||
});
|
||||
|
||||
if (updImg.status === 200) {
|
||||
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
|
||||
finalFormSurat.syaratDokumen = updateArrayByKey(
|
||||
finalFormSurat.syaratDokumen,
|
||||
itemUpload.key,
|
||||
updImg.data?.filename || "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ SET STATE SEKALI (optional, untuk UI)
|
||||
setFormSurat(finalFormSurat);
|
||||
|
||||
// 4️⃣ SUBMIT KE API
|
||||
const res = await apiFetch.api.pelayanan.create.post(finalFormSurat);
|
||||
|
||||
if (res.status === 200) {
|
||||
setNoPengajuan(res.data?.noPengajuan || "");
|
||||
} else {
|
||||
notification({
|
||||
title: "Gagal",
|
||||
message:
|
||||
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification({
|
||||
title: "Gagal",
|
||||
message: "Server Error",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function updateArrayByKey(
|
||||
list: DataItem[],
|
||||
targetKey: string,
|
||||
value: string,
|
||||
): DataItem[] {
|
||||
return list.map((item) =>
|
||||
item.key === targetKey ? { ...item, value } : item,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function validateField(key: string, value: any) {
|
||||
const stringValue = String(value ?? "").trim();
|
||||
|
||||
// wajib diisi
|
||||
if (!stringValue) {
|
||||
return "Field wajib diisi";
|
||||
}
|
||||
|
||||
// 🔥 semua key yang mengandung "nik"
|
||||
if (key.toLowerCase().includes("nik")) {
|
||||
if (!/^\d+$/.test(stringValue)) {
|
||||
return "NIK harus berupa angka";
|
||||
}
|
||||
|
||||
if (stringValue.length !== 16) {
|
||||
return "NIK harus 16 digit";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function validationForm({
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
|
||||
value: any;
|
||||
}) {
|
||||
if (key === "dataPelengkap" || key === "syaratDokumen") {
|
||||
if (value.required == true) {
|
||||
const errorMsg = validateField(value.key, value.value);
|
||||
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[value.key]: errorMsg,
|
||||
}));
|
||||
}
|
||||
setFormSurat((prev) => ({
|
||||
...prev,
|
||||
[key]: updateArrayByKey(prev[key], value.key, value.value),
|
||||
}));
|
||||
} else {
|
||||
const keyFix = key == "nama" ? "nama_kontak" : key;
|
||||
const errorMsg = validateField(keyFix, value);
|
||||
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[keyFix]: errorMsg,
|
||||
}));
|
||||
|
||||
setFormSurat({
|
||||
...formSurat,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"} pb={"lg"}>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Konfirmasi"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text>Apakah anda yakin ingin mengirim pengajuan surat ini?</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
onSubmit();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<FullScreenLoading visible={submitLoading} />
|
||||
{noPengajuan != "" ? (
|
||||
<SuccessPengajuan
|
||||
noPengajuan={noPengajuan}
|
||||
onClose={() => {
|
||||
onResetAll();
|
||||
navigate("/darmasaba/surat");
|
||||
}}
|
||||
category="create"
|
||||
/>
|
||||
) : (
|
||||
<Box>
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" align="center">
|
||||
<Group align="center">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<div>
|
||||
<Text fw={800} size="xl">
|
||||
Layanan Pengajuan Surat Administrasi
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Formulir resmi untuk mengajukan berbagai jenis surat
|
||||
administrasi desa/kelurahan secara online.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group>
|
||||
<Badge radius="sm">Form Length: 4 Sections</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
<FormSection
|
||||
title="Jenis Surat Pengajuan"
|
||||
icon={<IconCategory size={16} />}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Jenis Surat"
|
||||
hint="Jenis surat yang ingin diajukan"
|
||||
/>
|
||||
}
|
||||
placeholder="Pilih jenis surat"
|
||||
data={listCategory.map((item: any) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}))}
|
||||
value={jenisSuratFix.name == "" ? null : jenisSuratFix.name}
|
||||
onChange={(value) => {
|
||||
const slug = toSlug(String(value));
|
||||
navigate("/darmasaba/surat?jenis=" + slug);
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{/* Kontak Section */}
|
||||
<FormSection
|
||||
title="Kontak"
|
||||
icon={<IconPhone size={16} />}
|
||||
description="Informasi kontak yg dapat dihubungi"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Nama" hint="Nama kontak" required />}
|
||||
placeholder="Budi Setiawan"
|
||||
value={formSurat.nama}
|
||||
error={errors.nama_kontak}
|
||||
onChange={(e) =>
|
||||
validationForm({ key: "nama", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
required
|
||||
label="Nomor Telephone"
|
||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||||
/>
|
||||
}
|
||||
placeholder="08123456789"
|
||||
value={formSurat.phone}
|
||||
error={errors.phone}
|
||||
type="number"
|
||||
onChange={(e) =>
|
||||
validationForm({ key: "phone", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{jenisSuratFix.id != "" &&
|
||||
dataSurat &&
|
||||
dataSurat.dataPelengkap && (
|
||||
<>
|
||||
<FormSection
|
||||
title="Data Yang Diperlukan"
|
||||
description="Data yang diperlukan untuk mengajukan surat"
|
||||
icon={<IconNotes size={16} />}
|
||||
>
|
||||
<Grid>
|
||||
{dataSurat.dataPelengkap.map(
|
||||
(item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
{item.type == "enum" ? (
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label={
|
||||
<FieldLabel
|
||||
label={item.name}
|
||||
hint={item.desc}
|
||||
required={item.required}
|
||||
/>
|
||||
}
|
||||
data={item.options ?? []}
|
||||
placeholder={item.name}
|
||||
onChange={(e) => {
|
||||
validationForm({
|
||||
key: "dataPelengkap",
|
||||
value: { key: item.key, value: e, required: item.required },
|
||||
});
|
||||
}}
|
||||
value={
|
||||
formSurat.dataPelengkap.find(
|
||||
(n: any) => n.key == item.key,
|
||||
)?.value
|
||||
}
|
||||
/>
|
||||
) : item.type == "date" ? (
|
||||
<DateInput
|
||||
locale="id"
|
||||
valueFormat="DD MMMM YYYY"
|
||||
label={
|
||||
<FieldLabel
|
||||
label={item.name}
|
||||
hint={item.desc}
|
||||
required={item.required}
|
||||
/>
|
||||
}
|
||||
placeholder={item.name}
|
||||
onChange={(e) => {
|
||||
const formatted = e
|
||||
? dayjs(e)
|
||||
.locale("id")
|
||||
.format("DD MMMM YYYY")
|
||||
: "";
|
||||
validationForm({
|
||||
key: "dataPelengkap",
|
||||
value: {
|
||||
key: item.key,
|
||||
value: formatted,
|
||||
required: item.required,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
error={errors[item.key]}
|
||||
type={item.type}
|
||||
label={
|
||||
<FieldLabel
|
||||
label={item.name}
|
||||
hint={item.desc}
|
||||
required={item.required}
|
||||
/>
|
||||
}
|
||||
placeholder={item.name}
|
||||
onChange={(e) =>
|
||||
validationForm({
|
||||
key: "dataPelengkap",
|
||||
value: {
|
||||
key: item.key,
|
||||
value: e.target.value,
|
||||
required: item.required,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={
|
||||
formSurat.dataPelengkap.find(
|
||||
(n: any) => n.key == item.key,
|
||||
)?.value
|
||||
}
|
||||
rightSection={
|
||||
item.satuan != null &&
|
||||
<Text mr={"lg"}>{item.satuan}</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Grid.Col>
|
||||
),
|
||||
)}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Syarat Dokumen"
|
||||
description="Syarat dokumen yang diperlukan"
|
||||
icon={<IconFiles size={16} />}
|
||||
>
|
||||
<Grid>
|
||||
{dataSurat.syaratDokumen.map(
|
||||
(item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<FileInputWrapper
|
||||
label={item.desc}
|
||||
placeholder={"Upload file "}
|
||||
accept="image/*,application/pdf"
|
||||
onChange={(file) =>
|
||||
validationForm({
|
||||
key: "syaratDokumen",
|
||||
value: { key: item.key, value: file },
|
||||
})
|
||||
}
|
||||
name={item.name}
|
||||
required={item.required}
|
||||
/>
|
||||
</Grid.Col>
|
||||
),
|
||||
)}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{/* Actions */}
|
||||
<Group justify="right" mt="md">
|
||||
{/* <Button variant="default" onClick={() => { }}>
|
||||
Reset
|
||||
</Button> */}
|
||||
<Button onClick={onChecking}>Kirim</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
|
||||
return (
|
||||
<Group justify="apart" gap="xs" align="center">
|
||||
<Group gap={4} align="center">
|
||||
<Text fw={600}>
|
||||
{label}
|
||||
{required && (
|
||||
<Text span c="red" ml={4}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{hint && (
|
||||
<Tooltip label={hint} withArrow>
|
||||
<ActionIcon size={24} variant="subtle">
|
||||
<IconInfoCircle size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card radius="md" shadow="sm" withBorder>
|
||||
<Group justify="apart" align="center" mb="xs">
|
||||
<Group align="center" gap="xs">
|
||||
{icon}
|
||||
<Text fw={700}>{title}</Text>
|
||||
</Group>
|
||||
{description && <Badge variant="light">{description}</Badge>}
|
||||
</Group>
|
||||
|
||||
<Divider mb="sm" />
|
||||
<Stack gap="sm">{children}</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FileInputWrapper({
|
||||
label,
|
||||
placeholder,
|
||||
accept,
|
||||
onChange,
|
||||
preview,
|
||||
name,
|
||||
description,
|
||||
required = false,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
accept?: string;
|
||||
onChange: (file: File | null) => void;
|
||||
preview?: string | null;
|
||||
name: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Flex direction={"column"}>
|
||||
<Group justify="apart" align="center">
|
||||
<Text fw={500}>
|
||||
{label}
|
||||
{required && (
|
||||
<Text span c="red" ml={4}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<FileInput
|
||||
accept={accept}
|
||||
placeholder={placeholder}
|
||||
onChange={(f) => onChange(f)}
|
||||
leftSection={<IconUpload />}
|
||||
aria-label={label}
|
||||
name={name}
|
||||
clearable={true}
|
||||
/>
|
||||
|
||||
{preview ? (
|
||||
<div>
|
||||
<Text size="xs" color="dimmed">
|
||||
Preview:
|
||||
</Text>
|
||||
{/* If preview is an image it will show; pdf preview might not render as image */}
|
||||
{/* Use <object> or <img> depending on file type — keep simple here */}
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<img
|
||||
src={preview}
|
||||
alt={`${label} preview`}
|
||||
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
837
src/pages/darmasaba/update_data_surat.tsx
Normal file
837
src/pages/darmasaba/update_data_surat.tsx
Normal file
@@ -0,0 +1,837 @@
|
||||
import FullScreenLoading from "@/components/FullScreenLoading";
|
||||
import ModalFile from "@/components/ModalFile";
|
||||
import { DataNotFound } from "@/components/NotFoundPengajuanSurat";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { parseTanggalID } from "@/server/lib/stringToDate";
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Anchor,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
FileInput,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Modal,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconBuildingCommunity,
|
||||
IconFiles,
|
||||
IconInfoCircle,
|
||||
IconNotes,
|
||||
IconUpload,
|
||||
} from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/id";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
type DataItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type UpdateDataItem = {
|
||||
id: string;
|
||||
key: string;
|
||||
value: any;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
kategoriId: string;
|
||||
nama: string;
|
||||
phone: string;
|
||||
dataPelengkap: DataItem[];
|
||||
syaratDokumen: DataItem[];
|
||||
};
|
||||
|
||||
type FormUpdateSurat = {
|
||||
dataPelengkap: UpdateDataItem[];
|
||||
syaratDokumen: UpdateDataItem[];
|
||||
};
|
||||
|
||||
type DataPengajuan = {
|
||||
id: string;
|
||||
noPengajuan: string;
|
||||
category: string;
|
||||
status: "antrian" | "diproses" | "selesai" | "ditolak";
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
idSurat: string | undefined;
|
||||
alasan: string | undefined | null;
|
||||
};
|
||||
|
||||
export default function UpdateDataSurat() {
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const noPengajuan = query.get("pengajuan");
|
||||
const [found, setFound] = useState(true);
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"}>
|
||||
<Box>
|
||||
<Stack gap="lg">
|
||||
{found && (
|
||||
<Group justify="space-between" align="center" mt={"lg"}>
|
||||
<Group align="center">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<div>
|
||||
<Text fw={800} size="xl">
|
||||
Update Data Pengajuan Surat Administrasi
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Formulir ini digunakan untuk memperbarui data pengajuan
|
||||
surat administrasi yang telah diajukan sebelumnya.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
<Stack gap="lg" mb="lg">
|
||||
{!noPengajuan ? (
|
||||
<SearchData />
|
||||
) : found ? (
|
||||
<DataUpdate
|
||||
noPengajuan={noPengajuan}
|
||||
onValidate={(e) => {
|
||||
setFound(e);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<DataNotFound
|
||||
backTo={() => navigate("/darmasaba/update-data-surat")}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
|
||||
return (
|
||||
<Group justify="apart" gap="xs" align="center">
|
||||
<Group gap={4} align="center">
|
||||
<Text fw={600}>
|
||||
{label}
|
||||
{required && (
|
||||
<Text span c="red" ml={4}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{hint && (
|
||||
<Tooltip label={hint} withArrow>
|
||||
<ActionIcon size={24} variant="subtle">
|
||||
<IconInfoCircle size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
description,
|
||||
info,
|
||||
}: {
|
||||
title?: string;
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
description?: string;
|
||||
info?: string;
|
||||
}) {
|
||||
return (
|
||||
<Card radius="md" shadow="sm" withBorder>
|
||||
<Box mb="xs">
|
||||
<Group justify="apart" align="center">
|
||||
<Group align="center" gap="xs">
|
||||
{icon}
|
||||
{title && <Text fw={700}>{title}</Text>}
|
||||
</Group>
|
||||
{description && <Badge variant="light">{description}</Badge>}
|
||||
</Group>
|
||||
{info && (
|
||||
<Text size="sm" c="dimmed">
|
||||
{info}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{title && <Divider mb="sm" />}
|
||||
<Stack gap="sm">{children}</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function FileInputWrapper({
|
||||
label,
|
||||
placeholder,
|
||||
accept,
|
||||
onChange,
|
||||
preview,
|
||||
name,
|
||||
description,
|
||||
linkView,
|
||||
disabled,
|
||||
required = false,
|
||||
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
accept?: string;
|
||||
linkView?: string;
|
||||
onChange: (file: File | null) => void;
|
||||
preview?: string | null;
|
||||
name: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}) {
|
||||
const [viewImg, setViewImg] = useState("");
|
||||
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (viewImg) {
|
||||
setOpenedPreviewFile(true);
|
||||
}
|
||||
}, [viewImg]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalFile
|
||||
open={openedPreviewFile && !_.isEmpty(viewImg)}
|
||||
onClose={() => {
|
||||
setOpenedPreviewFile(false);
|
||||
}}
|
||||
folder="syarat-dokumen"
|
||||
fileName={viewImg}
|
||||
/>
|
||||
|
||||
<Stack gap="xs">
|
||||
<Flex direction={"column"}>
|
||||
<Group justify="apart" align="center">
|
||||
<Text fw={500}>
|
||||
{label}
|
||||
{required && (
|
||||
<Text span c="red" ml={4}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
{linkView && (
|
||||
<Anchor onClick={() => setViewImg(linkView)} size="sm">
|
||||
Lihat dokumen sebelumnya
|
||||
</Anchor>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<FileInput
|
||||
accept={accept}
|
||||
placeholder={placeholder}
|
||||
onChange={(f) => onChange(f)}
|
||||
leftSection={<IconUpload />}
|
||||
aria-label={label}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
clearable={true}
|
||||
/>
|
||||
|
||||
{preview ? (
|
||||
<div>
|
||||
<Text size="xs" color="dimmed">
|
||||
Preview:
|
||||
</Text>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<img
|
||||
src={preview}
|
||||
alt={`${label} preview`}
|
||||
style={{ maxWidth: "200px", borderRadius: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SearchData() {
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [searchPengajuan, setSearchPengajuan] = useState("");
|
||||
const [searchPengajuanPhone, setSearchPengajuanPhone] = useState("");
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function handleSearch() {
|
||||
try {
|
||||
setSubmitLoading(true);
|
||||
if (searchPengajuan == "" || searchPengajuanPhone == "") {
|
||||
notification({
|
||||
title: "Peringatan",
|
||||
message: "Silakan isi nomor pengajuan atau nomor telephone",
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({
|
||||
phone: searchPengajuanPhone,
|
||||
noPengajuan: searchPengajuan,
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
if (response.data?.success) {
|
||||
navigate(
|
||||
`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`,
|
||||
);
|
||||
} else {
|
||||
notification({
|
||||
title: "Peringatan",
|
||||
message: response.data?.message || "Data pengajuan tidak valid",
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Pengajuan tidak ditemukan atau gagal memuat data",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error searching:", error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Gagal mencari data pengajuan",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenLoading visible={submitLoading} text="Mencari Data" />
|
||||
<FormSection
|
||||
title="Cari Pengajuan Surat"
|
||||
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nomor Pengajuan"
|
||||
hint="Nomor pengajuan surat"
|
||||
/>
|
||||
}
|
||||
placeholder="PS-2025-000123"
|
||||
onChange={(e) => {
|
||||
setSearchPengajuan(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nomor Telephone"
|
||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||||
/>
|
||||
}
|
||||
placeholder="08123456789"
|
||||
type="number"
|
||||
onChange={(e) => {
|
||||
setSearchPengajuanPhone(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
handleSearch();
|
||||
}}
|
||||
loading={submitLoading}
|
||||
>
|
||||
Cari Pengajuan
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DataUpdate({
|
||||
noPengajuan,
|
||||
onValidate,
|
||||
}: {
|
||||
noPengajuan: string;
|
||||
onValidate: (e: boolean) => void;
|
||||
}) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = useState<Record<string, string | null>>({});
|
||||
const [sukses, setSukses] = useState(false);
|
||||
const [submitLoading, setSubmitLoading] = useState(false);
|
||||
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
|
||||
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
|
||||
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
|
||||
const [status, setStatus] = useState("");
|
||||
const [loadingFetchData, setLoadingFetchData] = useState(false);
|
||||
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
|
||||
dataPelengkap: [],
|
||||
syaratDokumen: [],
|
||||
});
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
setLoadingFetchData(true);
|
||||
const res = await apiFetch.api.pelayanan["detail-data"].post({
|
||||
nomerPengajuan: noPengajuan,
|
||||
});
|
||||
if (res.data && res.data.success === true) {
|
||||
onValidate(true);
|
||||
setDataPelengkap(res.data.dataPelengkap || []);
|
||||
setDataSyaratDokumen(res.data.syaratDokumen || []);
|
||||
setDataPengajuan(res.data.pengajuan || {});
|
||||
|
||||
setStatus(
|
||||
res.data.pengajuan && "status" in res.data.pengajuan
|
||||
? res.data.pengajuan.status
|
||||
: "",
|
||||
);
|
||||
} else {
|
||||
// notification({
|
||||
// title: "Error",
|
||||
// message: res.data?.message || "Gagal memuat data",
|
||||
// type: "error",
|
||||
// });
|
||||
onValidate(false);
|
||||
setDataPelengkap([]);
|
||||
setDataSyaratDokumen([]);
|
||||
setDataPengajuan({});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoadingFetchData(false);
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
function validateField(key: string, value: any) {
|
||||
const stringValue = String(value ?? "").trim();
|
||||
|
||||
// wajib diisi
|
||||
if (!stringValue) {
|
||||
return "Field wajib diisi";
|
||||
}
|
||||
|
||||
// 🔥 semua key yg mengandung "nik"
|
||||
if (key.toLowerCase().includes("nik")) {
|
||||
if (!/^\d+$/.test(stringValue)) {
|
||||
return "NIK harus berupa angka";
|
||||
}
|
||||
|
||||
if (stringValue.length !== 16) {
|
||||
return "NIK harus 16 digit";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
|
||||
const index = array.findIndex((v) => v.id === item.id);
|
||||
|
||||
// ➕ insert
|
||||
if (index === -1) {
|
||||
return [...array, item];
|
||||
}
|
||||
|
||||
// ✏️ update
|
||||
return array.map((v, i) => (i === index ? { ...v, ...item } : v));
|
||||
}
|
||||
|
||||
function validationForm({
|
||||
kategori,
|
||||
value,
|
||||
}: {
|
||||
kategori: "dataPelengkap" | "syaratDokumen";
|
||||
value: UpdateDataItem;
|
||||
}) {
|
||||
if (kategori == "syaratDokumen" && value.value == null) {
|
||||
setFormSurat((prev) => ({
|
||||
...prev,
|
||||
syaratDokumen: prev.syaratDokumen.filter(
|
||||
(item) => item.id !== value.id
|
||||
),
|
||||
}));
|
||||
} else {
|
||||
if (value.required == true) {
|
||||
const errorMsg = validateField(value.key, value.value);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[value.id]: errorMsg,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
setFormSurat((prev) => ({
|
||||
...prev,
|
||||
[kategori]: upsertById(prev[kategori], {
|
||||
id: value.id,
|
||||
key: value.key,
|
||||
value: value.value,
|
||||
required: value.required,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateArrayByKey(
|
||||
list: UpdateDataItem[],
|
||||
id: string,
|
||||
value: any,
|
||||
): UpdateDataItem[] {
|
||||
return list.map((item) => (item.id === id ? { ...item, value } : item));
|
||||
}
|
||||
|
||||
function onChecking() {
|
||||
const hasError = Object.values(errors).some((v) => v);
|
||||
|
||||
if (hasError) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Masih ada data yang belum valid",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
formSurat.dataPelengkap.length == 0 &&
|
||||
formSurat.syaratDokumen.length == 0
|
||||
)
|
||||
return notification({
|
||||
title: "Peringatan",
|
||||
message: "Tidak ada data yang diupdate",
|
||||
type: "warning",
|
||||
});
|
||||
const isFormKosong = Object.values(formSurat).some(
|
||||
(value: UpdateDataItem[] | string) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(
|
||||
(item) =>
|
||||
(typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
if (isFormKosong) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Silahkan lengkapi form surat",
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
setSubmitLoading(true);
|
||||
// 🔥 CLONE state SEKALI
|
||||
let finalFormSurat = structuredClone(formSurat);
|
||||
|
||||
// 2️⃣ Upload satu per satu
|
||||
for (const itemUpload of finalFormSurat.syaratDokumen) {
|
||||
const updImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: itemUpload.value,
|
||||
folder: "syarat-dokumen",
|
||||
});
|
||||
|
||||
if (updImg.status === 200) {
|
||||
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
|
||||
finalFormSurat.syaratDokumen = updateArrayByKey(
|
||||
finalFormSurat.syaratDokumen,
|
||||
itemUpload.id,
|
||||
updImg.data?.filename || "",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3️⃣ SET STATE SEKALI (optional, untuk UI)
|
||||
setFormSurat(finalFormSurat);
|
||||
|
||||
// 4️⃣ SUBMIT KE API
|
||||
const res = await apiFetch.api.pelayanan.update.post({
|
||||
id: dataPengajuan && "id" in dataPengajuan ? dataPengajuan.id : "",
|
||||
dataPelengkap: finalFormSurat.dataPelengkap,
|
||||
syaratDokumen: finalFormSurat.syaratDokumen,
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setSukses(true);
|
||||
} else {
|
||||
notification({
|
||||
title: "Gagal",
|
||||
message:
|
||||
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification({
|
||||
title: "Gagal",
|
||||
message: "Server Error",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setSubmitLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenLoading visible={submitLoading || loadingFetchData} />
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title={"Konfirmasi"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Text>Apakah anda yakin ingin mengupdate pengajuan surat ini?</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => {
|
||||
onSubmit();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
{sukses ? (
|
||||
<SuccessPengajuan
|
||||
noPengajuan={noPengajuan}
|
||||
onClose={() => {
|
||||
navigate("/darmasaba/update-data-surat");
|
||||
}}
|
||||
category="update"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{status != "ditolak" && status != "antrian" && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="yellow"
|
||||
radius="lg"
|
||||
title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`}
|
||||
icon={<span style={{ fontSize: "1.2rem" }}>⚠</span>}
|
||||
/>
|
||||
)}
|
||||
{status == "ditolak" && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color="yellow"
|
||||
radius="lg"
|
||||
title={`Data pengajuan surat ini ditolak, karena ${dataPengajuan && 'alasan' in dataPengajuan && dataPengajuan.alasan
|
||||
? dataPengajuan.alasan
|
||||
: "alasan tidak tersedia"
|
||||
}. Silahkan perbaiki data pengajuan surat ini.`}
|
||||
icon={<span style={{ fontSize: "1.2rem" }}>⚠</span>}
|
||||
/>
|
||||
)}
|
||||
<FormSection
|
||||
title="Data Yang Diperlukan"
|
||||
description="Data yang diperlukan"
|
||||
icon={<IconNotes size={16} />}
|
||||
>
|
||||
<Grid>
|
||||
{dataPelengkap.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
{item.type == "enum" ? (
|
||||
<Select
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
allowDeselect={false}
|
||||
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
|
||||
data={item.options ?? []}
|
||||
placeholder={item.name}
|
||||
onChange={(e) => {
|
||||
validationForm({
|
||||
kategori: "dataPelengkap",
|
||||
value: { id: item.id, key: item.key, value: e, required: item.required },
|
||||
});
|
||||
}}
|
||||
value={
|
||||
formSurat.dataPelengkap.find(
|
||||
(n: any) => n.key == item.key,
|
||||
)?.value ||
|
||||
dataPelengkap.find((n: any) => n.key == item.key)?.value
|
||||
}
|
||||
/>
|
||||
) : item.type == "date" ? (
|
||||
<DateInput
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
locale="id"
|
||||
valueFormat="DD MMMM YYYY"
|
||||
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
|
||||
placeholder={item.name}
|
||||
onChange={(e) => {
|
||||
const formatted = e
|
||||
? dayjs(e).locale("id").format("DD MMMM YYYY")
|
||||
: "";
|
||||
validationForm({
|
||||
kategori: "dataPelengkap",
|
||||
value: {
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: formatted,
|
||||
required: item.required
|
||||
},
|
||||
});
|
||||
}}
|
||||
value={
|
||||
formSurat.dataPelengkap.find(
|
||||
(n: any) => n.key === item.key,
|
||||
)?.value
|
||||
? parseTanggalID(
|
||||
formSurat.dataPelengkap.find(
|
||||
(n: any) => n.key === item.key,
|
||||
)?.value,
|
||||
)
|
||||
: parseTanggalID(item.value)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
error={errors[item.id]}
|
||||
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
|
||||
placeholder={item.name}
|
||||
type={item.type}
|
||||
onChange={(e) =>
|
||||
validationForm({
|
||||
kategori: "dataPelengkap",
|
||||
value: {
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: e.target.value,
|
||||
required: item.required,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={
|
||||
formSurat.dataPelengkap.find((n) => n.id === item.id)
|
||||
?.value ??
|
||||
dataPelengkap.find((n: any) => n.key == item.key)?.value
|
||||
}
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
rightSection={
|
||||
item.satuan != null &&
|
||||
<Text mr={"lg"}>{item.satuan}</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Syarat Dokumen hjh"
|
||||
description="Syarat dokumen yang diperlukan"
|
||||
icon={<IconFiles size={16} />}
|
||||
>
|
||||
<Grid>
|
||||
{dataSyaratDokumen.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<FileInputWrapper
|
||||
required={item.required}
|
||||
label={item.desc}
|
||||
placeholder={"Upload file terbaru untuk mengupdate"}
|
||||
accept="image/*,application/pdf"
|
||||
linkView={item.value}
|
||||
onChange={(file) =>
|
||||
validationForm({
|
||||
kategori: "syaratDokumen",
|
||||
value: { id: item.id, key: item.key, value: file, required: item.required },
|
||||
})
|
||||
}
|
||||
name={item.name}
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<Group justify="right" mt="md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
onChecking();
|
||||
}}
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
>
|
||||
Kirim
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +1,16 @@
|
||||
import DashboardCountData from "@/components/DashboardCountData";
|
||||
import DashboardGrafik from "@/components/DashboardGrafik";
|
||||
import DashboardLastData from "@/components/DashboardLastData";
|
||||
import {
|
||||
Card,
|
||||
Badge,
|
||||
Container,
|
||||
Flex,
|
||||
Group,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Progress,
|
||||
Badge,
|
||||
Button,
|
||||
Grid,
|
||||
Divider,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconActivity,
|
||||
IconUsers,
|
||||
IconServer,
|
||||
IconDatabase,
|
||||
IconSettings,
|
||||
IconArrowRight,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
@@ -43,144 +34,15 @@ export default function Dashboard() {
|
||||
Live
|
||||
</Badge>
|
||||
</Group>
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: "teal", to: "cyan", deg: 45 }}
|
||||
radius="md"
|
||||
rightSection={<IconArrowRight size={18} />}
|
||||
style={{
|
||||
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
|
||||
}}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconUsers size={28} />}
|
||||
label="Active Users"
|
||||
value="1,248"
|
||||
change="+12%"
|
||||
color="teal"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconServer size={28} />}
|
||||
label="Server Uptime"
|
||||
value="99.98%"
|
||||
change="+0.02%"
|
||||
color="cyan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconDatabase size={28} />}
|
||||
label="Database Ops"
|
||||
value="82.4K"
|
||||
change="+5.6%"
|
||||
color="blue"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
|
||||
<MetricCard
|
||||
icon={<IconActivity size={28} />}
|
||||
label="System Health"
|
||||
value="Stable"
|
||||
change=""
|
||||
color="green"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.0">
|
||||
System Performance
|
||||
</Title>
|
||||
<IconSettings size={20} color="gray" />
|
||||
</Flex>
|
||||
<Divider my="xs" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Resource usage and performance indicators.
|
||||
</Text>
|
||||
<Stack gap="sm" mt="md">
|
||||
<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>
|
||||
</Card>
|
||||
<DashboardCountData />
|
||||
<DashboardGrafik />
|
||||
<DashboardLastData />
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
change,
|
||||
color,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="md"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
|
||||
}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Group gap={6}>
|
||||
{icon}
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Text fw={600} size="xl" c="gray.0">
|
||||
{value}
|
||||
</Text>
|
||||
{change && (
|
||||
<Text size="sm" c={color}>
|
||||
{change}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressSection({
|
||||
label,
|
||||
value,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -72,7 +73,7 @@ export default function DashboardLayout() {
|
||||
<AppShell
|
||||
padding="lg"
|
||||
navbar={{
|
||||
width: 260,
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened, desktop: !opened },
|
||||
}}
|
||||
@@ -212,36 +213,54 @@ function HostView() {
|
||||
function NavigationDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPermissions() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
if (Array.isArray(data?.permissions)) {
|
||||
setPermissions(data.permissions);
|
||||
} else {
|
||||
setPermissions([]);
|
||||
}
|
||||
}
|
||||
fetchPermissions();
|
||||
}, []);
|
||||
|
||||
const isActive = (path: keyof typeof clientRoute) =>
|
||||
location.pathname.startsWith(clientRoute[path]);
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
key: "dashboard",
|
||||
path: "/scr/dashboard/dashboard-home",
|
||||
icon: <IconDashboard size={20} />,
|
||||
label: "Dashboard Overview",
|
||||
description: "Quick summary and insights",
|
||||
},
|
||||
{
|
||||
key: "pengaduan",
|
||||
path: "/scr/dashboard/pengaduan/list",
|
||||
icon: <IconMessageReport size={20} />,
|
||||
label: "Pengaduan Warga",
|
||||
description: "Manage pengaduan warga",
|
||||
},
|
||||
{
|
||||
key: "pelayanan",
|
||||
path: "/scr/dashboard/pelayanan-surat/list-pelayanan",
|
||||
icon: <IconFileCertificate size={20} />,
|
||||
label: "Pelayanan Surat",
|
||||
description: "Manage pelayanan surat",
|
||||
},
|
||||
{
|
||||
key: "warga",
|
||||
path: "/scr/dashboard/warga/list-warga",
|
||||
icon: <IconUsersGroup size={20} />,
|
||||
label: "Warga",
|
||||
description: "Manage warga",
|
||||
},
|
||||
{
|
||||
key: "setting",
|
||||
path: "/scr/dashboard/setting/detail-setting",
|
||||
icon: <IconSettings size={20} />,
|
||||
label: "Setting",
|
||||
@@ -249,12 +268,14 @@ function NavigationDashboard() {
|
||||
"Manage setting (category pengaduan dan pelayanan surat, desa, etc)",
|
||||
},
|
||||
{
|
||||
key: "api_key",
|
||||
path: "/scr/dashboard/apikey/apikey",
|
||||
icon: <IconKey size={20} />,
|
||||
label: "API Key Manager",
|
||||
description: "Create and manage API keys",
|
||||
},
|
||||
{
|
||||
key: "credential",
|
||||
path: "/scr/dashboard/credential/credential",
|
||||
icon: <IconLock size={20} />,
|
||||
label: "Credentials",
|
||||
@@ -264,45 +285,59 @@ function NavigationDashboard() {
|
||||
|
||||
return (
|
||||
<Stack gap="xs" p="sm">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute)}
|
||||
leftSection={item.icon}
|
||||
label={
|
||||
<Flex align="center" gap={6}>
|
||||
<Text fw={500}>{item.label}</Text>
|
||||
{isActive(item.path as keyof typeof clientRoute) && (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="teal"
|
||||
radius="sm"
|
||||
size="xs"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
description={item.description}
|
||||
onClick={() =>
|
||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive(item.path as keyof typeof clientRoute)
|
||||
? "rgba(0,255,200,0.1)"
|
||||
: "transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
styles={{
|
||||
label: { color: "white" },
|
||||
description: { color: "#aaa" },
|
||||
section: { color: "teal" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{navItems
|
||||
.filter((item) => permissions.includes(item.key))
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute) ||
|
||||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
|
||||
leftSection={item.icon}
|
||||
label={
|
||||
<Flex align="center" gap={6}>
|
||||
<Text fw={500}>{item.label}</Text>
|
||||
{(
|
||||
isActive(item.path as keyof typeof clientRoute) ||
|
||||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
||||
)
|
||||
&& (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="teal"
|
||||
radius="sm"
|
||||
size="xs"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
description={item.description}
|
||||
onClick={() =>
|
||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
|
||||
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
||||
? "rgba(0,255,200,0.1)"
|
||||
: "transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
styles={{
|
||||
label: { color: "white" },
|
||||
description: { color: "#aaa" },
|
||||
section: { color: "teal" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import FullScreenLoading from "@/components/FullScreenLoading";
|
||||
import ModalFile from "@/components/ModalFile";
|
||||
import ModalSurat from "@/components/ModalSurat";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { parseTanggalID } from "@/server/lib/stringToDate";
|
||||
import {
|
||||
ActionIcon,
|
||||
Anchor,
|
||||
Badge,
|
||||
Button,
|
||||
@@ -13,30 +18,44 @@ import {
|
||||
Group,
|
||||
List,
|
||||
Modal,
|
||||
Select,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title
|
||||
Title,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconAlignJustified,
|
||||
IconCheck,
|
||||
IconEdit,
|
||||
IconFileCertificate,
|
||||
IconFileCheck,
|
||||
IconInfoCircle,
|
||||
IconMessageReport,
|
||||
IconPhone,
|
||||
IconUser
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
|
||||
export default function DetailPengajuanPage() {
|
||||
const dataMenu = [
|
||||
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||
{ title: "Pelayanan Surat", link: "/scr/dashboard/pelayanan-surat/list-pelayanan", active: false },
|
||||
{ title: "Detail Pengajuan Surat", link: "#", active: true },
|
||||
];
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const id = query.get("id");
|
||||
@@ -55,9 +74,20 @@ export default function DetailPengajuanPage() {
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<BreadCrumbs dataLink={dataMenu} back />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
|
||||
<DetailDataPengajuan
|
||||
data={data?.data?.pengajuan}
|
||||
warga={data && data.data && data.data.warga ? data.data.warga : undefined}
|
||||
syaratDokumen={data?.data?.syaratDokumen}
|
||||
dataText={data?.data?.dataText}
|
||||
onAction={() => {
|
||||
mutate();
|
||||
}}
|
||||
/>
|
||||
<DetailDataHistori data={data?.data?.history} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
@@ -69,33 +99,132 @@ export default function DetailPengajuanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
|
||||
function DetailDataPengajuan({
|
||||
data,
|
||||
warga,
|
||||
syaratDokumen,
|
||||
dataText,
|
||||
onAction,
|
||||
}: {
|
||||
data: any;
|
||||
warga?: { phone?: string | null } | null;
|
||||
syaratDokumen: any;
|
||||
dataText: any;
|
||||
onAction: () => void;
|
||||
}) {
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
const [noSurat, setNoSurat] = useState("");
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
const [viewImg, setViewImg] = useState({ file: "", folder: "" });
|
||||
const [uploading, setUploading] = useState({ ok: false, file: "" });
|
||||
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", satuan: null as string | null, option: null as any, type: "", key: "" })
|
||||
const [openEdit, setOpenEdit] = useState(false)
|
||||
const [loadingUpdate, setLoadingUpdate] = useState(false)
|
||||
const [loadingFS, setLoadingFS] = useState({ value: false, text: "" })
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost(data?.user ?? null);
|
||||
|
||||
if (data?.permissions && Array.isArray(data.permissions)) {
|
||||
const onlySetting = data.permissions.filter((p: any) =>
|
||||
p.startsWith("pelayanan"),
|
||||
);
|
||||
setPermissions(onlySetting);
|
||||
}
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
|
||||
try {
|
||||
setLoadingFS({ value: true, text: "Sending message to warga" })
|
||||
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
|
||||
noPengajuan: data?.noPengajuan ?? "",
|
||||
jenisSurat: data?.category ?? "",
|
||||
alasan: keterangan,
|
||||
status,
|
||||
linkSurat,
|
||||
linkUpdate,
|
||||
tlp: warga?.phone ?? "",
|
||||
})
|
||||
|
||||
if (resWA?.status === 200) {
|
||||
if (resWA.data?.success) {
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Success send message to warga",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
if (status == "selesai") {
|
||||
onAction()
|
||||
}
|
||||
|
||||
} else {
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed send message to warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed send message to warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed send message to warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
finally {
|
||||
setLoadingFS({ value: false, text: "" })
|
||||
}
|
||||
}
|
||||
|
||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||
try {
|
||||
setLoadingFS({ value: true, text: "Updating status" })
|
||||
const statusFix = cat == "tolak"
|
||||
? "ditolak"
|
||||
: data.status == "antrian"
|
||||
? "diterima"
|
||||
: "selesai"
|
||||
|
||||
const res = await apiFetch.api.pelayanan["update-status"].post({
|
||||
id: data?.id,
|
||||
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
|
||||
status: statusFix,
|
||||
keterangan: keterangan,
|
||||
idUser: host?.id ?? "",
|
||||
noSurat: noSurat
|
||||
noSurat: noSurat,
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
if (statusFix == "selesai") {
|
||||
setTimeout(() => {
|
||||
setOpenedPreview(true)
|
||||
}, 1000)
|
||||
} else {
|
||||
sendWA({
|
||||
status: statusFix,
|
||||
linkSurat: "",
|
||||
linkUpdate: statusFix == "ditolak" ? res.data?.linkUpdate ?? '' : '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
onAction();
|
||||
close();
|
||||
notification({
|
||||
@@ -110,7 +239,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
@@ -118,12 +246,160 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
message: "Failed to update pengajuan surat",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setLoadingFS({ value: false, text: "" })
|
||||
}
|
||||
};
|
||||
|
||||
async function updateDataText() {
|
||||
try {
|
||||
setLoadingUpdate(true)
|
||||
const res = await apiFetch.api.pelayanan["update-data-pelengkap"].post({
|
||||
id: editValue.id,
|
||||
value: editValue.val,
|
||||
jenis: editValue.key,
|
||||
idUser: host?.id ?? "",
|
||||
})
|
||||
|
||||
if (res?.status === 200) {
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Success update data",
|
||||
type: "success",
|
||||
})
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update data",
|
||||
type: "error",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to update data",
|
||||
type: "error",
|
||||
})
|
||||
} finally {
|
||||
setLoadingUpdate(false)
|
||||
setOpenEdit(false)
|
||||
onAction()
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (viewImg) {
|
||||
setOpenedPreviewFile(true);
|
||||
}
|
||||
}, [viewImg]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (uploading.ok && uploading.file) {
|
||||
sendWA({
|
||||
status: "selesai",
|
||||
linkSurat: uploading.file,
|
||||
linkUpdate: "",
|
||||
});
|
||||
}
|
||||
}, [uploading]);
|
||||
|
||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
return (
|
||||
<Group justify="apart" gap="xs" align="center">
|
||||
<Text fw={600}>{label}</Text>
|
||||
{hint && (
|
||||
<Tooltip label={hint} withArrow>
|
||||
<ActionIcon size={24} variant="subtle">
|
||||
<IconInfoCircle size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<FullScreenLoading visible={loadingFS.value} text={loadingFS.text} />
|
||||
{/* MODAL EDIT DATA PELENGKAP */}
|
||||
<Modal
|
||||
opened={openEdit}
|
||||
onClose={() => setOpenEdit(false)}
|
||||
title={"Edit"}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
{editValue.type == "enum" ? (
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label={<FieldLabel label={editValue.jenis} />}
|
||||
data={editValue.option ?? []}
|
||||
placeholder={editValue.jenis}
|
||||
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
|
||||
value={editValue.val}
|
||||
/>
|
||||
) : editValue.type == "date" ? (
|
||||
<DateInput
|
||||
locale="id"
|
||||
valueFormat="DD MMMM YYYY"
|
||||
label={<FieldLabel label={editValue.jenis} />}
|
||||
placeholder={editValue.jenis}
|
||||
onChange={(e) => {
|
||||
const formatted = e
|
||||
? dayjs(e).locale("id").format("DD MMMM YYYY")
|
||||
: "";
|
||||
setEditValue({
|
||||
...editValue,
|
||||
val: formatted
|
||||
})
|
||||
}}
|
||||
value={
|
||||
editValue.val
|
||||
? parseTanggalID(editValue.val)
|
||||
: parseTanggalID(editValue.val)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label={<FieldLabel label={editValue.jenis} />}
|
||||
placeholder={editValue.jenis}
|
||||
type={editValue.type}
|
||||
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
|
||||
value={editValue.val}
|
||||
rightSection={
|
||||
editValue.satuan != null &&
|
||||
<Text mr={"lg"}>{editValue.satuan}</Text>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={updateDataText}
|
||||
disabled={loadingUpdate || !editValue.val}
|
||||
loading={loadingUpdate}
|
||||
>
|
||||
Simpan
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
<ModalFile
|
||||
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
|
||||
onClose={() => {
|
||||
setOpenedPreviewFile(false);
|
||||
setViewImg({ file: "", folder: "" })
|
||||
}}
|
||||
folder={viewImg.folder}
|
||||
fileName={viewImg.file}
|
||||
/>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
@@ -135,14 +411,25 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
{catModal === "tolak" ? (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
|
||||
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan
|
||||
penolakan
|
||||
</Text>
|
||||
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
|
||||
<Textarea
|
||||
size="md"
|
||||
minRows={5}
|
||||
value={keterangan}
|
||||
onChange={(e) => setKeterangan(e.target.value)}
|
||||
/>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
disabled={keterangan.length < 1}
|
||||
onClick={() => handleKonfirmasi("tolak")}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -150,21 +437,31 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
) : (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
|
||||
{
|
||||
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
|
||||
}
|
||||
Anda yakin ingin{" "}
|
||||
{data?.status == "antrian" ? "menerima" : "menyetujui"}{" "}
|
||||
pengajuan surat ini?
|
||||
{data.status == "diterima" &&
|
||||
"Masukkan nomer surat yang akan dibuat"}
|
||||
</Text>
|
||||
{
|
||||
data.status == 'diterima' && (
|
||||
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" />
|
||||
)
|
||||
}
|
||||
{data.status == "diterima" && (
|
||||
<Textarea
|
||||
size="md"
|
||||
minRows={5}
|
||||
value={noSurat}
|
||||
onChange={(e) => setNoSurat(e.target.value)}
|
||||
placeholder="Contoh : 08/D-IV/11/2025"
|
||||
/>
|
||||
)}
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => handleKonfirmasi("terima")}
|
||||
disabled={data.status == "diterima" && noSurat.length < 1}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -172,11 +469,18 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
{
|
||||
data?.status == "selesai" &&
|
||||
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />)
|
||||
}
|
||||
|
||||
{/* MODAL PREVIEW SURAT */}
|
||||
{data?.status == "selesai" && !data?.fileSurat && (
|
||||
<ModalSurat
|
||||
open={openedPreview}
|
||||
onClose={(val) => {
|
||||
setOpenedPreview(false)
|
||||
setUploading({ ok: val.success, file: val.data })
|
||||
}}
|
||||
surat={data?.idSurat}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card
|
||||
radius="md"
|
||||
@@ -239,7 +543,11 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
>
|
||||
{syaratDokumen?.map((v: any) => (
|
||||
<List.Item key={v.id}>
|
||||
<Anchor href="https://mantine.dev/" target="_blank">
|
||||
<Anchor
|
||||
onClick={() => {
|
||||
setViewImg({ file: v.value, folder: "syarat-dokumen" });
|
||||
}}
|
||||
>
|
||||
{v.jenis}
|
||||
</Anchor>
|
||||
</List.Item>
|
||||
@@ -247,8 +555,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
</List>
|
||||
</Flex>
|
||||
|
||||
|
||||
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} />
|
||||
@@ -257,75 +563,116 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
|
||||
<Table withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{
|
||||
dataText?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
{dataText?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td
|
||||
style={{ whiteSpace: "nowrap", width: "10%" }}
|
||||
>
|
||||
{_.upperFirst(item.jenis)}
|
||||
</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>
|
||||
<Flex
|
||||
gap="md"
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
direction="row"
|
||||
>
|
||||
<Text>
|
||||
{_.upperFirst(item.value)} {item.satuan}
|
||||
</Text>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
aria-label="Edit"
|
||||
onClick={() => {
|
||||
setEditValue({ id: item.id, val: item.value, type: item.type, satuan: item.satuan, option: item.options, jenis: item.jenis, key: item.key })
|
||||
setOpenEdit(true)
|
||||
}}>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setOpenedPreview(!openedPreview)}
|
||||
>
|
||||
Surat
|
||||
</Button>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
{data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.terima")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!permissions.includes("pelayanan.diterima.setujui")
|
||||
}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "selesai" ?
|
||||
!data?.fileSurat ?
|
||||
(
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => { setOpenedPreview(true) }}
|
||||
>
|
||||
Kirim Ulang Surat
|
||||
</Button>
|
||||
</Group>
|
||||
)
|
||||
:
|
||||
(
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
|
||||
>
|
||||
Surat
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -350,32 +697,48 @@ function DetailDataHistori({ data }: { data: any }) {
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Histori Pengajuan Surat
|
||||
Riwayat Pengajuan Surat
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Tanggal</Table.Th>
|
||||
<Table.Th>Deskripsi</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Spoiler
|
||||
maxHeight={200}
|
||||
showLabel="Show more"
|
||||
hideLabel="Hide"
|
||||
transitionDuration={1000}
|
||||
>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Tanggal</Table.Th>
|
||||
<Table.Th>Deskripsi</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.nameUser ? item.nameUser : "-"}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Spoiler>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
IconClockHour3,
|
||||
IconFileSad,
|
||||
IconSearch,
|
||||
IconUser
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -113,23 +115,25 @@ type StatusKey =
|
||||
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", async () => {
|
||||
const res = await apiFetch.api.pelayanan.list.get({
|
||||
const { data, mutate, isLoading } = useSwr("/", async () =>
|
||||
apiFetch.api.pelayanan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
page: page.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
setPage(1);
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [page]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
@@ -155,27 +159,48 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data || [];
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
const toDate = (d: any) => new Date(d);
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group grow>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengajuan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
<Grid>
|
||||
<Grid.Col span={9}>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengajuan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Group justify="flex-end">
|
||||
<Text
|
||||
size="sm"
|
||||
c="gray.5"
|
||||
>{`${pageSize * (page - 1) + 1} – ${Math.min(total, pageSize * page)} of ${total}`}</Text>
|
||||
<Pagination
|
||||
total={totalPage}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
withPages={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
{list?.length === 0 ? (
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
{Array.isArray(list) && list?.length === 0 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
@@ -185,7 +210,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : (
|
||||
Array.isArray(list) && list?.map((v: any) => (
|
||||
Array.isArray(list) &&
|
||||
list?.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
@@ -214,7 +240,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
#{v.noPengajuan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
{String(v.updatedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
@@ -247,7 +273,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
Tanggal Ajuan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
<Text size="md">
|
||||
{toDate(v.createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import ModalFile from "@/components/ModalFile";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
@@ -10,8 +12,8 @@ import {
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Image,
|
||||
Modal,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
@@ -31,12 +33,19 @@ import {
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
|
||||
|
||||
export default function DetailPengaduanPage() {
|
||||
const dataMenu = [
|
||||
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||
{ title: "Pengaduan", link: "/scr/dashboard/pengaduan/list", active: false },
|
||||
{ title: "Detail Pengaduan", link: "#", active: true },
|
||||
];
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const id = query.get("id");
|
||||
@@ -55,9 +64,18 @@ export default function DetailPengaduanPage() {
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<BreadCrumbs dataLink={dataMenu} back />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
|
||||
<DetailDataPengaduan
|
||||
data={data?.data?.pengaduan}
|
||||
phone={data && data.data && data.data.warga ? data.data.warga.phone : null}
|
||||
onAction={() => {
|
||||
mutate();
|
||||
}}
|
||||
/>
|
||||
<DetailDataHistori data={data?.data?.history} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
@@ -69,33 +87,71 @@ export default function DetailPengaduanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
|
||||
function DetailDataPengaduan({
|
||||
data,
|
||||
phone,
|
||||
onAction,
|
||||
}: {
|
||||
data: any | null;
|
||||
phone?: string | null;
|
||||
onAction: () => void;
|
||||
}) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
const [openedModalImage, { open: openModalImage, close: closeModalImage }] =
|
||||
useDisclosure(false);
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
const [host, setHost] = useState<User | null>(null);
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchHost() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
setHost(data?.user ?? null);
|
||||
|
||||
if (data?.permissions && Array.isArray(data.permissions)) {
|
||||
const onlySetting = data.permissions.filter((p: any) =>
|
||||
p.startsWith("pengaduan"),
|
||||
);
|
||||
setPermissions(onlySetting);
|
||||
}
|
||||
}
|
||||
fetchHost();
|
||||
}, []);
|
||||
|
||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await apiFetch.api.pengaduan["update-status"].post({
|
||||
id: data?.id,
|
||||
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : data.status == 'diterima' ? 'dikerjakan' : 'selesai',
|
||||
status:
|
||||
cat == "tolak"
|
||||
? "ditolak"
|
||||
: data.status == "antrian"
|
||||
? "diterima"
|
||||
: data.status == "diterima"
|
||||
? "dikerjakan"
|
||||
: "selesai",
|
||||
keterangan: keterangan,
|
||||
idUser: host?.id ?? ""
|
||||
idUser: host?.id ?? "",
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
const resWA = await apiFetch.api["send-wa"].pengaduan.post({
|
||||
noPengaduan: data?.noPengaduan,
|
||||
judulPengaduan: data?.title,
|
||||
status:
|
||||
cat == "tolak"
|
||||
? "ditolak"
|
||||
: data.status == "antrian"
|
||||
? "diterima"
|
||||
: data.status == "diterima"
|
||||
? "dikerjakan"
|
||||
: "selesai",
|
||||
alasan: keterangan,
|
||||
tlp: String(phone),
|
||||
})
|
||||
|
||||
onAction();
|
||||
close();
|
||||
notification({
|
||||
@@ -103,6 +159,28 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
message: "Success update pengaduan",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
if (resWA?.status === 200) {
|
||||
if (resWA.data?.success) {
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Success send message to warga",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed send message to warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed send message to warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification({
|
||||
title: "Error",
|
||||
@@ -110,7 +188,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
@@ -118,12 +195,13 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
message: "Failed to update pengaduan",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
@@ -138,24 +216,48 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
|
||||
</Text>
|
||||
|
||||
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
|
||||
<Textarea
|
||||
size="md"
|
||||
minRows={5}
|
||||
value={keterangan}
|
||||
onChange={(e) => setKeterangan(e.target.value)}
|
||||
/>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</Button>
|
||||
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
disabled={keterangan.length < 1}
|
||||
onClick={() => handleKonfirmasi("tolak")}
|
||||
loading={isLoading}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} pengaduan ini?</Text>
|
||||
<Text>
|
||||
Anda yakin ingin{" "}
|
||||
{data?.status == "antrian"
|
||||
? "menerima"
|
||||
: data.status == "diterima"
|
||||
? "mengerjakan"
|
||||
: "menyelesaikan"}{" "}
|
||||
pengaduan ini?
|
||||
</Text>
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => handleKonfirmasi("terima")}
|
||||
loading={isLoading}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -164,16 +266,13 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* MODAL GAMBAR */}
|
||||
<Modal
|
||||
opened={openedModalImage}
|
||||
onClose={closeModalImage}
|
||||
title="Gambar Pengaduan"
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Image src={imageSrc!} />
|
||||
</Modal>
|
||||
<ModalFile
|
||||
open={openedPreview && !_.isEmpty(data?.image)}
|
||||
onClose={() => setOpenedPreview(false)}
|
||||
folder="pengaduan"
|
||||
fileName={data?.image}
|
||||
/>
|
||||
|
||||
<Card
|
||||
radius="md"
|
||||
@@ -256,9 +355,20 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
<IconPhotoScan size={20} />
|
||||
<Text size="md">Gambar</Text>
|
||||
</Group>
|
||||
<Anchor href="#" onClick={() => { }}>
|
||||
Lihat Gambar
|
||||
</Anchor>
|
||||
{data?.image != null && data?.image != "" ? (
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={() => {
|
||||
setOpenedPreview(true);
|
||||
}}
|
||||
>
|
||||
Lihat Gambar
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text size="md" c="white">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
@@ -273,70 +383,76 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
{_.upperFirst(data?.detail)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{
|
||||
data?.keterangan && (
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">Keterangan</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
{_.upperFirst(data?.keterangan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
{data?.keterangan && (
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">Keterangan</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
{_.upperFirst(data?.keterangan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Kerjakan
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "dikerjakan" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</Group>
|
||||
) : <></>
|
||||
}
|
||||
{data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
disabled={!permissions.includes("pengaduan.antrian.tolak")}
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.antrian.terima")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={
|
||||
!permissions.includes("pengaduan.diterima.dikerjakan")
|
||||
}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Kerjakan
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "dikerjakan" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={
|
||||
!permissions.includes("pengaduan.dikerjakan.selesai")
|
||||
}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -361,32 +477,48 @@ function DetailDataHistori({ data }: { data: any }) {
|
||||
<Stack gap="md">
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={4} c="gray.2">
|
||||
Histori Pengaduan
|
||||
Riwayat Pengaduan
|
||||
</Title>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Tanggal</Table.Th>
|
||||
<Table.Th>Deskripsi</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Spoiler
|
||||
maxHeight={200}
|
||||
showLabel="Show more"
|
||||
hideLabel="Hide"
|
||||
transitionDuration={1000}
|
||||
>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Tanggal</Table.Th>
|
||||
<Table.Th>Deskripsi</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>User</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.nameUser ? item.nameUser : "-"}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Spoiler>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -124,22 +126,25 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", async () => {
|
||||
const res = await apiFetch.api.pengaduan.list.get({
|
||||
const { data, mutate, isLoading } = useSwr("/", async () =>
|
||||
apiFetch.api.pengaduan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
page: page.toString(),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
useShallowEffect(() => {
|
||||
setPage(1);
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
}, [page]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
@@ -163,31 +168,48 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data || [];
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
const toDate = (d: any) => new Date(d);
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group grow>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengaduan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
<Grid>
|
||||
<Grid.Col span={9}>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengaduan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Group justify="flex-end">
|
||||
<Text
|
||||
size="sm"
|
||||
c="gray.5"
|
||||
>{`${pageSize * (page - 1) + 1} – ${Math.min(total, pageSize * page)} of ${total}`}</Text>
|
||||
<Pagination
|
||||
total={totalPage}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
withPages={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{/* <Group justify="flex-end">
|
||||
<Text size="sm">Menampilkan {Number(data?.data?.length) * (page - 1) + 1} – {Math.min(10, Number(data?.data?.length) * page)} dari {Number(data?.data?.length)}</Text>
|
||||
<Pagination total={Number(data?.data?.length)} value={page} onChange={setPage} withPages={false} />
|
||||
</Group> */}
|
||||
</Group>
|
||||
{list.length === 0 ? (
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
{Array.isArray(list) && list.length === 0 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
@@ -197,7 +219,8 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : (
|
||||
Array.isArray(list) && list?.map((v: any) => (
|
||||
Array.isArray(list) &&
|
||||
list?.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
@@ -224,7 +247,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
#{v.noPengaduan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
{String(v.updatedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
@@ -257,7 +280,13 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
Tanggal Aduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
<Text size="md">
|
||||
{toDate(v.createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -1,31 +1,103 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import DesaSetting from "@/components/DesaSetting";
|
||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||
import ProfileUser from "@/components/ProfileUser";
|
||||
import UserRoleSetting from "@/components/UserRoleSetting";
|
||||
import UserSetting from "@/components/UserSetting";
|
||||
import {
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
NavLink
|
||||
} from "@mantine/core";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Card, Container, Grid, NavLink } from "@mantine/core";
|
||||
import {
|
||||
IconBuildingBank,
|
||||
IconCategory2,
|
||||
IconMailSpark,
|
||||
IconUserCog,
|
||||
IconUserScreen,
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function DetailSettingPage() {
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const type = query.get("type");
|
||||
const navigate = useNavigate();
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
const dataMenu = [
|
||||
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||
{ title: "Setting", link: "#", active: false },
|
||||
{ title: type == "cat-pengaduan" ? "Kategori Pengaduan" : type == "cat-pelayanan" ? "Kategori Pelayanan Surat" : type ? _.upperFirst(type) : "Profile", link: "#", active: true },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchPermissions() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
if (Array.isArray(data?.permissions)) {
|
||||
const onlySetting = data.permissions.filter((p: any) =>
|
||||
p.startsWith("setting"),
|
||||
);
|
||||
setPermissions(onlySetting);
|
||||
} else {
|
||||
setPermissions([]);
|
||||
}
|
||||
}
|
||||
fetchPermissions();
|
||||
}, []);
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
key: "setting.profile",
|
||||
path: "profile",
|
||||
icon: <IconUserCog size={20} />,
|
||||
label: "Profile",
|
||||
description: "Manage profile settings",
|
||||
},
|
||||
{
|
||||
key: "setting.user",
|
||||
path: "user",
|
||||
icon: <IconUsersGroup size={20} />,
|
||||
label: "User",
|
||||
description: "Manage user accounts",
|
||||
},
|
||||
{
|
||||
key: "setting.user_role",
|
||||
path: "role",
|
||||
icon: <IconUserScreen size={20} />,
|
||||
label: "Role",
|
||||
description: "Manage user roles",
|
||||
},
|
||||
{
|
||||
key: "setting.kategori_pengaduan",
|
||||
path: "cat-pengaduan",
|
||||
icon: <IconCategory2 size={20} />,
|
||||
label: "Kategori Pengaduan",
|
||||
description: "Manage complaint categories",
|
||||
},
|
||||
{
|
||||
key: "setting.kategori_pelayanan",
|
||||
path: "cat-pelayanan",
|
||||
icon: <IconMailSpark size={20} />,
|
||||
label: "Kategori Pelayanan Surat",
|
||||
description: "Manage letter service categories",
|
||||
},
|
||||
{
|
||||
key: "setting.desa",
|
||||
path: "desa",
|
||||
icon: <IconBuildingBank size={20} />,
|
||||
label: "Desa",
|
||||
description: "Manage desa information",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<BreadCrumbs dataLink={dataMenu} back />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Card
|
||||
radius="md"
|
||||
@@ -38,36 +110,19 @@ export default function DetailSettingPage() {
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
href={`?type=profile`}
|
||||
label="Profile"
|
||||
leftSection={<IconUserCog size={16} stroke={1.5} />}
|
||||
active={type === "profile" || !type}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=user`}
|
||||
label="User"
|
||||
leftSection={<IconUsersGroup size={16} stroke={1.5} />}
|
||||
active={type === "user"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pengaduan`}
|
||||
label="Kategori Pengaduan"
|
||||
leftSection={<IconCategory2 size={16} stroke={1.5} />}
|
||||
active={type === "cat-pengaduan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=cat-pelayanan`}
|
||||
label="Kategori Pelayanan Surat"
|
||||
leftSection={<IconMailSpark size={16} stroke={1.5} />}
|
||||
active={type === "cat-pelayanan"}
|
||||
/>
|
||||
<NavLink
|
||||
href={`?type=desa`}
|
||||
label="Desa"
|
||||
leftSection={<IconBuildingBank size={16} stroke={1.5} />}
|
||||
active={type === "desa"}
|
||||
/>
|
||||
{navItems
|
||||
.filter((item) => permissions.includes(item.key))
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.key}
|
||||
onClick={()=>{navigate("?type=" + item.path)}}
|
||||
label={item.label}
|
||||
leftSection={item.icon}
|
||||
active={
|
||||
type === item.path || (!type && item.path === "profile")
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
@@ -83,15 +138,47 @@ export default function DetailSettingPage() {
|
||||
}}
|
||||
>
|
||||
{type === "cat-pengaduan" ? (
|
||||
<KategoriPengaduan />
|
||||
<KategoriPengaduan
|
||||
permissions={permissions.filter(
|
||||
(p) =>
|
||||
typeof p === "string" &&
|
||||
p.startsWith("setting.kategori_pengaduan"),
|
||||
)}
|
||||
/>
|
||||
) : type === "cat-pelayanan" ? (
|
||||
<KategoriPelayananSurat />
|
||||
<KategoriPelayananSurat
|
||||
permissions={permissions.filter(
|
||||
(p) =>
|
||||
typeof p === "string" &&
|
||||
p.startsWith("setting.kategori_pelayanan"),
|
||||
)}
|
||||
/>
|
||||
) : type === "desa" ? (
|
||||
<DesaSetting />
|
||||
<DesaSetting
|
||||
permissions={permissions.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("setting.desa"),
|
||||
)}
|
||||
/>
|
||||
) : type === "user" ? (
|
||||
<UserSetting />
|
||||
<UserSetting
|
||||
permissions={permissions.filter(
|
||||
(p) => typeof p === "string" && p.startsWith("setting.user."),
|
||||
)}
|
||||
/>
|
||||
) : type === "role" ? (
|
||||
<UserRoleSetting
|
||||
permissions={permissions.filter(
|
||||
(p) =>
|
||||
typeof p === "string" && p.startsWith("setting.user_role"),
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<ProfileUser />
|
||||
<ProfileUser
|
||||
permissions={permissions.filter(
|
||||
(p) =>
|
||||
typeof p === "string" && p.startsWith("setting.profile"),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -1,66 +1,146 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CloseButton,
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
LoadingOverlay,
|
||||
Pagination,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import { IconPhone } from "@tabler/icons-react";
|
||||
import { IconPhone, IconSearch } from "@tabler/icons-react";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSwr from "swr";
|
||||
|
||||
export default function DetailWargaPage() {
|
||||
const dataMenu = [
|
||||
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||
{ title: "Warga", link: "/scr/dashboard/warga/list-warga", active: false },
|
||||
{ title: "Detail Warga", link: "#", active: true },
|
||||
];
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const id = query.get("id");
|
||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
apiFetch.api.warga.detail.get({
|
||||
query: {
|
||||
id: id!,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
// const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
// apiFetch.api.warga.detail.get({
|
||||
// query: {
|
||||
// id: id!,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
|
||||
// useShallowEffect(() => {
|
||||
// mutate();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
|
||||
<LoadingOverlay
|
||||
// visible={isLoading}
|
||||
zIndex={1000}
|
||||
overlayProps={{ radius: "sm", blur: 2 }}
|
||||
/>
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<BreadCrumbs dataLink={dataMenu} back />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4}>
|
||||
<DetailWarga data={data?.data?.warga} />
|
||||
<DetailWarga id={id!} />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
|
||||
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
|
||||
<DetailDataHistori
|
||||
id={id!}
|
||||
kategori="pengaduan"
|
||||
/>
|
||||
<DetailDataHistori
|
||||
id={id!}
|
||||
kategori="pelayanan"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
|
||||
function DetailDataHistori({
|
||||
id,
|
||||
kategori,
|
||||
}: {
|
||||
id: string;
|
||||
kategori: "pengaduan" | "pelayanan";
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<any>([]);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalRows, setTotalRows] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
const res = await apiFetch.api.warga.detail.get({
|
||||
query: {
|
||||
id,
|
||||
category: kategori,
|
||||
page: String(page),
|
||||
search
|
||||
}
|
||||
}) as { data: { success: boolean; data: any[]; totalPages: number, totalRows: number } };
|
||||
|
||||
if (res?.data?.success) {
|
||||
setData(res.data.data)
|
||||
setTotalPages(res?.data?.totalPages)
|
||||
setTotalRows(res?.data?.totalRows)
|
||||
} else {
|
||||
setData([])
|
||||
setTotalPages(1)
|
||||
setTotalRows(0)
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed to get data",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed to get data",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
getData()
|
||||
}, [page])
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
setPage(1)
|
||||
if (page == 1) {
|
||||
getData()
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -79,49 +159,83 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
|
||||
<Title order={4} c="gray.2">
|
||||
Histori {_.upperFirst(kategori)}
|
||||
</Title>
|
||||
<Flex
|
||||
gap="md"
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
direction="row"
|
||||
>
|
||||
<Input
|
||||
value={search}
|
||||
placeholder="Cari data..."
|
||||
onChange={(event) => setSearch(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setSearch("")}
|
||||
style={{ display: search ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Text size="sm" c="gray.5" >
|
||||
{`${5 * (page - 1) + 1} – ${Math.min(totalRows, 5 * page)} of ${totalRows}`}
|
||||
</Text>
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
withPages={false}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>No {_.upperFirst(kategori)}</Table.Th>
|
||||
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th>
|
||||
<Table.Th>
|
||||
{kategori == "pengaduan" ? "Judul" : "Kategori"}
|
||||
</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
kategori == "pengaduan" ?
|
||||
navigate(
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
) :
|
||||
navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
|
||||
{data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>
|
||||
{kategori == "pengaduan" ? item.title : item.category}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
kategori == "pengaduan"
|
||||
? navigate(
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
)
|
||||
: navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
}
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4} align="center">
|
||||
Tidak ada data
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
@@ -129,7 +243,33 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
|
||||
);
|
||||
}
|
||||
|
||||
function DetailWarga({ data }: { data: any }) {
|
||||
function DetailWarga({ id }: { id: string }) {
|
||||
const [data, setData] = useState<any>(null);
|
||||
|
||||
async function getWarga() {
|
||||
try {
|
||||
const res = await apiFetch.api.warga.detail.get({
|
||||
query: {
|
||||
id: id,
|
||||
category: "warga",
|
||||
page: "1",
|
||||
search: "",
|
||||
},
|
||||
});
|
||||
setData(res.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Failed",
|
||||
message: "Failed to get data warga",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
getWarga();
|
||||
}, []);
|
||||
return (
|
||||
<Card
|
||||
radius="md"
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
@@ -19,22 +22,31 @@ import useSWR from "swr";
|
||||
|
||||
export default function ListWargaPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
const [pages, setPages] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate } = useSWR("/", () =>
|
||||
apiFetch.api.warga.list.get({
|
||||
query: {
|
||||
search: value,
|
||||
page: pages,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const list = data?.data || [];
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
|
||||
useShallowEffect(() => {
|
||||
setPages(1);
|
||||
mutate();
|
||||
}, [value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [pages]);
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
@@ -48,10 +60,10 @@ export default function ListWargaPage() {
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={3} c="gray.2">
|
||||
List Data Warga
|
||||
</Title>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={3} c="gray.2">
|
||||
List Data Warga
|
||||
</Title>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari warga..."
|
||||
@@ -66,6 +78,15 @@ export default function ListWargaPage() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Group>
|
||||
<Text size="sm">{`${pageSize * (pages - 1) + 1} – ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
|
||||
<Pagination
|
||||
total={totalPage}
|
||||
value={pages}
|
||||
onChange={setPages}
|
||||
withPages={false}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
@@ -77,32 +98,33 @@ export default function ListWargaPage() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
||||
{Array.isArray(list) && list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">
|
||||
Tidak ada data
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
Array.isArray(list) &&
|
||||
list?.map((item, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td w={250}>{item.phone}</Table.Td>
|
||||
<Table.Td w={150}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : (
|
||||
list?.map((item, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>{item.phone}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)
|
||||
}
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
|
||||
@@ -8,16 +8,19 @@ export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }:
|
||||
idCategory,
|
||||
idWarga,
|
||||
noSurat,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!surat.id) {
|
||||
return { success: false, message: 'gagal membuat surat' }
|
||||
return { success: false, message: 'gagal membuat surat', idSurat: '' }
|
||||
}
|
||||
|
||||
return { success: true, message: 'surat sudah dibuat' }
|
||||
return { success: true, message: 'surat sudah dibuat', idSurat: surat.id }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
return { success: false, message: 'gagal membuat surat' }
|
||||
}
|
||||
|
||||
|
||||
25
src/server/lib/detect-type-of-file.ts
Normal file
25
src/server/lib/detect-type-of-file.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
function getExtension(fileName: string): string | null {
|
||||
if (!fileName || typeof fileName !== "string") return null;
|
||||
|
||||
const parts = fileName.split(".");
|
||||
if (parts.length <= 1) return null;
|
||||
|
||||
return parts.pop()?.toLowerCase() || null;
|
||||
}
|
||||
|
||||
|
||||
export function detectFileType(fileName: string) {
|
||||
const ext = getExtension(fileName);
|
||||
|
||||
if (!ext) return { ext: null, type: "unknown" };
|
||||
|
||||
if (["jpg", "jpeg", "png", "gif", "webp", "bmp"].includes(ext)) {
|
||||
return { ext, type: "image" };
|
||||
}
|
||||
|
||||
if (ext === "pdf") {
|
||||
return { ext, type: "pdf" };
|
||||
}
|
||||
|
||||
return { ext, type: "other" };
|
||||
}
|
||||
@@ -16,8 +16,11 @@ interface McpTool {
|
||||
|
||||
/**
|
||||
* Convert OpenAPI 3.x JSON spec into MCP-compatible tool definitions.
|
||||
* * @param openApiJson OpenAPI JSON specification object.
|
||||
* @param filterTag A string or array of strings. Operations must match at least one tag
|
||||
* (case-insensitive partial match).
|
||||
*/
|
||||
export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): McpTool[] {
|
||||
export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string | string[]): McpTool[] {
|
||||
const tools: McpTool[] = [];
|
||||
|
||||
if (!openApiJson || typeof openApiJson !== "object") {
|
||||
@@ -25,6 +28,15 @@ export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): M
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Cast filterTag to an array and normalize to lowercase for comparison
|
||||
const filterTags = _.castArray(filterTag)
|
||||
.filter(t => typeof t === "string" && t.trim() !== "")
|
||||
.map(t => t.toLowerCase());
|
||||
|
||||
if (filterTags.length === 0) {
|
||||
console.warn("Filter tag is empty or invalid. Returning all tools with tags.");
|
||||
}
|
||||
|
||||
const paths = openApiJson.paths || {};
|
||||
|
||||
if (Object.keys(paths).length === 0) {
|
||||
@@ -34,7 +46,6 @@ export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): M
|
||||
|
||||
for (const [path, methods] of Object.entries(paths)) {
|
||||
if (!path || typeof path !== "string") continue;
|
||||
if (path.startsWith("/mcp")) continue;
|
||||
|
||||
if (!methods || typeof methods !== "object") continue;
|
||||
|
||||
@@ -45,10 +56,19 @@ export function convertOpenApiToMcpTools(openApiJson: any, filterTag: string): M
|
||||
if (!operation || typeof operation !== "object") continue;
|
||||
|
||||
const tags: string[] = Array.isArray(operation.tags) ? operation.tags : [];
|
||||
const lowerCaseTags = tags.map(t => typeof t === "string" ? t.toLowerCase() : "");
|
||||
|
||||
if (!tags.length || !tags.some(t =>
|
||||
typeof t === "string" && t.toLowerCase().includes(filterTag)
|
||||
)) continue;
|
||||
// ✅ MODIFIKASI: Pengecekan filterTags
|
||||
if (filterTags.length > 0) {
|
||||
const isTagMatch = lowerCaseTags.some(opTag =>
|
||||
filterTags.some(fTag => opTag.includes(fTag))
|
||||
);
|
||||
|
||||
if (!isTagMatch) continue;
|
||||
} else if (tags.length === 0) {
|
||||
// Jika tidak ada filter, hanya proses operation yang memiliki tags
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tool = createToolFromOperation(path, method, operation, tags);
|
||||
@@ -75,18 +95,20 @@ function createToolFromOperation(
|
||||
tags: string[]
|
||||
): McpTool | null {
|
||||
try {
|
||||
const rawName = _.snakeCase(operation.operationId || `${method}_${path}`) || "unnamed_tool";
|
||||
const name = cleanToolName(rawName);
|
||||
const rawName = _.snakeCase(`${operation.operationId}` || `${method}_${path}`) || "unnamed_tool";
|
||||
const name = _.snakeCase(cleanToolName(operation.summary)) || cleanToolName(rawName);
|
||||
|
||||
if (!name || name === "unnamed_tool") {
|
||||
console.warn(`Invalid tool name for ${method} ${path}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const description =
|
||||
let description =
|
||||
operation.description ||
|
||||
operation.summary ||
|
||||
`Execute ${method.toUpperCase()} ${path}`;
|
||||
operation.summary;
|
||||
|
||||
description += `\n
|
||||
Execute ${method.toUpperCase()} ${path}`;
|
||||
|
||||
// ✅ Extract schema berdasarkan method
|
||||
let schema;
|
||||
@@ -343,9 +365,8 @@ function cleanToolName(name: string): string {
|
||||
.replace(/[^a-zA-Z0-9_]/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_|_$/g, "")
|
||||
.replace(/^(get|post|put|delete|patch|api)_/i, "")
|
||||
.replace(/^(get_|post_|put_|delete_|patch_|api_)+/gi, "")
|
||||
.replace(/(^_|_$)/g, "")
|
||||
// ❗️ METHOD PREFIX TIDAK DIHAPUS LAGI (agar tidak duplicate)
|
||||
.toLowerCase()
|
||||
|| "unnamed_tool";
|
||||
} catch (error) {
|
||||
console.error("Error cleaning tool name:", error);
|
||||
@@ -353,10 +374,14 @@ function cleanToolName(name: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ambil OpenAPI JSON dari endpoint dan konversi ke tools MCP
|
||||
* * @param url URL of the OpenAPI spec.
|
||||
* @param filterTag A string or array of strings. Operations must match at least one tag
|
||||
* (case-insensitive partial match).
|
||||
*/
|
||||
export async function getMcpTools(url: string, filterTag: string): Promise<McpTool[]> {
|
||||
export async function getMcpTools(url: string, filterTag: string | string[]): Promise<McpTool[]> {
|
||||
try {
|
||||
|
||||
console.log(`Fetching OpenAPI spec from: ${url}`);
|
||||
@@ -370,12 +395,12 @@ export async function getMcpTools(url: string, filterTag: string): Promise<McpTo
|
||||
const openApiJson = await response.json();
|
||||
const tools = convertOpenApiToMcpTools(openApiJson, filterTag);
|
||||
|
||||
console.log(`✅ Successfully generated ${tools.length} MCP tools`);
|
||||
const filterStr = _.castArray(filterTag).join(", ");
|
||||
console.log(`✅ Successfully generated ${tools.length} MCP tools for tags: [${filterStr}]`);
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MCP tools:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
export function isValidPhone(number: string): boolean {
|
||||
const clean = number.replace(/[\s.-]/g, ""); // hapus spasi, titik, strip
|
||||
const regex = /^(?:\+62|62|0)8\d{7,12}$/;
|
||||
return regex.test(clean);
|
||||
}
|
||||
|
||||
export function normalizePhoneNumber({ phone }: { phone: string }) {
|
||||
// Hapus semua spasi, tanda hubung, atau karakter non-digit (+ tetap dipertahankan untuk dicek)
|
||||
let cleaned = phone.trim().replace(/[\s-]/g, "");
|
||||
let cleaned = phone.trim().replace(/[\s.-]/g, "");
|
||||
|
||||
// Jika diawali dengan +62 → ganti jadi 62
|
||||
if (cleaned.startsWith("+62")) {
|
||||
|
||||
@@ -94,7 +94,6 @@ export async function fetchWithAuth(config: Config, url: string, options: Reques
|
||||
} catch {
|
||||
console.error('🔍 Could not read response body');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -139,7 +138,7 @@ export async function catFile(config: Config, folder: string, fileName: string):
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
export async function uploadFile(config: Config, file: File, folder: string): Promise<string> {
|
||||
const remoteName = path.basename(file.name);
|
||||
|
||||
// 1. Dapatkan upload link (pakai Authorization)
|
||||
@@ -152,7 +151,7 @@ export async function uploadFile(config: Config, file: File): Promise<string> {
|
||||
// 2. Siapkan form-data
|
||||
const formData = new FormData();
|
||||
formData.append("parent_dir", "/");
|
||||
formData.append("relative_path", "syarat-dokumen"); // tanpa slash di akhir
|
||||
formData.append("relative_path", folder); // tanpa slash di akhir
|
||||
formData.append("file", file, remoteName); // file langsung, jangan pakai Blob
|
||||
|
||||
// 3. Upload file TANPA Authorization header, token di query param
|
||||
@@ -232,10 +231,10 @@ export async function uploadFileToFolder(config: Config, base64File: { name: str
|
||||
}
|
||||
|
||||
|
||||
export async function removeFile(config: Config, fileName: string, folder: string): Promise<string> {
|
||||
const res = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${folder}/${fileName}`, { method: 'DELETE' });
|
||||
|
||||
|
||||
export async function removeFile(config: Config, fileName: string): Promise<string> {
|
||||
await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${fileName}`, { method: 'DELETE' });
|
||||
if (!res.ok) return 'gagal menghapus file';
|
||||
return `🗑️ Removed ${fileName}`
|
||||
}
|
||||
|
||||
@@ -249,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string)
|
||||
return `✏️ Renamed ${oldName} → ${newName}`
|
||||
}
|
||||
|
||||
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> {
|
||||
const localName = localFile || remoteFile;
|
||||
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`);
|
||||
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||
export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
|
||||
const localName = localFile || fileName;
|
||||
// 🔹 gabungkan path folder + file
|
||||
const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/");
|
||||
|
||||
// 🔹 encode path agar aman (spasi, dll)
|
||||
const params = new URLSearchParams({
|
||||
p: filePath,
|
||||
});
|
||||
|
||||
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`);
|
||||
if(!downloadUrlResponse.ok)
|
||||
return 'gagal'
|
||||
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
|
||||
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
|
||||
await fs.writeFile(localName, buffer);
|
||||
return `⬇️ Downloaded ${remoteFile} → ${localName}`
|
||||
return `⬇️ Downloaded ${fileName} → ${localName}`
|
||||
}
|
||||
|
||||
export async function getFileLink(config: Config, fileName: string): Promise<string> {
|
||||
|
||||
18
src/server/lib/slug_converter.ts
Normal file
18
src/server/lib/slug_converter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function toSlug(text: string): string {
|
||||
return encodeURIComponent(
|
||||
text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
export function fromSlug(slug: string): string {
|
||||
return decodeURIComponent(slug)
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function capitalizeWords(text: string): string {
|
||||
return text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
11
src/server/lib/stringToDate.ts
Normal file
11
src/server/lib/stringToDate.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import "dayjs/locale/id";
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
export function parseTanggalID( value: string ): Date | null {
|
||||
if (!value) return null;
|
||||
|
||||
const parsed = dayjs(value, "DD MMMM YYYY", "id", true);
|
||||
return parsed.isValid() ? parsed.toDate() : null;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { prisma } from '@/server/lib/prisma'
|
||||
import { jwt as jwtPlugin, type JWTPayloadSpec } from '@elysiajs/jwt'
|
||||
import Elysia, { t, type Cookie, type HTTPHeaders, type StatusMap } from 'elysia'
|
||||
import { type ElysiaCookie } from 'elysia/cookies'
|
||||
import { prisma } from '@/server/lib/prisma'
|
||||
|
||||
const secret = process.env.JWT_SECRET
|
||||
if (!secret) {
|
||||
@@ -75,6 +75,15 @@ async function login({
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: {
|
||||
id: true,
|
||||
password: true,
|
||||
Role: {
|
||||
select: {
|
||||
permissions: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
@@ -87,6 +96,12 @@ async function login({
|
||||
return { message: 'Invalid password' }
|
||||
}
|
||||
|
||||
const rawPermissions = user.Role?.permissions;
|
||||
|
||||
const akses = Array.isArray(rawPermissions)
|
||||
? rawPermissions[0]?.toString()
|
||||
: undefined;
|
||||
|
||||
const token = await issueToken({
|
||||
jwt,
|
||||
cookie,
|
||||
@@ -94,7 +109,7 @@ async function login({
|
||||
role: 'user',
|
||||
expiresAt: Math.floor(Date.now() / 1000) + NINETY_YEARS,
|
||||
})
|
||||
return { token }
|
||||
return { token, akses }
|
||||
} catch (error) {
|
||||
console.error('Error logging in:', error)
|
||||
return {
|
||||
@@ -146,7 +161,7 @@ const Auth = new Elysia({
|
||||
detail: {
|
||||
summary: 'logout',
|
||||
description: 'Logout (clear token cookie)',
|
||||
|
||||
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
278
src/server/routes/dashboard_route.ts
Normal file
278
src/server/routes/dashboard_route.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import Elysia from "elysia";
|
||||
import { getLastUpdated } from "../lib/get-last-updated";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const DashboardRoute = new Elysia({
|
||||
prefix: "dashboard",
|
||||
tags: ["dashboard"],
|
||||
})
|
||||
|
||||
.get("/count", async () => {
|
||||
// ---- RANGE HARI INI ----
|
||||
const now = new Date();
|
||||
|
||||
const startOfToday = new Date(now);
|
||||
startOfToday.setHours(0, 0, 0, 0);
|
||||
|
||||
const endOfToday = new Date(now);
|
||||
endOfToday.setHours(23, 59, 59, 999);
|
||||
|
||||
// ---- RANGE KEMARIN ----
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const startOfYesterday = new Date(yesterday);
|
||||
startOfYesterday.setHours(0, 0, 0, 0);
|
||||
|
||||
const endOfYesterday = new Date(yesterday);
|
||||
endOfYesterday.setHours(23, 59, 59, 999);
|
||||
|
||||
// ---- QUERY ----
|
||||
|
||||
const dataWarga = await prisma.warga.count();
|
||||
|
||||
// Pengaduan
|
||||
const dataPengaduanToday = await prisma.pengaduan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfToday,
|
||||
lte: endOfToday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dataPengaduanYesterday = await prisma.pengaduan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfYesterday,
|
||||
lte: endOfYesterday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const kenaikanPengaduan =
|
||||
dataPengaduanYesterday === 0
|
||||
? dataPengaduanToday > 0
|
||||
? dataPengaduanToday * 100
|
||||
: 0
|
||||
: ((dataPengaduanToday - dataPengaduanYesterday) / dataPengaduanYesterday) * 100;
|
||||
|
||||
// Pelayanan
|
||||
const dataPelayananToday = await prisma.pelayananAjuan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfToday,
|
||||
lte: endOfToday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const dataPelayananYesterday = await prisma.pelayananAjuan.count({
|
||||
where: {
|
||||
isActive: true,
|
||||
status: "antrian",
|
||||
createdAt: {
|
||||
gte: startOfYesterday,
|
||||
lte: endOfYesterday,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const kenaikanPelayanan =
|
||||
dataPelayananYesterday === 0
|
||||
? dataPelayananToday > 0
|
||||
? dataPelayananToday * 100
|
||||
: 0
|
||||
: ((dataPelayananToday - dataPelayananYesterday) / dataPelayananYesterday) * 100;
|
||||
|
||||
// ---- FINAL OUTPUT ----
|
||||
|
||||
const dataFix = {
|
||||
warga: dataWarga,
|
||||
pengaduan: {
|
||||
today: dataPengaduanToday,
|
||||
yesterday: dataPengaduanYesterday,
|
||||
kenaikan: Number(kenaikanPengaduan.toFixed(2)), // dalam persen
|
||||
},
|
||||
pelayanan: {
|
||||
today: dataPelayananToday,
|
||||
yesterday: dataPelayananYesterday,
|
||||
kenaikan: Number(kenaikanPelayanan.toFixed(2)), // dalam persen
|
||||
},
|
||||
};
|
||||
|
||||
return dataFix;
|
||||
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - Menghitung Data",
|
||||
description: `tool untuk menghitung data pengaduan dan pelayanan yg masuk hari ini dan data warga`,
|
||||
}
|
||||
})
|
||||
.get("/last-update", async () => {
|
||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||
skip: 0,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
})
|
||||
|
||||
const dataPengaduanFix = dataPengaduan.map((item) => {
|
||||
return {
|
||||
noPengaduan: item.noPengaduan,
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||
skip: 0,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
noPengajuan: true,
|
||||
updatedAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayananFix = dataPelayanan.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
noPengajuan: item.noPengajuan,
|
||||
title: item.CategoryPelayanan.name,
|
||||
status: item.status,
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
const dataFix = {
|
||||
pengaduan: dataPengaduanFix,
|
||||
pelayanan: dataPelayananFix,
|
||||
}
|
||||
|
||||
return dataFix;
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - List data pengaduan dan pelayanan terupdate",
|
||||
description: `tool untuk mendapatkan list data pengaduan dan pelayanan yg terupdate`,
|
||||
}
|
||||
})
|
||||
.get("/grafik", async () => {
|
||||
const now = new Date();
|
||||
|
||||
const start7Days = new Date(now);
|
||||
start7Days.setDate(start7Days.getDate() - 7);
|
||||
start7Days.setHours(0, 0, 0, 0);
|
||||
|
||||
const endToday = new Date(now);
|
||||
endToday.setHours(23, 59, 59, 999);
|
||||
|
||||
// Ambil semua data pengaduan & pelayanan dalam 7 hari
|
||||
const pengaduan = await prisma.pengaduan.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start7Days,
|
||||
lte: endToday,
|
||||
},
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const pelayanan = await prisma.pelayananAjuan.findMany({
|
||||
where: {
|
||||
createdAt: {
|
||||
gte: start7Days,
|
||||
lte: endToday,
|
||||
},
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
createdAt: true
|
||||
}
|
||||
});
|
||||
|
||||
// --- BUAT RANGE TANGGAL 7 HARI ---
|
||||
const resultMap: Record<string, { pengaduan: number; pelayanan: number }> = {};
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const d = new Date(start7Days);
|
||||
d.setDate(d.getDate() + i);
|
||||
|
||||
const formatted = d.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
resultMap[formatted] = { pengaduan: 0, pelayanan: 0 };
|
||||
}
|
||||
|
||||
// --- HITUNG PENGADUAN PER HARI ---
|
||||
pengaduan.forEach((item) => {
|
||||
const t = item.createdAt.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
if (resultMap[t]) {
|
||||
resultMap[t].pengaduan += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// --- HITUNG PELAYANAN PER HARI ---
|
||||
pelayanan.forEach((item) => {
|
||||
const t = item.createdAt.toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long"
|
||||
});
|
||||
|
||||
if (resultMap[t]) {
|
||||
resultMap[t].pelayanan += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// --- KONVERSI KE FORMAT FINAL ---
|
||||
const source = Object.keys(resultMap).map((tanggal) => ({
|
||||
tanggal,
|
||||
pengaduan: resultMap[tanggal]?.pengaduan,
|
||||
pelayanan: resultMap[tanggal]?.pelayanan,
|
||||
}));
|
||||
|
||||
return {
|
||||
dimensions: ["tanggal", "pengaduan", "pelayanan"],
|
||||
source,
|
||||
};
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Dashboard - Grafik data pengaduan dan pelayanan",
|
||||
description: `tool untuk mendapatkan grafik data pengaduan dan pelayanan`,
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
;
|
||||
|
||||
export default DashboardRoute
|
||||
@@ -1,250 +1,485 @@
|
||||
// server/mcpServer.ts
|
||||
import { Elysia } from "elysia";
|
||||
import { getMcpTools } from "../lib/mcp_tool_convert";
|
||||
|
||||
var tools = [] as any[];
|
||||
const OPENAPI_URL = process.env.BUN_PUBLIC_BASE_URL + "/docs/json";
|
||||
const FILTER_TAG = "mcp";
|
||||
/**
|
||||
* Refactored Elysia-based MCP server
|
||||
* - Fixes inconsistent "text/json" handling by normalizing response extraction
|
||||
* - Robust executeTool: supports path/query/header/cookie/body params (if provided in x-props)
|
||||
* - Proper baseUrl/path normalization and URLSearchParams building (repeated keys for arrays)
|
||||
* - Consistent MCP content conversion: always returns either { type: 'json', data } or { type: 'text', text }
|
||||
* - Safer error handling, batch support, Promise.allSettled to avoid full failure on single-item error
|
||||
* - Lightweight in-memory tools cache with explicit init endpoint (keeps original behavior)
|
||||
*/
|
||||
|
||||
/* -------------------------
|
||||
Environment & Globals
|
||||
------------------------- */
|
||||
if (!process.env.BUN_PUBLIC_BASE_URL) {
|
||||
throw new Error("BUN_PUBLIC_BASE_URL environment variable is not set");
|
||||
throw new Error("BUN_PUBLIC_BASE_URL environment variable is not set");
|
||||
}
|
||||
|
||||
// =====================
|
||||
// MCP Protocol Types
|
||||
// =====================
|
||||
const OPENAPI_URL = `${process.env.BUN_PUBLIC_BASE_URL.replace(/\/+$/, "")}/docs/json`;
|
||||
const FILTER_TAG = "mcp";
|
||||
|
||||
let tools: any[] = [];
|
||||
|
||||
/* -------------------------
|
||||
MCP Types
|
||||
------------------------- */
|
||||
type JSONRPCRequest = {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: any;
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
method: string;
|
||||
params?: any;
|
||||
credentials?: any;
|
||||
};
|
||||
|
||||
type JSONRPCResponse = {
|
||||
jsonrpc: "2.0";
|
||||
id: string | number;
|
||||
result?: any;
|
||||
error?: {
|
||||
code: number;
|
||||
message: string;
|
||||
data?: any;
|
||||
};
|
||||
jsonrpc: "2.0";
|
||||
id: string | number | null;
|
||||
result?: any;
|
||||
error?: { code: number; message: string; data?: any };
|
||||
};
|
||||
|
||||
// =====================
|
||||
// Tool Executor
|
||||
// =====================
|
||||
/* -------------------------
|
||||
Helpers
|
||||
------------------------- */
|
||||
|
||||
/** Ensure baseUrl doesn't end with slash; ensure path begins with slash */
|
||||
function joinBasePath(base: string, path: string) {
|
||||
const normalizedBase = base.replace(/\/+$/, "");
|
||||
const normalizedPath = path ? (path.startsWith("/") ? path : `/${path}`) : "";
|
||||
return `${normalizedBase}${normalizedPath}`;
|
||||
}
|
||||
|
||||
/** Serialize query object to repeated-key QS when arrays provided */
|
||||
function buildQueryString(q: Record<string, any>): string {
|
||||
const parts: string[] = [];
|
||||
for (const [k, v] of Object.entries(q)) {
|
||||
if (v === undefined || v === null) continue;
|
||||
if (Array.isArray(v)) {
|
||||
for (const item of v) {
|
||||
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(item))}`);
|
||||
}
|
||||
} else if (typeof v === "object") {
|
||||
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(JSON.stringify(v))}`);
|
||||
} else {
|
||||
parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
|
||||
}
|
||||
}
|
||||
return parts.length ? `?${parts.join("&")}` : "";
|
||||
}
|
||||
|
||||
/** Safely extract "useful" payload from a fetch result:
|
||||
* Prefer resp.data if present, otherwise resp itself.
|
||||
* If resp is a string, keep as string.
|
||||
*/
|
||||
function extractRaw(result: { data: any } | any) {
|
||||
// If result shaped as { data: ... } prefer inner .data
|
||||
if (result && typeof result === "object" && "data" in result) {
|
||||
return result.data;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Convert various payloads into MCP content shape */
|
||||
function convertToMcpContent(payload: any) {
|
||||
if (typeof payload === "string") {
|
||||
return { type: "text", text: payload };
|
||||
}
|
||||
|
||||
if (payload == null) {
|
||||
return { type: "text", text: String(payload) };
|
||||
}
|
||||
|
||||
// If payload looks like an image/audio wrapper produced by converter
|
||||
if (payload?.__mcp_type === "image" && payload.base64) {
|
||||
return { type: "image", data: payload.base64, mimeType: payload.mimeType || "image/png" };
|
||||
}
|
||||
if (payload?.__mcp_type === "audio" && payload.base64) {
|
||||
return { type: "audio", data: payload.base64, mimeType: payload.mimeType || "audio/mpeg" };
|
||||
}
|
||||
|
||||
// If already an object/array → return JSON
|
||||
if (typeof payload === "object") {
|
||||
return { type: "json", data: payload };
|
||||
}
|
||||
|
||||
// Fallback — stringify
|
||||
try {
|
||||
return { type: "text", text: JSON.stringify(payload) };
|
||||
} catch {
|
||||
return { type: "text", text: String(payload) };
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------
|
||||
executeTool (robust)
|
||||
------------------------- */
|
||||
/**
|
||||
* Execute a tool converted from OpenAPI -> expected x-props shape:
|
||||
* x-props may contain:
|
||||
* - method, path
|
||||
* - parameters: [{ name, in, required? }]
|
||||
*
|
||||
* If x.parameters present, we inspect args and place them accordingly.
|
||||
*/
|
||||
export async function executeTool(
|
||||
tool: any,
|
||||
args: Record<string, any> = {},
|
||||
baseUrl: string
|
||||
tool: any,
|
||||
args: Record<string, any> = {},
|
||||
baseUrl: string,
|
||||
xPayload: Record<string, any> = {}
|
||||
) {
|
||||
const x = tool["x-props"] || {};
|
||||
const x = tool["x-props"] || {};
|
||||
const method = (x.method || "GET").toUpperCase();
|
||||
|
||||
const method = (x.method || "GET").toUpperCase();
|
||||
const path = x.path || `/${tool.name}`;
|
||||
const url = `${baseUrl}${path}`;
|
||||
// Start with provided path (may contain {param})
|
||||
let path = x.path ?? `/${tool.name}`;
|
||||
|
||||
const opts: RequestInit = {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
};
|
||||
// Headers, cookies, query, body collection
|
||||
const headers: Record<string, any> = {
|
||||
"Content-Type": "application/json",
|
||||
...(x.defaultHeaders || {}),
|
||||
};
|
||||
const query: Record<string, any> = {};
|
||||
const cookies: string[] = [];
|
||||
let bodyPayload: any = undefined;
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
opts.body = JSON.stringify(args || {});
|
||||
// If parameters described, map args accordingly
|
||||
if (Array.isArray(x.parameters)) {
|
||||
for (const p of x.parameters) {
|
||||
try {
|
||||
const name: string = p.name;
|
||||
const value = args?.[name];
|
||||
|
||||
// skip undefined unless required — we let API validate required semantics
|
||||
if (value === undefined) continue;
|
||||
|
||||
switch (p.in) {
|
||||
case "path":
|
||||
if (path.includes(`{${name}}`)) {
|
||||
path = path.replace(new RegExp(`{${name}}`, "g"), encodeURIComponent(String(value)));
|
||||
} else {
|
||||
// fallback to query
|
||||
query[name] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case "query":
|
||||
query[name] = value;
|
||||
break;
|
||||
|
||||
case "header":
|
||||
headers[name] = value;
|
||||
break;
|
||||
|
||||
case "cookie":
|
||||
cookies.push(`${name}=${value}`);
|
||||
break;
|
||||
|
||||
case "body":
|
||||
case "requestBody":
|
||||
bodyPayload = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
// unknown location -> place into body
|
||||
bodyPayload = bodyPayload ?? {};
|
||||
bodyPayload[name] = value;
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
// best-effort: skip problematic param
|
||||
console.warn(`[MCP] Skipping parameter ${String(p?.name)} due to error:`, err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no param descriptions: assume all args are body
|
||||
bodyPayload = Object.keys(args || {}).length ? args : undefined;
|
||||
}
|
||||
|
||||
const res = await fetch(url, opts);
|
||||
const contentType = res.headers.get("content-type") || "";
|
||||
const data = contentType.includes("application/json")
|
||||
? await res.json()
|
||||
: await res.text();
|
||||
if (cookies.length) {
|
||||
headers["Cookie"] = cookies.join("; ");
|
||||
}
|
||||
|
||||
return {
|
||||
success: res.ok,
|
||||
status: res.status,
|
||||
method,
|
||||
path,
|
||||
data,
|
||||
};
|
||||
// Build full URL
|
||||
const urlBase = baseUrl || process.env.BUN_PUBLIC_BASE_URL!;
|
||||
let url = joinBasePath(urlBase, path);
|
||||
const qs = buildQueryString(query);
|
||||
if (qs) url += qs;
|
||||
|
||||
// Build RequestInit
|
||||
const opts: RequestInit & { headers?: Record<string, any> } = { method, headers };
|
||||
|
||||
// Body handling for applicable methods
|
||||
const bodyMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
||||
const contentTypeLower = (headers["Content-Type"] || "").toLowerCase();
|
||||
|
||||
if (bodyMethods.has(method) && bodyPayload !== undefined) {
|
||||
// Support tiny formdata marker shape: { __formdata: true, entries: [ [k,v], ... ] }
|
||||
if (bodyPayload && bodyPayload.__formdata === true && Array.isArray(bodyPayload.entries)) {
|
||||
const form = new FormData();
|
||||
for (const [k, v] of bodyPayload.entries) {
|
||||
form.append(k, v as any);
|
||||
}
|
||||
// Let fetch set boundary
|
||||
delete opts.headers!["Content-Type"];
|
||||
opts.body = form as any;
|
||||
} else if (contentTypeLower.includes("application/x-www-form-urlencoded")) {
|
||||
opts.body = new URLSearchParams(bodyPayload as Record<string, string>).toString();
|
||||
} else if (contentTypeLower.includes("multipart/form-data")) {
|
||||
// If caller explicitly requested multipart but didn't pass FormData — convert object to form
|
||||
const form = new FormData();
|
||||
if (typeof bodyPayload === "object") {
|
||||
for (const [k, v] of Object.entries(bodyPayload)) {
|
||||
form.append(k, (v as any) as any);
|
||||
}
|
||||
} else {
|
||||
form.append("payload", String(bodyPayload));
|
||||
}
|
||||
delete opts.headers!["Content-Type"];
|
||||
opts.body = form as any;
|
||||
} else {
|
||||
// Default JSON
|
||||
opts.body = JSON.stringify(bodyPayload);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute fetch
|
||||
console.log(`[MCP] → ${method} ${url}`);
|
||||
for(const [key, value] of Object.entries(xPayload)) {
|
||||
opts.headers![key] = value;
|
||||
}
|
||||
const res = await fetch(url, opts);
|
||||
|
||||
const resContentType = (res.headers.get("content-type") || "").toLowerCase();
|
||||
|
||||
let data: any;
|
||||
try {
|
||||
if (resContentType.includes("application/json")) {
|
||||
data = await res.json();
|
||||
} else {
|
||||
data = await res.text();
|
||||
}
|
||||
} catch (err) {
|
||||
// fallback to text
|
||||
try {
|
||||
data = await res.text();
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: res.ok,
|
||||
status: res.status,
|
||||
method,
|
||||
url,
|
||||
path,
|
||||
headers: res.headers,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
// =====================
|
||||
// MCP Handler (Async)
|
||||
// =====================
|
||||
async function handleMCPRequestAsync(
|
||||
request: JSONRPCRequest
|
||||
): Promise<JSONRPCResponse> {
|
||||
const { id, method, params } = request;
|
||||
/* -------------------------
|
||||
JSON-RPC Handler
|
||||
------------------------- */
|
||||
async function handleMCPRequestAsync(request: JSONRPCRequest, xPayload: Record<string, any>): Promise<JSONRPCResponse> {
|
||||
const { id, method, params } = request;
|
||||
|
||||
switch (method) {
|
||||
case "initialize":
|
||||
const makeError = (code: number, message: string, data?: any): JSONRPCResponse => ({
|
||||
jsonrpc: "2.0",
|
||||
id: id ?? null,
|
||||
error: { code, message, data },
|
||||
});
|
||||
|
||||
switch (method) {
|
||||
case "initialize":
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
||||
},
|
||||
};
|
||||
|
||||
case "tools/list":
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
tools: tools.map((t) => {
|
||||
const inputSchema =
|
||||
typeof t.inputSchema === "object" && t.inputSchema?.type === "object"
|
||||
? t.inputSchema
|
||||
: { type: "object", properties: {}, required: [] };
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "elysia-mcp-server", version: "1.0.0" },
|
||||
},
|
||||
name: t.name,
|
||||
description: t.description || "No description provided",
|
||||
inputSchema,
|
||||
"x-props": t["x-props"],
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
case "tools/list":
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||
name,
|
||||
description,
|
||||
inputSchema,
|
||||
"x-props": x,
|
||||
})),
|
||||
},
|
||||
};
|
||||
case "tools/call": {
|
||||
const toolName = params?.name;
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
if (!tool) return makeError(-32601, `Tool '${toolName}' not found`);
|
||||
|
||||
case "tools/call": {
|
||||
const toolName = params?.name;
|
||||
const tool = tools.find((t) => t.name === toolName);
|
||||
try {
|
||||
const baseUrl = (params?.credentials?.baseUrl as string) || process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const args = params?.arguments || {};
|
||||
|
||||
if (!tool) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Tool '${toolName}' not found` },
|
||||
};
|
||||
}
|
||||
const result = await executeTool(tool, args, baseUrl, xPayload);
|
||||
|
||||
try {
|
||||
const baseUrl =
|
||||
process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const result = await executeTool(tool, params?.arguments || {}, baseUrl);
|
||||
const data = result.data.data;
|
||||
const isObject = typeof data === "object" && data !== null;
|
||||
// Extract the meaningful payload (prefer nested .data if present)
|
||||
const raw = extractRaw(result.data);
|
||||
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
isObject
|
||||
? { type: "json", data: data }
|
||||
: { type: "text", text: JSON.stringify(data || result.data || result) },
|
||||
],
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32603, message: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
// Normalize content shape consistently:
|
||||
const contentItem = convertToMcpContent(raw ?? result.data ?? result);
|
||||
|
||||
case "ping":
|
||||
return { jsonrpc: "2.0", id, result: {} };
|
||||
|
||||
default:
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Method '${method}' not found` },
|
||||
};
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [contentItem],
|
||||
},
|
||||
};
|
||||
} catch (err: any) {
|
||||
// avoid leaking secrets — small debug
|
||||
const dbg = { message: err?.message };
|
||||
return makeError(-32603, err?.message ?? "Internal error", dbg);
|
||||
}
|
||||
}
|
||||
|
||||
case "ping":
|
||||
return { jsonrpc: "2.0", id, result: {} };
|
||||
|
||||
default:
|
||||
return makeError(-32601, `Method '${method}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// Elysia MCP Server
|
||||
// =====================
|
||||
export const MCPRoute = new Elysia({
|
||||
tags: ["MCP Server"]
|
||||
})
|
||||
.post("/mcp", async ({ request, set }) => {
|
||||
if (!tools.length) {
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Content-Type"] = "application/json";
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
/* -------------------------
|
||||
Elysia App & Routes
|
||||
------------------------- */
|
||||
export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
|
||||
.post("/mcp", async ({ request, set, headers }) => {
|
||||
set.headers["Content-Type"] = "application/json";
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
// Lazy load the tools (keeps previous behavior)
|
||||
if (!tools.length) {
|
||||
try {
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
} catch (err) {
|
||||
console.error("[MCP] Failed to load tools during lazy init:", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(body)) {
|
||||
const res = await handleMCPRequestAsync(body);
|
||||
return res;
|
||||
}
|
||||
const xPayload = {
|
||||
['x-user']: headers['x-user'] || "",
|
||||
['x-phone']: headers['x-phone'] || ""
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
body.map((req) => handleMCPRequestAsync(req))
|
||||
);
|
||||
return results;
|
||||
} catch (error: any) {
|
||||
set.status = 400;
|
||||
return {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// If batch array -> allSettled for resilience
|
||||
if (Array.isArray(body)) {
|
||||
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req, xPayload));
|
||||
const settled = await Promise.allSettled(promises);
|
||||
const responses = settled.map((s) =>
|
||||
s.status === "fulfilled"
|
||||
? s.value
|
||||
: ({
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
error: {
|
||||
code: -32700,
|
||||
message: "Parse error",
|
||||
data: error.message,
|
||||
code: -32000,
|
||||
message: "Unhandled handler error",
|
||||
data: String((s as PromiseRejectedResult).reason),
|
||||
},
|
||||
};
|
||||
}
|
||||
})
|
||||
} as JSONRPCResponse)
|
||||
);
|
||||
return responses;
|
||||
}
|
||||
|
||||
// Tools list (debug)
|
||||
.get("/mcp/tools", async ({ set }) => {
|
||||
if (!tools.length) {
|
||||
const single = await handleMCPRequestAsync(body as JSONRPCRequest, xPayload);
|
||||
return single;
|
||||
} catch (err: any) {
|
||||
set.status = 400;
|
||||
return {
|
||||
jsonrpc: "2.0",
|
||||
id: null,
|
||||
error: { code: -32700, message: "Parse error", data: err?.message ?? String(err) },
|
||||
} as JSONRPCResponse;
|
||||
}
|
||||
})
|
||||
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
}
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return {
|
||||
tools: tools.map(({ name, description, inputSchema, ["x-props"]: x }) => ({
|
||||
name,
|
||||
description,
|
||||
inputSchema,
|
||||
"x-props": x,
|
||||
})),
|
||||
};
|
||||
})
|
||||
/* Debug / management endpoints */
|
||||
.get("/mcp/tools", async ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
if (!tools.length) {
|
||||
try {
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
} catch (err) {
|
||||
console.error("[MCP] Failed to load tools for /mcp/tools:", err);
|
||||
}
|
||||
}
|
||||
return {
|
||||
tools: tools.map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
inputSchema: t.inputSchema,
|
||||
"x-props": t["x-props"],
|
||||
})),
|
||||
};
|
||||
})
|
||||
|
||||
// MCP status
|
||||
.get("/mcp/status", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "active", timestamp: Date.now() };
|
||||
})
|
||||
.get("/mcp/status", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "active", timestamp: Date.now() };
|
||||
})
|
||||
|
||||
// Health check
|
||||
.get("/health", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
||||
})
|
||||
.get("/mcp/init", async ({ set }) => {
|
||||
.get("/health", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
return { status: "ok", timestamp: Date.now(), tools: tools.length };
|
||||
})
|
||||
|
||||
const _tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
tools = _tools;
|
||||
return {
|
||||
success: true,
|
||||
message: "MCP initialized",
|
||||
tools: tools.length,
|
||||
};
|
||||
})
|
||||
// Force re-init (useful for admin / CI)
|
||||
.get("/mcp/init", async ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
try {
|
||||
tools = await getMcpTools(OPENAPI_URL, FILTER_TAG);
|
||||
return { success: true, message: "MCP initialized", tools: tools.length };
|
||||
} catch (err) {
|
||||
set.status = 500;
|
||||
return { success: false, message: "Failed to initialize tools", error: String(err) };
|
||||
}
|
||||
})
|
||||
|
||||
// CORS
|
||||
.options("/mcp", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] =
|
||||
"Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
})
|
||||
.options("/mcp/tools", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] =
|
||||
"Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
});
|
||||
/* CORS preflight */
|
||||
.options("/mcp", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,POST,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
})
|
||||
.options("/mcp/tools", ({ set }) => {
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
set.headers["Access-Control-Allow-Methods"] = "GET,OPTIONS";
|
||||
set.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization,X-API-Key";
|
||||
set.status = 204;
|
||||
return "";
|
||||
});
|
||||
|
||||
/* -------------------------
|
||||
End
|
||||
------------------------- */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,16 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
import { getLastUpdated } from "../lib/get-last-updated"
|
||||
import { mimeToExtension } from "../lib/mimetypeToExtension"
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
import { renameFile } from "../lib/rename-file"
|
||||
import { catFile, defaultConfigSF, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||
import Elysia, { t } from "elysia";
|
||||
import fs from 'fs';
|
||||
import type { StatusPengaduan } from "generated/prisma";
|
||||
import _ from "lodash";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { getLastUpdated } from "../lib/get-last-updated";
|
||||
import { mimeToExtension } from "../lib/mimetypeToExtension";
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan";
|
||||
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone";
|
||||
import { prisma } from "../lib/prisma";
|
||||
import { renameFile } from "../lib/rename-file";
|
||||
import { catFile, defaultConfigSF, downloadFile, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile";
|
||||
|
||||
const PengaduanRoute = new Elysia({
|
||||
prefix: "pengaduan",
|
||||
@@ -106,12 +109,14 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
|
||||
// --- PENGADUAN ---
|
||||
.post("/create", async ({ body }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||
.post("/create", async ({ body, headers }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId } = body
|
||||
const namaWarga = headers['x-user'] || ""
|
||||
const noTelepon = headers['x-phone'] || ""
|
||||
let imageFix = namaGambar
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
let idWargaFix = ""
|
||||
|
||||
if (idCategoryFix) {
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
@@ -127,56 +132,49 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
idCategoryFix = "lainnya"
|
||||
}
|
||||
|
||||
if (!isValidPhone(noTelepon)) {
|
||||
return { success: false, message: `nomor telepon ${noTelepon} tidak valid, harap masukkan nomor yang benar` }
|
||||
}
|
||||
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const dataWarga = await prisma.warga.upsert({
|
||||
where: {
|
||||
id: wargaId,
|
||||
phone: nomorHP
|
||||
},
|
||||
create: {
|
||||
name: namaWarga,
|
||||
phone: nomorHP,
|
||||
},
|
||||
update: {
|
||||
name: namaWarga,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
idWargaFix = dataWarga.id
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
idWargaFix = wargaCreate.id
|
||||
} else {
|
||||
idWargaFix = cariWarga.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title: judulPengaduan,
|
||||
detail: detailPengaduan,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
idWarga: idWargaFix || "",
|
||||
location: lokasi,
|
||||
image: imageFix,
|
||||
noPengaduan,
|
||||
@@ -218,50 +216,31 @@ const PengaduanRoute = new Elysia({
|
||||
description: "Alamat atau titik lokasi pengaduan"
|
||||
}),
|
||||
|
||||
namaGambar: t.String({
|
||||
optional: true,
|
||||
namaGambar: t.Optional(t.String({
|
||||
examples: ["sampah.jpg"],
|
||||
description: "Nama file gambar yang telah diupload (opsional)"
|
||||
}),
|
||||
})),
|
||||
|
||||
kategoriId: t.String({
|
||||
optional: true,
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
}),
|
||||
kategoriId: t.Optional(t.String({
|
||||
examples: ["kebersihan", "infrastruktur", "keamanan"],
|
||||
description: "Nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
})),
|
||||
|
||||
wargaId: t.String({
|
||||
optional: true,
|
||||
examples: ["budiman"],
|
||||
description: "ID unik warga yang melapor (jika sudah terdaftar)"
|
||||
}),
|
||||
// namaWarga: t.String({
|
||||
// examples: ["budiman"],
|
||||
// description: "Nama warga yang melapor"
|
||||
// }),
|
||||
|
||||
noTelepon: t.String({
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
// noTelepon: t.String({
|
||||
// error: "Nomor telepon harus diisi",
|
||||
// examples: ["08123456789", "+628123456789"],
|
||||
// description: "Nomor telepon warga pelapor"
|
||||
// }),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Buat Pengaduan Warga",
|
||||
description: `
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
|
||||
|
||||
Alur proses:
|
||||
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
|
||||
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
|
||||
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
|
||||
2. Sistem memvalidasi data warga berdasarkan ID.
|
||||
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
|
||||
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
|
||||
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
|
||||
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
|
||||
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
|
||||
|
||||
Respon:
|
||||
- success: true jika pengaduan berhasil dibuat.
|
||||
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
|
||||
description: `Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
@@ -316,18 +295,90 @@ Respon:
|
||||
description: `tool untuk update status pengaduan`
|
||||
}
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { noPengaduan, judul, detail, lokasi, namaGambar } = body
|
||||
let dataUpdate = {}
|
||||
|
||||
const cek = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!cek) {
|
||||
return { success: false, message: 'gagal update status pengaduan, nomer ' + noPengaduan + ' tidak ditemukan' }
|
||||
}
|
||||
|
||||
if (judul) {
|
||||
dataUpdate = { title: judul }
|
||||
}
|
||||
|
||||
if (detail) {
|
||||
dataUpdate = { ...dataUpdate, detail }
|
||||
}
|
||||
|
||||
if (lokasi) {
|
||||
dataUpdate = { ...dataUpdate, location: lokasi }
|
||||
}
|
||||
|
||||
if (namaGambar) {
|
||||
dataUpdate = { ...dataUpdate, image: namaGambar }
|
||||
}
|
||||
|
||||
const pengaduan = await prisma.pengaduan.updateMany({
|
||||
where: {
|
||||
noPengaduan
|
||||
},
|
||||
data: dataUpdate
|
||||
})
|
||||
|
||||
const keys = Object.keys(dataUpdate).join(", ");
|
||||
|
||||
await prisma.historyPengaduan.create({
|
||||
data: {
|
||||
idPengaduan: cek.id,
|
||||
deskripsi: `Pengaduan diupdate oleh warga (data yg diupdate: ${keys})`,
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'pengaduan dengan nomer ' + noPengaduan + ' sudah diupdate' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
noPengaduan: t.String({
|
||||
error: "nomer pengaduan harus diisi",
|
||||
description: "Nomer pengaduan yang ingin diupdate"
|
||||
}),
|
||||
judul: t.Optional(t.String({
|
||||
error: "judul harus diisi",
|
||||
description: "Judul pengaduan yang ingin diupdate"
|
||||
})),
|
||||
detail: t.Optional(t.String({
|
||||
description: "detail pengaduan yang ingin diupdate"
|
||||
})),
|
||||
lokasi: t.Optional(t.String({
|
||||
description: "lokasi pengaduan yang ingin diupdate"
|
||||
})),
|
||||
namaGambar: t.Optional(t.String({
|
||||
description: "Nama file gambar yang telah diupload untuk update data pengaduan"
|
||||
})),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Update Data Pengaduan",
|
||||
description: `tool untuk update data pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
const data = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
noPengaduan: id
|
||||
}, {
|
||||
id: id
|
||||
}
|
||||
]
|
||||
id: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -362,9 +413,19 @@ Respon:
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
const datafix = {
|
||||
pengaduan: {},
|
||||
history: [],
|
||||
warga: null,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}
|
||||
|
||||
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||
where: {
|
||||
idPengaduan: id,
|
||||
idPengaduan: data?.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -377,26 +438,19 @@ Respon:
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
deskripsi: item.deskripsi,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}),
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item: any) => ({
|
||||
..._.omit(item, ["User", "createdAt"]),
|
||||
nameUser: item.User?.name,
|
||||
createdAt: item.createdAt
|
||||
}))
|
||||
|
||||
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
@@ -426,50 +480,51 @@ Respon:
|
||||
}
|
||||
|
||||
return datafix
|
||||
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
|
||||
tags: ["mcp"]
|
||||
summary: "Detail Pengaduan Warga By ID",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id pengaduan`,
|
||||
}
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const { take, page, search, phone } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
.get("/", async ({ query, headers }) => {
|
||||
// const { take, page, search } = query
|
||||
const phone = headers['x-phone'] || ""
|
||||
// const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
// skip,
|
||||
// take: !take ? 10 : Number(take),
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengaduan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
],
|
||||
AND: {
|
||||
Warga: {
|
||||
phone: phone
|
||||
}
|
||||
}
|
||||
// OR: [
|
||||
// {
|
||||
// title: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// noPengaduan: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// detail: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// }
|
||||
// ],
|
||||
// AND: {
|
||||
// Warga: {
|
||||
// phone: phone
|
||||
// }
|
||||
// }
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -504,12 +559,11 @@ Respon:
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
page: t.String({ optional: true }),
|
||||
search: t.String({ optional: true }),
|
||||
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
|
||||
}),
|
||||
// query: t.Object({
|
||||
// take: t.String({ optional: true }),
|
||||
// page: t.String({ optional: true }),
|
||||
// search: t.String({ optional: true }),
|
||||
// }),
|
||||
detail: {
|
||||
summary: "List Pengaduan Warga By Phone",
|
||||
description: `tool untuk mendapatkan list pengaduan warga by phone`,
|
||||
@@ -517,7 +571,7 @@ Respon:
|
||||
}
|
||||
})
|
||||
.post("/upload", async ({ body }) => {
|
||||
const { file } = body;
|
||||
const { file, folder } = body;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
@@ -530,7 +584,7 @@ Respon:
|
||||
|
||||
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// const buffer = await file.arrayBuffer();
|
||||
const result = await uploadFile(defaultConfigSF, renamedFile);
|
||||
const result = await uploadFile(defaultConfigSF, renamedFile, folder);
|
||||
if (result == 'gagal') {
|
||||
return { success: false, message: "Upload gagal" };
|
||||
}
|
||||
@@ -544,19 +598,100 @@ Respon:
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.Any()
|
||||
file: t.Any(),
|
||||
folder: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File",
|
||||
description: "Tool untuk upload file ke Seafile",
|
||||
tags: ["mcp"],
|
||||
summary: "Upload File (FormData)",
|
||||
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
.get("/download", async ({ query, set }) => {
|
||||
const { file, folder } = query;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
// if (!folder) {
|
||||
// return { success: false, message: "Folder tidak ditemukan" };
|
||||
// }
|
||||
|
||||
const localPath = path.join("/tmp", file);
|
||||
|
||||
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// const buffer = await file.arrayBuffer();
|
||||
const result = await downloadFile(defaultConfigSF, file, 'surat', localPath);
|
||||
|
||||
if(result=="gagal") {
|
||||
return { success: false, message: "Download gagal" };
|
||||
}
|
||||
|
||||
set.headers["Content-Type"] = "application/pdf";
|
||||
set.headers["Content-Disposition"] = `attachment; filename="${file}"`;
|
||||
|
||||
// 🔹 kirim file ke browser
|
||||
return fs.createReadStream(localPath);
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.Any(),
|
||||
folder: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Download Surat",
|
||||
description: "Tool untuk download surat dari Seafile",
|
||||
},
|
||||
})
|
||||
.post("/upload-file-form-data", async ({ body }) => {
|
||||
const { file } = body;
|
||||
|
||||
// // Validasi file
|
||||
// if (!file) {
|
||||
// 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, renamedFile, 'pengaduan');
|
||||
// if (result == 'gagal') {
|
||||
// return { success: false, message: "Upload gagal" };
|
||||
// }
|
||||
|
||||
return {
|
||||
success: true,
|
||||
file: JSON.stringify(file),
|
||||
fileInfo: {
|
||||
name: file.name || 'kosong',
|
||||
size: file.size || 0,
|
||||
type: file.type || 'kosong'
|
||||
}
|
||||
// message: "Upload berhasil",
|
||||
// filename: renamedFile.name,
|
||||
// size: renamedFile.size,
|
||||
// seafileResult: result
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.Any(),
|
||||
// folder: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (FormData)",
|
||||
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
.post("/upload-base64", async ({ body }) => {
|
||||
const { data, mimetype } = body;
|
||||
const { data, mimetype, kategori } = body;
|
||||
const ext = mimeToExtension(mimetype)
|
||||
const name = `${uuidv4()}.${ext}`
|
||||
const kategoriFix = kategori === 'pengaduan' ? 'pengaduan' : 'syarat-dokumen';
|
||||
|
||||
// Validasi file
|
||||
if (!data) {
|
||||
@@ -568,7 +703,8 @@ Respon:
|
||||
// const base64String = Buffer.from(buffer).toString("base64");
|
||||
|
||||
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
|
||||
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||
// const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||
const result = await uploadFileToFolder(defaultConfigSF, { name: name, data: data }, kategoriFix);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -577,17 +713,18 @@ Respon:
|
||||
name,
|
||||
mimetype,
|
||||
ext,
|
||||
kategori,
|
||||
}
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
data: t.String(),
|
||||
mimetype: t.String()
|
||||
mimetype: t.String(),
|
||||
kategori: t.String()
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (Base64)",
|
||||
description: "Tool untuk upload file ke Seafile dalam format Base64",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
@@ -634,6 +771,10 @@ Respon:
|
||||
}
|
||||
}
|
||||
|
||||
const totalData = await prisma.pengaduan.count({
|
||||
where
|
||||
});
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
@@ -671,12 +812,20 @@ Respon:
|
||||
detail: item.detail,
|
||||
status: item.status,
|
||||
location: item.location,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
const dataReturn = {
|
||||
data: dataFix,
|
||||
total: totalData,
|
||||
page: Number(page) || 1,
|
||||
pageSize: !take ? 10 : Number(take),
|
||||
totalPages: Math.ceil(totalData / (!take ? 10 : Number(take)))
|
||||
}
|
||||
|
||||
return dataReturn
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
@@ -728,12 +877,14 @@ Respon:
|
||||
const hasil = await catFile(defaultConfigSF, folder, fileName);
|
||||
|
||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||
const mime =
|
||||
ext === "jpg" || ext === "jpeg"
|
||||
? "image/jpeg"
|
||||
: ext === "png"
|
||||
? "image/png"
|
||||
: "application/octet-stream";
|
||||
let mime = "application/octet-stream"; // default
|
||||
|
||||
if (["jpg", "jpeg"].includes(ext!)) mime = "image/jpeg";
|
||||
if (["png"].includes(ext!)) mime = "image/png";
|
||||
if (["gif"].includes(ext!)) mime = "image/gif";
|
||||
if (["webp"].includes(ext!)) mime = "image/webp";
|
||||
if (["svg"].includes(ext!)) mime = "image/svg+xml";
|
||||
if (["pdf"].includes(ext!)) mime = "application/pdf";
|
||||
|
||||
set.headers["Content-Type"] = mime;
|
||||
set.headers["Content-Length"] = hasil.byteLength.toString();
|
||||
@@ -749,8 +900,145 @@ Respon:
|
||||
description: "tool untuk mendapatkan gambar",
|
||||
}
|
||||
})
|
||||
.post("/delete-image", async ({ body }) => {
|
||||
const { file, folder } = body;
|
||||
|
||||
// Validasi file
|
||||
if (!file) {
|
||||
return { success: false, message: "File tidak ditemukan" };
|
||||
}
|
||||
|
||||
const result = await removeFile(defaultConfigSF, file, folder);
|
||||
if (result == 'gagal') {
|
||||
return { success: false, message: "Delete gagal" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Delete berhasil",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.String(),
|
||||
folder: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Delete File",
|
||||
description: "Tool untuk delete file Seafile",
|
||||
},
|
||||
})
|
||||
.post("/detail-data", async ({ body }) => {
|
||||
const { nomerPengaduan } = body
|
||||
|
||||
const data = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
noPengaduan: nomerPengaduan
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengaduan: true,
|
||||
title: true,
|
||||
detail: true,
|
||||
location: true,
|
||||
image: true,
|
||||
idCategory: true,
|
||||
idWarga: true,
|
||||
status: true,
|
||||
keterangan: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPengaduan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
_count: {
|
||||
select: {
|
||||
Pengaduan: true,
|
||||
PelayananAjuan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
return { success: false, message: "Data tidak ditemukan" };
|
||||
}
|
||||
|
||||
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||
where: {
|
||||
idPengaduan: data?.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
deskripsi: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item: any) => ({
|
||||
..._.omit(item, ["User", "createdAt"]),
|
||||
nameUser: item.User?.name,
|
||||
createdAt: item.createdAt
|
||||
}))
|
||||
|
||||
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
phone: data?.Warga?.phone,
|
||||
pengaduan: data?.Warga?._count.Pengaduan,
|
||||
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||
}
|
||||
|
||||
const dataPengaduan = {
|
||||
id: data?.id,
|
||||
noPengaduan: data?.noPengaduan,
|
||||
title: data?.title,
|
||||
detail: data?.detail,
|
||||
location: data?.location,
|
||||
image: data?.image,
|
||||
category: data?.CategoryPengaduan.name,
|
||||
status: data?.status,
|
||||
keterangan: data?.keterangan,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
pengaduan: dataPengaduan,
|
||||
history: dataHistoryFix,
|
||||
warga: warga,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}, {
|
||||
body: t.Object({
|
||||
nomerPengaduan: t.String({
|
||||
description: "Nomer pengaduan yg ingin diakses",
|
||||
examples: ["PGD-101225-001", "PGD-101225-002"],
|
||||
error: "Nomer pengaduan harus diisi",
|
||||
}),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga By Nomor Pengaduan",
|
||||
description: `tool untuk mendapatkan detail data pengaduan berdasarkan nomor pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default PengaduanRoute
|
||||
|
||||
153
src/server/routes/send_wa_route.ts
Normal file
153
src/server/routes/send_wa_route.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
|
||||
const SendWaRoute = new Elysia({
|
||||
prefix: "send-wa",
|
||||
tags: ["send-wa"],
|
||||
})
|
||||
|
||||
// --- KATEGORI PENGADUAN ---
|
||||
.post("/pengaduan", async ({ body }) => {
|
||||
const { noPengaduan, judulPengaduan, status, alasan, tlp } = body
|
||||
|
||||
let text = ""
|
||||
|
||||
if (status === "ditolak") {
|
||||
text = `Pemberitahuan Aduan
|
||||
|
||||
Aduan dengan Nomor Pengaduan: ${noPengaduan}
|
||||
Judul Pengaduan: ${judulPengaduan}
|
||||
Kami informasikan bahwa aduan tersebut tidak dapat ditindaklanjuti (ditolak).
|
||||
Alasan penolakan:${alasan}
|
||||
|
||||
Terima kasih atas pengertian Bapak/Ibu.`
|
||||
} else if (status == "diterima") {
|
||||
text = `Pemberitahuan Aduan
|
||||
|
||||
Aduan dengan Nomor Pengaduan: ${noPengaduan}
|
||||
Judul Pengaduan: ${judulPengaduan}
|
||||
Telah kami terima dan akan segera diproses sesuai ketentuan yang berlaku.
|
||||
|
||||
Terima kasih atas laporan Bapak/Ibu.`
|
||||
} else if (status == "dikerjakan") {
|
||||
text = `Pemberitahuan Aduan
|
||||
|
||||
Aduan dengan Nomor Pengaduan: ${noPengaduan}
|
||||
Judul Pengaduan: ${judulPengaduan}
|
||||
Saat ini sedang dalam proses penanganan oleh petugas terkait.
|
||||
|
||||
Mohon menunggu informasi selanjutnya.`
|
||||
} else if (status == "selesai") {
|
||||
text = `Pemberitahuan Aduan
|
||||
|
||||
Aduan dengan Nomor Pengaduan: ${noPengaduan}
|
||||
Judul Pengaduan: ${judulPengaduan}
|
||||
Telah selesai ditindaklanjuti.
|
||||
|
||||
Terima kasih atas partisipasi dan kepercayaan Bapak/Ibu.`
|
||||
}
|
||||
|
||||
const textFix = encodeURIComponent(text)
|
||||
|
||||
const res = await fetch(
|
||||
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
|
||||
{
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status !== 200)
|
||||
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
|
||||
|
||||
|
||||
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
noPengaduan: t.String({ minLength: 1, error: "nomer pengaduan harus diisi" }),
|
||||
judulPengaduan: t.String({ minLength: 1, error: "judul pengaduan harus diisi" }),
|
||||
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||
alasan: t.String({ optional: true }),
|
||||
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Send pemberitahuan pengaduan lewat WA",
|
||||
description: `tool untuk send pemberitahuan pengaduan lewat WA`
|
||||
}
|
||||
})
|
||||
.post("/pengajuan-surat", async ({ body }) => {
|
||||
const { noPengajuan, jenisSurat, status, alasan, tlp, linkSurat, linkUpdate } = body
|
||||
|
||||
let text = ""
|
||||
|
||||
if (status === "ditolak") {
|
||||
text = `Pemberitahuan Pengajuan Surat
|
||||
|
||||
Nomor Pengajuan: ${noPengajuan}
|
||||
Surat: ${jenisSurat}
|
||||
Kami informasikan bahwa pengajuan surat tersebut tidak dapat diproses (ditolak).
|
||||
Alasan penolakan: ${alasan}
|
||||
|
||||
Bapak/Ibu dapat melakukan perbaikan atau pembaruan data melalui tautan berikut:
|
||||
👉 ${linkUpdate}
|
||||
Setelah data diperbarui, pengajuan akan diproses kembali sesuai ketentuan yang berlaku.
|
||||
|
||||
Terima kasih atas pengertian Bapak/Ibu.`
|
||||
} else if (status == "diterima") {
|
||||
text = `Pemberitahuan Pengajuan Surat
|
||||
|
||||
Nomor Pengajuan: ${noPengajuan}
|
||||
Surat: ${jenisSurat}
|
||||
Kami informasikan bahwa pengajuan surat yang Bapak/Ibu ajukan telah kami terima dan sedang menunggu proses verifikasi serta penanganan lebih lanjut.
|
||||
|
||||
Terima kasih atas kesabaran Bapak/Ibu.`
|
||||
} else if (status == "selesai") {
|
||||
text = `Pemberitahuan Pengajuan Surat
|
||||
|
||||
Nomor Pengajuan: ${noPengajuan}
|
||||
Surat: ${jenisSurat}
|
||||
Kami informasikan bahwa pengajuan surat tersebut telah selesai diproses.
|
||||
|
||||
Bapak/Ibu dapat mengunduh surat melalui tautan berikut:
|
||||
👉 ${linkSurat}
|
||||
|
||||
Terima kasih atas kepercayaan Bapak/Ibu.`
|
||||
}
|
||||
|
||||
const textFix = encodeURIComponent(text)
|
||||
|
||||
|
||||
const res = await fetch(
|
||||
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
|
||||
{
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (res.status !== 200)
|
||||
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
|
||||
|
||||
|
||||
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
noPengajuan: t.String({ minLength: 1, error: "nomer pengajuan harus diisi" }),
|
||||
jenisSurat: t.String({ minLength: 1, error: "jenis surat harus diisi" }),
|
||||
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||
alasan: t.String({ optional: true }),
|
||||
linkSurat: t.String({ optional: true }),
|
||||
linkUpdate: t.String({ optional: true }),
|
||||
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Send pemberitahuan pengajuan surat lewat WA",
|
||||
description: `tool untuk send pemberitahuan pengajuan surat lewat WA`
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default SendWaRoute
|
||||
@@ -17,10 +17,16 @@ const SuratRoute = new Elysia({
|
||||
noSurat: true,
|
||||
idCategory: true,
|
||||
createdAt: true,
|
||||
file: true,
|
||||
PelayananAjuan: {
|
||||
select: {
|
||||
DataTextPelayanan: true,
|
||||
}
|
||||
},
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -37,7 +43,9 @@ const SuratRoute = new Elysia({
|
||||
surat: {
|
||||
id: dataSurat?.id,
|
||||
idCategory: dataSurat?.idCategory,
|
||||
nameCategory: dataSurat?.CategoryPelayanan?.name,
|
||||
noSurat: dataSurat?.noSurat,
|
||||
file: dataSurat?.file,
|
||||
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
|
||||
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
},
|
||||
@@ -54,6 +62,33 @@ const SuratRoute = new Elysia({
|
||||
}
|
||||
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { id, filename } = body
|
||||
|
||||
await prisma.suratPelayanan.update({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
file: filename,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'surat sudah diperbarui',
|
||||
link: `${process.env.BUN_PUBLIC_BASE_URL}/api/pengaduan/download?file=${filename}`
|
||||
}
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
filename: t.String({ minLength: 1, error: "filename harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "update file surat",
|
||||
description: `tool untuk update file surat`
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default SuratRoute
|
||||
|
||||
@@ -4,7 +4,8 @@ import { generateNoPengaduan } from "../lib/no-pengaduan";
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone";
|
||||
|
||||
const TestPengaduanRoute = new Elysia({
|
||||
prefix: "online-pengaduan"
|
||||
prefix: "online-pengaduan",
|
||||
tags: ["test"]
|
||||
})
|
||||
.get("/category", async () => {
|
||||
const data = await prisma.categoryPengaduan.findMany({
|
||||
@@ -20,7 +21,6 @@ const TestPengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return { data }
|
||||
}, {
|
||||
detail: {
|
||||
@@ -31,71 +31,61 @@ const TestPengaduanRoute = new Elysia({
|
||||
})
|
||||
|
||||
.post("/create", async ({ body }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, wargaId, noTelepon } = body
|
||||
let imageFix = namaGambar
|
||||
const { judulPengaduan, detailPengaduan, lokasi, kategoriId, noTelepon, image } = body
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
id: kategoriId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
if (idCategoryFix) {
|
||||
const category = await prisma.categoryPengaduan.findUnique({
|
||||
where: {
|
||||
name: kategoriId,
|
||||
id: idCategoryFix,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id: wargaId,
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
if (!category) {
|
||||
const cariCategory = await prisma.categoryPengaduan.findFirst({
|
||||
where: {
|
||||
name: kategoriId,
|
||||
}
|
||||
})
|
||||
idWargaFix = wargaCreate.id
|
||||
} else {
|
||||
idWargaFix = cariWarga.id
|
||||
}
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
idCategoryFix = "lainnya"
|
||||
}
|
||||
|
||||
|
||||
|
||||
const nomorHP = normalizePhoneNumber({ phone: "089697338821" })
|
||||
const cariWarga = await prisma.warga.upsert({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
},
|
||||
create: {
|
||||
name: "malik",
|
||||
phone: nomorHP,
|
||||
},
|
||||
update: {
|
||||
name: "malik",
|
||||
phone: nomorHP,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
const pengaduan = await prisma.pengaduan.create({
|
||||
data: {
|
||||
title: judulPengaduan,
|
||||
detail: detailPengaduan,
|
||||
idCategory: idCategoryFix,
|
||||
idWarga: idWargaFix,
|
||||
idWarga: cariWarga.id,
|
||||
location: lokasi,
|
||||
image: imageFix,
|
||||
image: body.image || "",
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
@@ -117,69 +107,18 @@ const TestPengaduanRoute = new Elysia({
|
||||
return { success: true, message: 'pengaduan sudah dibuat dengan nomer ' + noPengaduan + ', nomer ini akan digunakan untuk mengakses pengaduan ini' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
judulPengaduan: t.String({
|
||||
error: "Judul pengaduan harus diisi dan minimal 3 karakter",
|
||||
examples: ["Sampah menumpuk di depan rumah"],
|
||||
description: "Judul singkat dari pengaduan warga"
|
||||
}),
|
||||
|
||||
detailPengaduan: t.String({
|
||||
error: "Deskripsi pengaduan harus diisi dan minimal 10 karakter",
|
||||
examples: ["Terdapat sampah yang menumpuk selama seminggu di depan rumah saya"],
|
||||
description: "Penjelasan lebih detail mengenai pengaduan"
|
||||
}),
|
||||
|
||||
lokasi: t.String({
|
||||
error: "Lokasi pengaduan harus diisi",
|
||||
examples: ["Jl. Raya No. 1, RT 01 RW 02, Darmasaba"],
|
||||
description: "Alamat atau titik lokasi pengaduan"
|
||||
}),
|
||||
|
||||
namaGambar: t.String({
|
||||
optional: true,
|
||||
examples: ["sampah.jpg"],
|
||||
description: "Nama file gambar yang telah diupload (opsional)"
|
||||
}),
|
||||
|
||||
kategoriId: t.String({
|
||||
error: "ID kategori pengaduan harus diisi",
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
}),
|
||||
|
||||
wargaId: t.String({
|
||||
error: "ID warga harus diisi",
|
||||
examples: ["budiman"],
|
||||
description: "ID unik warga yang melapor (jika sudah terdaftar)"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
judulPengaduan: t.String(),
|
||||
detailPengaduan: t.String(),
|
||||
lokasi: t.String(),
|
||||
kategoriId: t.String(),
|
||||
noTelepon: t.Optional(t.String()),
|
||||
image: t.Optional(t.String()),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Buat Pengaduan Warga",
|
||||
description: `
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.
|
||||
|
||||
Alur proses:
|
||||
1. Sistem memvalidasi kategori pengaduan berdasarkan ID.
|
||||
- Jika ID kategori tidak ditemukan, sistem akan mencari berdasarkan nama kategori.
|
||||
- Jika tetap tidak ditemukan, kategori akan diset menjadi "lainnya".
|
||||
2. Sistem memvalidasi data warga berdasarkan ID.
|
||||
- Jika warga tidak ditemukan, sistem akan mencari berdasarkan nomor telepon.
|
||||
- Jika tetap tidak ditemukan, data warga baru akan dibuat secara otomatis.
|
||||
3. Sistem menghasilkan nomor pengaduan unik (noPengaduan).
|
||||
4. Data pengaduan akan disimpan ke database, termasuk judul, detail, lokasi, gambar (opsional), dan data warga.
|
||||
5. Sistem juga membuat catatan riwayat awal pengaduan dengan deskripsi "Pengaduan dibuat".
|
||||
|
||||
Respon:
|
||||
- success: true jika pengaduan berhasil dibuat.
|
||||
- message: berisi pesan sukses dan nomor pengaduan yang dapat digunakan untuk melacak status pengaduan.`,
|
||||
tags: ["test"]
|
||||
Endpoint ini digunakan untuk membuat data pengaduan (laporan) baru dari warga.`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import Elysia, { t } from "elysia";
|
||||
import type { User } from "generated/prisma";
|
||||
import _ from "lodash";
|
||||
import { prisma } from "../lib/prisma";
|
||||
|
||||
const UserRoute = new Elysia({
|
||||
prefix: "user",
|
||||
tags: ["user"],
|
||||
})
|
||||
.get('/find', (ctx) => {
|
||||
.get('/find', async (ctx) => {
|
||||
const { user } = ctx as any
|
||||
const permissions = await prisma.role.findFirst({
|
||||
where: { id: user?.roleId },
|
||||
select: { permissions: true }
|
||||
});
|
||||
|
||||
return {
|
||||
user: user as User
|
||||
user: user as User,
|
||||
permissions: permissions?.permissions || []
|
||||
}
|
||||
}, {
|
||||
detail: {
|
||||
@@ -139,10 +146,31 @@ const UserRoute = new Elysia({
|
||||
NOT: {
|
||||
id: user.id
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
phone: true,
|
||||
email: true,
|
||||
roleId: true,
|
||||
Role: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
const dataFix = data.map((item: any) => ({
|
||||
..._.omit(item, ["Role"]),
|
||||
nameRole: item.Role?.name,
|
||||
name: String(item.name),
|
||||
phone: String(item.phone),
|
||||
email: String(item.email),
|
||||
roleId: String(item.roleId),
|
||||
}))
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "list",
|
||||
@@ -150,7 +178,14 @@ const UserRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
.get("/role", async () => {
|
||||
const data = await prisma.role.findMany()
|
||||
const data = await prisma.role.findMany({
|
||||
where: {
|
||||
isActive: true
|
||||
},
|
||||
orderBy: {
|
||||
name: "asc"
|
||||
}
|
||||
})
|
||||
return data
|
||||
}, {
|
||||
detail: {
|
||||
@@ -182,5 +217,80 @@ const UserRoute = new Elysia({
|
||||
description: "delete user",
|
||||
}
|
||||
})
|
||||
.post("role-create", async ({ body }) => {
|
||||
const { name, permissions } = body;
|
||||
const create = await prisma.role.create({
|
||||
data: {
|
||||
name,
|
||||
permissions: permissions
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Role created successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
permissions: t.Any(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "create-role",
|
||||
description: "create role",
|
||||
}
|
||||
})
|
||||
.post("/role-update", async ({ body }) => {
|
||||
const { id, name, permissions } = body;
|
||||
const update = await prisma.role.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
permissions
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User role updated successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" }),
|
||||
name: t.String({ minLength: 1, error: "name is required" }),
|
||||
permissions: t.Any()
|
||||
}),
|
||||
detail: {
|
||||
summary: "update-role",
|
||||
description: "update role",
|
||||
}
|
||||
})
|
||||
.post("role-delete", async ({ body }) => {
|
||||
const { id } = body;
|
||||
await prisma.role.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
isActive: false
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Role deleted successfully",
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id is required" })
|
||||
}),
|
||||
detail: {
|
||||
summary: "delete-role",
|
||||
description: "delete role",
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default UserRoute
|
||||
@@ -9,9 +9,31 @@ const WargaRoute = new Elysia({
|
||||
})
|
||||
|
||||
.get("/list", async ({ query }) => {
|
||||
const { search } = query
|
||||
const { search, page = 1 } = query
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * 10 - 10;
|
||||
|
||||
const totalData = await prisma.warga.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
},
|
||||
{
|
||||
phone: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const data = await prisma.warga.findMany({
|
||||
skip: dataSkip,
|
||||
take: 10,
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
@@ -33,7 +55,15 @@ const WargaRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
const dataFix = {
|
||||
data,
|
||||
total: totalData,
|
||||
page: Number(page) || 1,
|
||||
pageSize: 10,
|
||||
totalPages: Math.ceil(totalData / 10)
|
||||
};
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Warga",
|
||||
@@ -62,73 +92,142 @@ const WargaRoute = new Elysia({
|
||||
phone: t.String({ minLength: 1 })
|
||||
}),
|
||||
detail: {
|
||||
summary: "edit konfigurasi desa",
|
||||
description: `tool untuk edit konfigurasi desa`
|
||||
summary: "Edit Warga",
|
||||
description: `tool untuk edit warga`
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const { id, category, search, page } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * 5
|
||||
|
||||
const dataWarga = await prisma.warga.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
|
||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where: {
|
||||
if (!dataWarga)
|
||||
return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
|
||||
|
||||
if (category == "warga") {
|
||||
return dataWarga
|
||||
} else if (category == "pengaduan") {
|
||||
const where: any = {
|
||||
isActive: true,
|
||||
idWarga: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
noPengaduan: true,
|
||||
title: true
|
||||
idWarga: id,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengaduan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const totalData = await prisma.pengaduan.count({
|
||||
where
|
||||
});
|
||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
noPengaduan: true,
|
||||
title: true
|
||||
}
|
||||
})
|
||||
|
||||
const dataReturn = {
|
||||
success: true,
|
||||
message: "data pengaduan berhasil diambil",
|
||||
data: dataPengaduan,
|
||||
totalRows: totalData,
|
||||
totalPages: Math.ceil(totalData / 5)
|
||||
}
|
||||
|
||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where: {
|
||||
return dataReturn
|
||||
} else if (category == "pelayanan") {
|
||||
const where: any = {
|
||||
isActive: true,
|
||||
idWarga: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true
|
||||
idWarga: id,
|
||||
OR: [
|
||||
{
|
||||
CategoryPelayanan: {
|
||||
name: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengajuan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const totalData = await prisma.pelayananAjuan.count({
|
||||
where
|
||||
});
|
||||
|
||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||
skip,
|
||||
take: 5,
|
||||
orderBy: {
|
||||
createdAt: "desc"
|
||||
},
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayanFix = dataPelayanan.map((v: any) => ({
|
||||
..._.omit(v, ["CategoryPelayanan"]),
|
||||
id: v.id,
|
||||
noPengaduan: v.noPengajuan,
|
||||
status: v.status,
|
||||
category: v.CategoryPelayanan.name
|
||||
}))
|
||||
|
||||
const dataReturn = {
|
||||
success: true,
|
||||
message: "data pelayanan berhasil diambil",
|
||||
data: dataPelayanFix,
|
||||
totalRows: totalData,
|
||||
totalPages: Math.ceil(totalData / 5)
|
||||
}
|
||||
})
|
||||
|
||||
const dataPelayanFix = dataPelayanan.map((v: any) => ({
|
||||
..._.omit(v, ["CategoryPelayanan"]),
|
||||
id: v.id,
|
||||
noPengaduan: v.noPengajuan,
|
||||
status: v.status,
|
||||
category: v.CategoryPelayanan.name
|
||||
}))
|
||||
|
||||
return {
|
||||
warga: dataWarga,
|
||||
pengaduan: dataPengaduan,
|
||||
pelayanan: dataPelayanFix
|
||||
return dataReturn
|
||||
}
|
||||
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" })
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
category: t.String({ minLength: 1, error: "kategori harus diisi" }),
|
||||
page: t.String({ optional: true }),
|
||||
search: t.String({ optional: true }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Warga",
|
||||
|
||||
Reference in New Issue
Block a user