Compare commits
37 Commits
amalia/05-
...
amalia/20-
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -187,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)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -207,7 +207,7 @@ 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>
|
||||
|
||||
@@ -4,19 +4,17 @@ import {
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
List,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
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";
|
||||
@@ -625,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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Flex, Loader, Modal, Text } from "@mantine/core";
|
||||
import { Flex, Modal, Progress, Stack, Text } from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import html2canvas from "html2canvas";
|
||||
import jsPDF from "jspdf";
|
||||
@@ -23,7 +23,7 @@ export default function ModalSurat({
|
||||
surat,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClose: (val: { success: boolean, data: string }) => void;
|
||||
surat: string;
|
||||
}) {
|
||||
const A4Style = {
|
||||
@@ -35,7 +35,7 @@ export default function ModalSurat({
|
||||
fontSize: "14px",
|
||||
fontFamily: "Times New Roman",
|
||||
};
|
||||
const [uploading, setUploading] = useState<"Menyiapkan" | "Mengupload" | "Selesai">("Menyiapkan")
|
||||
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({
|
||||
@@ -45,75 +45,97 @@ export default function ModalSurat({
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
const uploadPdf = async () => {
|
||||
try {
|
||||
setUploading("Mengupload");
|
||||
const element = hiddenRef.current;
|
||||
const canvas = await html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
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);
|
||||
|
||||
// ⬇️ ambil sebagai Blob
|
||||
const pdfBlob = pdf.output("blob");
|
||||
// ⬇️ ambil sebagai Blob
|
||||
const pdfBlob = pdf.output("blob");
|
||||
|
||||
const pdfFile = new File(
|
||||
[pdfBlob],
|
||||
`${data?.data?.surat?.nameCategory}.pdf`,
|
||||
{
|
||||
type: "application/pdf",
|
||||
lastModified: Date.now(),
|
||||
const pdfFile = new File(
|
||||
[pdfBlob],
|
||||
`${data?.data?.surat?.nameCategory}.pdf`,
|
||||
{
|
||||
type: "application/pdf",
|
||||
lastModified: Date.now(),
|
||||
}
|
||||
);
|
||||
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: pdfFile,
|
||||
folder: "surat",
|
||||
});
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: pdfFile,
|
||||
folder: "surat",
|
||||
});
|
||||
|
||||
console.log(resImg.data)
|
||||
} catch (error) {
|
||||
console.error("Error uploading PDF:", error);
|
||||
} finally {
|
||||
setUploading("Selesai");
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
setTimeout(() => {
|
||||
uploadPdf();
|
||||
}, 5000);
|
||||
}, [surat]);
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
uploadPdf();
|
||||
}, 3000);
|
||||
}
|
||||
}, [surat, open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => onClose()}
|
||||
onClose={() => { }}
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
size="auto"
|
||||
withCloseButton={false}
|
||||
closeOnClickOutside={false}
|
||||
removeScrollProps={{ allowPinchZoom: true }}
|
||||
styles={{
|
||||
header: {
|
||||
@@ -127,14 +149,24 @@ export default function ModalSurat({
|
||||
},
|
||||
}}
|
||||
title={
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
|
||||
<>
|
||||
<Flex justify="space-between" align="center" w="100%">
|
||||
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
|
||||
|
||||
<Flex gap={8} align="center">
|
||||
{/* <Flex gap={8} align="center">
|
||||
<Loader color="blue" size="xs" />
|
||||
<Text size="sm">{uploading}</Text>
|
||||
<Text size="sm">{uploading.text}</Text>
|
||||
</Flex> */}
|
||||
</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}>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function SKKelahiran({ data }: { data: any }) {
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
@@ -161,7 +161,7 @@ export default function SKKelahiran({ data }: { data: any }) {
|
||||
<tr>
|
||||
<td>Tempat & Tanggal Lahir</td>
|
||||
<td>:</td>
|
||||
<td>{`${getValue("tempat_lahir_ayah")}, ${"tanggal_lahir_ayah"}`}</td>
|
||||
<td>{`${getValue("tempat_lahir_ayah")}, ${getValue("tanggal_lahir_ayah")}`}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pekerjaan</td>
|
||||
|
||||
@@ -8,472 +8,377 @@ export const categoryPelayananSurat = [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing"
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "dokumen_beda",
|
||||
name: "Dokumen Pendukung",
|
||||
desc: "Fotokopi dokumen yang terdapat perbedaan biodata (ijazah, sertifikat, dll)"
|
||||
desc: "Fotokopi dokumen yang terdapat perbedaan biodata (ijazah, sertifikat, dll)",
|
||||
required: true
|
||||
}
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text" },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text", required: true },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true },
|
||||
{
|
||||
key: "dokumen",
|
||||
name: "Nama Dokumen",
|
||||
desc: "Jenis dokumen yang mengalami perbedaan biodata (cth : ijazah, sertifikat, dll)",
|
||||
type: "text"
|
||||
desc: "Jenis dokumen yang mengalami perbedaan biodata",
|
||||
type: "text",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "data_dokumen",
|
||||
name: "Data Dokumen",
|
||||
desc: "Data dokumen yg berbeda (cth : nama, tempat lahir, tanggal lahir, jenis kelamin, alamat, pekerjaan)",
|
||||
type: "text"
|
||||
desc: "Data dokumen yg berbeda",
|
||||
type: "text",
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "dokumen_a",
|
||||
name: "Data pada Dokumen A",
|
||||
desc: "Data biodata yang tertulis pada dokumen pertama",
|
||||
type: "text"
|
||||
},
|
||||
{
|
||||
key: "dokumen_b",
|
||||
name: "Data pada Dokumen B",
|
||||
desc: "Data biodata yang tertulis pada dokumen kedua",
|
||||
type: "text"
|
||||
}
|
||||
{ key: "dokumen_a", name: "Data pada Dokumen A", desc: "Data biodata pada dokumen pertama", type: "text", required: true },
|
||||
{ key: "dokumen_b", name: "Data pada Dokumen B", desc: "Data biodata pada dokumen kedua", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skbelumkawin",
|
||||
name: "Surat Keterangan Belum Kawin",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
},
|
||||
{
|
||||
key: "akta_cerai",
|
||||
name: "Akta Cerai",
|
||||
desc: "Fotokopi akta cerai (jika berstatus janda/duda)"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau Kartu Keluarga", required: true },
|
||||
{ key: "akta_cerai", name: "Akta Cerai", desc: "Fotokopi akta cerai (jika berstatus janda/duda)", required: false }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true },
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama pemohon",
|
||||
type: "enum",
|
||||
options: enumAgama
|
||||
options: enumAgama,
|
||||
required: true
|
||||
},
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" }
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
id: "skdomisiliorganisasi",
|
||||
name: "Surat Keterangan Domisili Organisasi",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "skt_organisasi",
|
||||
name: "SKT Organisasi",
|
||||
desc: "Fotokopi SKT Organisasi atau pengukuhan kelompok"
|
||||
},
|
||||
{
|
||||
key: "susunan_pengurus",
|
||||
name: "Susunan Pengurus",
|
||||
desc: "Susunan pengurus lengkap dengan kop organisasi"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "skt_organisasi", name: "SKT Organisasi", desc: "Fotokopi SKT Organisasi", required: true },
|
||||
{ key: "susunan_pengurus", name: "Susunan Pengurus", desc: "Susunan pengurus organisasi", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text" },
|
||||
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis atau bentuk organisasi", type: "text" },
|
||||
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat organisasi", type: "text" },
|
||||
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text" },
|
||||
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan organisasi", type: "text" },
|
||||
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text" }
|
||||
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text", required: true },
|
||||
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis organisasi", type: "text", required: true },
|
||||
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat", type: "text", required: true },
|
||||
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text", required: true },
|
||||
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan", type: "text", required: true },
|
||||
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skkelahiran",
|
||||
name: "Surat Keterangan Kelahiran",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "surat_lahir",
|
||||
name: "Surat Keterangan Lahir",
|
||||
desc: "Surat keterangan lahir dari bidan/dokter (jika ada)"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "surat_lahir", name: "Surat Keterangan Lahir", desc: "Surat keterangan lahir dari bidan/dokter (jika ada)", required: false }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text" },
|
||||
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" },
|
||||
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran anak", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran anak", type: "text" },
|
||||
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text", required: true },
|
||||
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date", required: true },
|
||||
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran", type: "text", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin Anak",
|
||||
desc: "Jenis kelamin anak",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran anak", type: "number" },
|
||||
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number" },
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama lengkap ibu", type: "text" },
|
||||
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu kandung", type: "text" },
|
||||
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu kandung", type: "date" },
|
||||
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu kandung", type: "text" },
|
||||
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu kandung", type: "text" },
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama lengkap ayah", type: "text" },
|
||||
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number" },
|
||||
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah kandung", type: "text" },
|
||||
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah kandung", type: "date" },
|
||||
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah kandung", type: "text" },
|
||||
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah kandung", type: "text" },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pihak yang melaporkan", type: "text" },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan pelapor dengan anak", type: "text" },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text" }
|
||||
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran", type: "number", required: true },
|
||||
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number", required: true },
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text", required: true },
|
||||
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu", type: "text", required: true },
|
||||
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu", type: "date", required: true },
|
||||
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu", type: "text", required: true },
|
||||
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu", type: "text", required: true },
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text", required: true },
|
||||
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number", required: true },
|
||||
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah", type: "text", required: true },
|
||||
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah", type: "date", required: true },
|
||||
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah", type: "text", required: true },
|
||||
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah", type: "text", required: true },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan anak", type: "text", required: true },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true }
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
id: "skkelakuanbaik",
|
||||
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
desc: "Jenis kelamin",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama pemohon",
|
||||
desc: "Agama",
|
||||
type: "enum",
|
||||
options: enumAgama
|
||||
options: enumAgama,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" },
|
||||
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan pembuatan SKCK", type: "text" }
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true },
|
||||
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan", type: "text", required: true }
|
||||
]
|
||||
|
||||
},
|
||||
{
|
||||
id: "skkematian",
|
||||
name: "Surat Keterangan Kematian",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
},
|
||||
{
|
||||
key: "surat_kematian",
|
||||
name: "Surat Keterangan 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 },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true },
|
||||
{ key: "surat_kematian", name: "Surat Keterangan Kematian", desc: "Surat keterangan kematian dari rumah sakit/dokter (jika ada)", required: false }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "Nomor Induk Kependudukan pelapor", type: "number" },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama lengkap pelapor", type: "text" },
|
||||
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text" },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat tempat tinggal pelapor", type: "text" },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan dengan Almarhum", desc: "Hubungan pelapor dengan almarhum", type: "text" },
|
||||
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama lengkap almarhum", type: "text" },
|
||||
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "Nomor Induk Kependudukan almarhum", type: "number" },
|
||||
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir Almarhum", desc: "Tempat lahir almarhum", type: "text" },
|
||||
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir Almarhum", desc: "Tanggal lahir almarhum", type: "date" },
|
||||
{ key: "alamat_almarhum", name: "Alamat Almarhum", desc: "Alamat terakhir almarhum", type: "text" },
|
||||
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "NIK pelapor", type: "number", required: true },
|
||||
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true },
|
||||
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text", required: true },
|
||||
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true },
|
||||
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan almarhum", type: "text", required: true },
|
||||
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama almarhum", type: "text", required: true },
|
||||
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "NIK almarhum", type: "number", required: true },
|
||||
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir", desc: "Tempat lahir almarhum", type: "text", required: true },
|
||||
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir", desc: "Tanggal lahir almarhum", type: "date", required: true },
|
||||
{ key: "alamat_almarhum", name: "Alamat", desc: "Alamat terakhir", type: "text", required: true },
|
||||
{
|
||||
key: "agama_almarhum",
|
||||
name: "Agama Almarhum",
|
||||
desc: "Agama almarhum",
|
||||
type: "enum",
|
||||
options: enumAgama
|
||||
options: enumAgama,
|
||||
required: true
|
||||
},
|
||||
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date" },
|
||||
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text" },
|
||||
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text" },
|
||||
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text" }
|
||||
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date", required: true },
|
||||
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text", required: true },
|
||||
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text", required: true },
|
||||
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skpenghasilan",
|
||||
name: "Surat Keterangan Penghasilan",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_ortu_kk",
|
||||
name: "KTP Orang Tua / KK",
|
||||
desc: "Fotokopi KTP orang tua atau Kartu Keluarga"
|
||||
},
|
||||
{
|
||||
key: "surat_pernyataan",
|
||||
name: "Surat Pernyataan",
|
||||
desc: "Surat pernyataan penghasilan bermaterai"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "ktp_ortu_kk", name: "KTP Orang Tua / KK", desc: "Fotokopi KTP orang tua/KK", required: true },
|
||||
{ key: "surat_pernyataan", name: "Surat Pernyataan Penghasilan", desc: "Surat pernyataan penghasilan bermaterai", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemohon",
|
||||
desc: "Jenis kelamin",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon/orang tua", type: "text" },
|
||||
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number" },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text" }
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true },
|
||||
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number", required: true },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sktempatusaha",
|
||||
name: "Surat Keterangan Tempat Usaha",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
},
|
||||
{
|
||||
key: "foto_lokasi",
|
||||
name: "Foto Lokasi Usaha",
|
||||
desc: "Foto lokasi usaha dicetak dan distempel oleh Kelian"
|
||||
},
|
||||
{
|
||||
key: "dokumen_lahan",
|
||||
name: "Dokumen Lahan",
|
||||
desc: "SPPT, Sertifikat, atau surat sewa tempat usaha"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true },
|
||||
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true },
|
||||
{ key: "dokumen_lahan", name: "Dokumen Lahan", desc: "SPPT/Sertifikat/surat sewa tempat usaha", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
|
||||
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
|
||||
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik usaha", type: "text" },
|
||||
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text" },
|
||||
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang atau jenis usaha", type: "text" },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" },
|
||||
{ key: "status_tempat", name: "Status Tempat Usaha", desc: "Status kepemilikan tempat usaha", type: "enum", options: enumStatusTempatUsaha },
|
||||
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number" },
|
||||
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah tenaga kerja", type: "number" },
|
||||
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text" }
|
||||
{ key: "nik", name: "NIK", desc: "NIK pemilik", type: "number", required: true },
|
||||
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik", type: "text", required: true },
|
||||
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text", required: true },
|
||||
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang usaha", type: "text", required: true },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true },
|
||||
{
|
||||
key: "status_tempat",
|
||||
name: "Status Tempat Usaha",
|
||||
desc: "Status kepemilikan tempat usaha",
|
||||
type: "enum",
|
||||
options: enumStatusTempatUsaha,
|
||||
required: true
|
||||
},
|
||||
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number", required: true },
|
||||
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah karyawan", type: "number", required: true },
|
||||
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "sktidakmampu",
|
||||
name: "Surat Keterangan Tidak Mampu",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kia_kk",
|
||||
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 },
|
||||
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan pemohon", type: "number" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama lengkap pemohon", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal pemohon", type: "text" },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan Surat Keterangan Tidak Mampu", type: "text" }
|
||||
{ key: "nik", name: "NIK", desc: "NIK pemohon", type: "number", required: true },
|
||||
{ key: "nama Lengkap", name: "Nama", desc: "Nama pemohon", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat pemohon", type: "text", required: true },
|
||||
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan permohonan", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skusaha",
|
||||
name: "Surat Keterangan Usaha",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kk",
|
||||
name: "KTP / KK",
|
||||
desc: "Fotokopi KTP atau Kartu Keluarga"
|
||||
},
|
||||
{
|
||||
key: "foto_lokasi",
|
||||
name: "Foto Lokasi Usaha",
|
||||
desc: "Foto lokasi usaha dicetak dan distempel oleh Kelian"
|
||||
}
|
||||
{ key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true },
|
||||
{ key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true },
|
||||
{ key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemilik usaha", type: "text" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemilik usaha", type: "text", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin pemilik usaha",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
|
||||
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan pemilik usaha", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan", type: "text", required: true },
|
||||
{
|
||||
key: "agama",
|
||||
name: "Agama",
|
||||
desc: "Agama pemilik usaha",
|
||||
desc: "Agama",
|
||||
type: "enum",
|
||||
options: enumAgama
|
||||
options: enumAgama,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: "status_perkawinan",
|
||||
name: "Status Perkawinan",
|
||||
desc: "Status perkawinan",
|
||||
type: "enum",
|
||||
options: enumStatusPerkawinan
|
||||
options: enumStatusPerkawinan,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemilik usaha", type: "text" },
|
||||
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha yang dijalankan", type: "text" },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" }
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true },
|
||||
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha", type: "text", required: true },
|
||||
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "skyatimpiatu",
|
||||
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
|
||||
syaratDokumen: [
|
||||
{
|
||||
key: "pengantar_kelian",
|
||||
name: "Pengantar Kelian",
|
||||
desc: "Surat Pengantar Kelian Banjar Dinas"
|
||||
},
|
||||
{
|
||||
key: "ktp_kia_kk",
|
||||
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 },
|
||||
{ key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true }
|
||||
],
|
||||
dataText: [],
|
||||
dataPelengkap: [
|
||||
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
|
||||
{ key: "nama", name: "Nama Lengkap", desc: "Nama anak", type: "text" },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir Anak", desc: "Tempat lahir anak", type: "text" },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" },
|
||||
{ key: "nik", name: "NIK", desc: "NIK anak", type: "number", required: true },
|
||||
{ key: "nama", name: "Nama", desc: "Nama anak", type: "text", required: true },
|
||||
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true },
|
||||
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true },
|
||||
{
|
||||
key: "jenis_kelamin",
|
||||
name: "Jenis Kelamin",
|
||||
desc: "Jenis kelamin anak",
|
||||
type: "enum",
|
||||
options: enumJenisKelamin
|
||||
options: enumJenisKelamin,
|
||||
required: true
|
||||
},
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text" },
|
||||
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text" },
|
||||
{ key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true },
|
||||
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text", required: false },
|
||||
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah", type: "text", required: true },
|
||||
{
|
||||
key: "status_ayah",
|
||||
name: "Status Ayah",
|
||||
desc: "Status ayah (hidup / meninggal)",
|
||||
desc: "Status ayah",
|
||||
type: "enum",
|
||||
options: enumStatusHidup
|
||||
options: enumStatusHidup,
|
||||
required: true
|
||||
},
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text" },
|
||||
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu", type: "text", required: true },
|
||||
{
|
||||
key: "status_ibu",
|
||||
name: "Status Ibu",
|
||||
desc: "Status ibu (hidup / meninggal)",
|
||||
desc: "Status ibu",
|
||||
type: "enum",
|
||||
options: enumStatusHidup
|
||||
options: enumStatusHidup,
|
||||
required: true
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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";
|
||||
@@ -73,25 +73,73 @@ export default function Login() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Stack>
|
||||
<Text>Login</Text>
|
||||
<TextInput
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<PasswordInput
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import useSWR from "swr";
|
||||
type DataItem = {
|
||||
key: string;
|
||||
value: string;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
@@ -52,7 +53,10 @@ type FormSurat = {
|
||||
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);
|
||||
@@ -93,7 +97,7 @@ export default function FormSurat() {
|
||||
setJenisSuratFix({ name: "", id: "" });
|
||||
} else {
|
||||
const namaJenis = fromSlug(jenisSurat);
|
||||
const data = listCategory.find((item: any) => item.name == namaJenis);
|
||||
const data = listCategory.find((item: any) => item.name.toUpperCase() == namaJenis.toUpperCase());
|
||||
if (!data) return;
|
||||
setJenisSuratFix(data);
|
||||
}
|
||||
@@ -116,15 +120,17 @@ export default function FormSurat() {
|
||||
nama: "",
|
||||
phone: "",
|
||||
dataPelengkap: (get.data?.dataPelengkap || []).map(
|
||||
(item: { key: string }) => ({
|
||||
(item: { key: string, required: boolean }) => ({
|
||||
key: item.key,
|
||||
value: "",
|
||||
required: item.required
|
||||
}),
|
||||
),
|
||||
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||||
(item: { key: string }) => ({
|
||||
(item: { key: string, required: boolean }) => ({
|
||||
key: item.key,
|
||||
value: "",
|
||||
required: item.required
|
||||
}),
|
||||
),
|
||||
});
|
||||
@@ -150,22 +156,24 @@ export default function FormSurat() {
|
||||
}, [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.length === 0 ||
|
||||
value.some(
|
||||
(item) =>
|
||||
typeof item.value === "string" && item.value.trim() === "",
|
||||
)
|
||||
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;
|
||||
return typeof value === "string" && value.trim() === "" ;
|
||||
});
|
||||
|
||||
if (isFormKosong) {
|
||||
@@ -174,9 +182,9 @@ export default function FormSurat() {
|
||||
message: "Silahkan lengkapi form surat",
|
||||
type: "error",
|
||||
});
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
|
||||
open();
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -239,6 +247,30 @@ export default function FormSurat() {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
@@ -246,12 +278,28 @@ export default function FormSurat() {
|
||||
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
|
||||
value: any;
|
||||
}) {
|
||||
if (key == "dataPelengkap" || key == "syaratDokumen") {
|
||||
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,
|
||||
@@ -259,6 +307,7 @@ export default function FormSurat() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"} pb={"lg"}>
|
||||
<Modal
|
||||
@@ -324,6 +373,7 @@ export default function FormSurat() {
|
||||
<Grid>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
allowDeselect={false}
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Jenis Surat"
|
||||
@@ -354,9 +404,10 @@ export default function FormSurat() {
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Nama" hint="Nama kontak" />}
|
||||
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 })
|
||||
}
|
||||
@@ -367,12 +418,15 @@ export default function FormSurat() {
|
||||
<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 })
|
||||
}
|
||||
@@ -396,10 +450,12 @@ export default function FormSurat() {
|
||||
<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 ?? []}
|
||||
@@ -407,7 +463,7 @@ export default function FormSurat() {
|
||||
onChange={(e) => {
|
||||
validationForm({
|
||||
key: "dataPelengkap",
|
||||
value: { key: item.key, value: e },
|
||||
value: { key: item.key, value: e, required: item.required },
|
||||
});
|
||||
}}
|
||||
value={
|
||||
@@ -424,31 +480,35 @@ export default function FormSurat() {
|
||||
<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")
|
||||
.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}
|
||||
@@ -458,6 +518,7 @@ export default function FormSurat() {
|
||||
value: {
|
||||
key: item.key,
|
||||
value: e.target.value,
|
||||
required: item.required,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -494,6 +555,7 @@ export default function FormSurat() {
|
||||
})
|
||||
}
|
||||
name={item.name}
|
||||
required={item.required}
|
||||
/>
|
||||
</Grid.Col>
|
||||
),
|
||||
@@ -518,10 +580,20 @@ export default function FormSurat() {
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
|
||||
return (
|
||||
<Group justify="apart" gap="xs" align="center">
|
||||
<Text fw={600}>{label}</Text>
|
||||
<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">
|
||||
@@ -533,6 +605,7 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
@@ -568,6 +641,7 @@ function FileInputWrapper({
|
||||
preview,
|
||||
name,
|
||||
description,
|
||||
required = false,
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
@@ -576,12 +650,20 @@ function FileInputWrapper({
|
||||
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}</Text>
|
||||
<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 }}>
|
||||
@@ -597,6 +679,7 @@ function FileInputWrapper({
|
||||
leftSection={<IconUpload />}
|
||||
aria-label={label}
|
||||
name={name}
|
||||
clearable={true}
|
||||
/>
|
||||
|
||||
{preview ? (
|
||||
|
||||
@@ -50,6 +50,7 @@ type UpdateDataItem = {
|
||||
id: string;
|
||||
key: string;
|
||||
value: any;
|
||||
required: boolean;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
@@ -125,10 +126,20 @@ export default function UpdateDataSurat() {
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
function FieldLabel({ label, hint, required = false, }: { label: string; hint?: string; required?: boolean; }) {
|
||||
return (
|
||||
<Group justify="apart" gap="xs" align="center">
|
||||
<Text fw={600}>{label}</Text>
|
||||
<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">
|
||||
@@ -139,7 +150,6 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
@@ -185,6 +195,8 @@ function FileInputWrapper({
|
||||
description,
|
||||
linkView,
|
||||
disabled,
|
||||
required = false,
|
||||
|
||||
}: {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
@@ -195,6 +207,7 @@ function FileInputWrapper({
|
||||
name: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
}) {
|
||||
const [viewImg, setViewImg] = useState("");
|
||||
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
|
||||
@@ -219,7 +232,14 @@ function FileInputWrapper({
|
||||
<Stack gap="xs">
|
||||
<Flex direction={"column"}>
|
||||
<Group justify="apart" align="center">
|
||||
<Text fw={500}>{label}</Text>
|
||||
<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 }}>
|
||||
@@ -241,6 +261,7 @@ function FileInputWrapper({
|
||||
aria-label={label}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
clearable={true}
|
||||
/>
|
||||
|
||||
{preview ? (
|
||||
@@ -317,57 +338,60 @@ function SearchData() {
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
<>
|
||||
<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={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>
|
||||
<Grid.Col span={12}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={() => {
|
||||
handleSearch();
|
||||
}}
|
||||
loading={submitLoading}
|
||||
>
|
||||
Cari Pengajuan
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,12 +404,14 @@ function DataUpdate({
|
||||
}) {
|
||||
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: [],
|
||||
@@ -393,6 +419,7 @@ function DataUpdate({
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
setLoadingFetchData(true);
|
||||
const res = await apiFetch.api.pelayanan["detail-data"].post({
|
||||
nomerPengajuan: noPengajuan,
|
||||
});
|
||||
@@ -420,6 +447,8 @@ function DataUpdate({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoadingFetchData(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +456,29 @@ function DataUpdate({
|
||||
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);
|
||||
|
||||
@@ -446,16 +498,36 @@ function DataUpdate({
|
||||
kategori: "dataPelengkap" | "syaratDokumen";
|
||||
value: UpdateDataItem;
|
||||
}) {
|
||||
setFormSurat((prev) => ({
|
||||
...prev,
|
||||
[kategori]: upsertById(prev[kategori], {
|
||||
id: value.id,
|
||||
key: value.key,
|
||||
value: value.value,
|
||||
}),
|
||||
}));
|
||||
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,
|
||||
@@ -465,6 +537,16 @@ function DataUpdate({
|
||||
}
|
||||
|
||||
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
|
||||
@@ -479,7 +561,7 @@ function DataUpdate({
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(
|
||||
(item) =>
|
||||
typeof item.value === "string" && item.value.trim() === "",
|
||||
(typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -558,7 +640,7 @@ function DataUpdate({
|
||||
|
||||
return (
|
||||
<>
|
||||
<FullScreenLoading visible={submitLoading} />
|
||||
<FullScreenLoading visible={submitLoading || loadingFetchData} />
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
@@ -625,13 +707,15 @@ function DataUpdate({
|
||||
<Grid.Col span={6} key={index}>
|
||||
{item.type == "enum" ? (
|
||||
<Select
|
||||
label={<FieldLabel label={item.name} hint={item.desc} />}
|
||||
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 },
|
||||
value: { id: item.id, key: item.key, value: e, required: item.required },
|
||||
});
|
||||
}}
|
||||
value={
|
||||
@@ -643,9 +727,10 @@ function DataUpdate({
|
||||
/>
|
||||
) : item.type == "date" ? (
|
||||
<DateInput
|
||||
disabled={status != "ditolak" && status != "antrian"}
|
||||
locale="id"
|
||||
valueFormat="DD MMMM YYYY"
|
||||
label={<FieldLabel label={item.name} hint={item.desc} />}
|
||||
label={<FieldLabel label={item.name} hint={item.desc} required={item.required} />}
|
||||
placeholder={item.name}
|
||||
onChange={(e) => {
|
||||
const formatted = e
|
||||
@@ -657,6 +742,7 @@ function DataUpdate({
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: formatted,
|
||||
required: item.required
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -674,8 +760,10 @@ function DataUpdate({
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label={<FieldLabel label={item.name} hint={item.desc} />}
|
||||
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",
|
||||
@@ -683,6 +771,7 @@ function DataUpdate({
|
||||
id: item.id,
|
||||
key: item.key,
|
||||
value: e.target.value,
|
||||
required: item.required,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -708,6 +797,7 @@ function DataUpdate({
|
||||
{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"
|
||||
@@ -715,7 +805,7 @@ function DataUpdate({
|
||||
onChange={(file) =>
|
||||
validationForm({
|
||||
kategori: "syaratDokumen",
|
||||
value: { id: item.id, key: item.key, value: file },
|
||||
value: { id: item.id, key: item.key, value: file, required: item.required },
|
||||
})
|
||||
}
|
||||
name={item.name}
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function DashboardLayout() {
|
||||
<AppShell
|
||||
padding="lg"
|
||||
navbar={{
|
||||
width: 260,
|
||||
width: 300,
|
||||
breakpoint: "sm",
|
||||
collapsed: { mobile: !opened, desktop: !opened },
|
||||
}}
|
||||
@@ -290,22 +290,31 @@ function NavigationDashboard() {
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute)}
|
||||
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) && (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="teal"
|
||||
radius="sm"
|
||||
size="xs"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
{(
|
||||
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}
|
||||
@@ -313,7 +322,10 @@ function NavigationDashboard() {
|
||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive(item.path as keyof typeof clientRoute)
|
||||
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",
|
||||
|
||||
@@ -1,8 +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,
|
||||
@@ -14,24 +18,31 @@ import {
|
||||
Group,
|
||||
List,
|
||||
Modal,
|
||||
Select,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
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,
|
||||
} 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";
|
||||
@@ -40,6 +51,11 @@ 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");
|
||||
@@ -58,6 +74,9 @@ 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
|
||||
@@ -93,6 +112,7 @@ function DetailDataPengajuan({
|
||||
dataText: any;
|
||||
onAction: () => void;
|
||||
}) {
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
@@ -101,8 +121,12 @@ function DetailDataPengajuan({
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
|
||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||
const [viewImg, setViewImg] = useState("");
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [viewImg, setViewImg] = useState({ file: "", folder: "" });
|
||||
const [uploading, setUploading] = useState({ ok: false, file: "" });
|
||||
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", 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() {
|
||||
@@ -121,6 +145,7 @@ function DetailDataPengajuan({
|
||||
|
||||
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 ?? "",
|
||||
@@ -138,6 +163,11 @@ function DetailDataPengajuan({
|
||||
message: "Success send message to warga",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
if (status == "selesai") {
|
||||
onAction()
|
||||
}
|
||||
|
||||
} else {
|
||||
notification({
|
||||
title: "Failed",
|
||||
@@ -159,10 +189,14 @@ function DetailDataPengajuan({
|
||||
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"
|
||||
@@ -212,9 +246,48 @@ function DetailDataPengajuan({
|
||||
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);
|
||||
@@ -222,25 +295,105 @@ function DetailDataPengajuan({
|
||||
}, [viewImg]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (uploading) {
|
||||
if (uploading.ok && uploading.file) {
|
||||
sendWA({
|
||||
status: "selesai",
|
||||
linkSurat: "",
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<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)}
|
||||
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
|
||||
onClose={() => {
|
||||
setOpenedPreviewFile(false);
|
||||
setViewImg({ file: "", folder: "" })
|
||||
}}
|
||||
folder="syarat-dokumen"
|
||||
fileName={viewImg}
|
||||
folder={viewImg.folder}
|
||||
fileName={viewImg.file}
|
||||
/>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
@@ -312,12 +465,14 @@ function DetailDataPengajuan({
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
{data?.status == "selesai" && (
|
||||
|
||||
{/* MODAL PREVIEW SURAT */}
|
||||
{data?.status == "selesai" && !data?.fileSurat && (
|
||||
<ModalSurat
|
||||
open={openedPreview}
|
||||
onClose={() => {
|
||||
onClose={(val) => {
|
||||
setOpenedPreview(false)
|
||||
setUploading(true)
|
||||
setUploading({ ok: val.success, file: val.data })
|
||||
}}
|
||||
surat={data?.idSurat}
|
||||
/>
|
||||
@@ -386,7 +541,7 @@ function DetailDataPengajuan({
|
||||
<List.Item key={v.id}>
|
||||
<Anchor
|
||||
onClick={() => {
|
||||
setViewImg(v.value);
|
||||
setViewImg({ file: v.value, folder: "syarat-dokumen" });
|
||||
}}
|
||||
>
|
||||
{v.jenis}
|
||||
@@ -413,7 +568,25 @@ function DetailDataPengajuan({
|
||||
</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>
|
||||
{_.upperFirst(item.value)}
|
||||
<Flex
|
||||
gap="md"
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
direction="row"
|
||||
>
|
||||
<Text>
|
||||
{_.upperFirst(item.value)}
|
||||
</Text>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
aria-label="Edit"
|
||||
onClick={() => {
|
||||
setEditValue({ id: item.id, val: item.value, type: item.type, option: item.options, jenis: item.jenis, key: item.key })
|
||||
setOpenEdit(true)
|
||||
}}>
|
||||
<IconEdit size={16} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
@@ -471,18 +644,31 @@ function DetailDataPengajuan({
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "selesai" ? (
|
||||
<Group justify="center" grow>
|
||||
{/* <Button
|
||||
variant="light"
|
||||
onClick={() => setOpenedPreview(!openedPreview)}
|
||||
>
|
||||
Surat
|
||||
</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>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import ModalFile from "@/components/ModalFile";
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
@@ -40,6 +41,11 @@ 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");
|
||||
@@ -58,6 +64,9 @@ 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
|
||||
@@ -93,6 +102,7 @@ function DetailDataPengaduan({
|
||||
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() {
|
||||
@@ -111,6 +121,7 @@ function DetailDataPengaduan({
|
||||
|
||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await apiFetch.api.pengaduan["update-status"].post({
|
||||
id: data?.id,
|
||||
status:
|
||||
@@ -184,6 +195,8 @@ function DetailDataPengaduan({
|
||||
message: "Failed to update pengaduan",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -218,6 +231,7 @@ function DetailDataPengaduan({
|
||||
color="red"
|
||||
disabled={keterangan.length < 1}
|
||||
onClick={() => handleKonfirmasi("tolak")}
|
||||
loading={isLoading}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
@@ -242,6 +256,7 @@ function DetailDataPengaduan({
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => handleKonfirmasi("terima")}
|
||||
loading={isLoading}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||
import DesaSetting from "@/components/DesaSetting";
|
||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||
@@ -15,14 +16,21 @@ import {
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
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() {
|
||||
@@ -87,6 +95,9 @@ export default function DetailSettingPage() {
|
||||
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"
|
||||
@@ -104,7 +115,7 @@ export default function DetailSettingPage() {
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.key}
|
||||
href={"?type=" + item.path}
|
||||
onClick={()=>{navigate("?type=" + item.path)}}
|
||||
label={item.label}
|
||||
leftSection={item.icon}
|
||||
active={
|
||||
|
||||
@@ -1,62 +1,75 @@
|
||||
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!,
|
||||
},
|
||||
}),
|
||||
);
|
||||
// const { data, mutate, isLoading } = useSwr("/", () =>
|
||||
// apiFetch.api.warga.detail.get({
|
||||
// query: {
|
||||
// id: id!,
|
||||
// },
|
||||
// }),
|
||||
// );
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
// useShallowEffect(() => {
|
||||
// mutate();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingOverlay
|
||||
visible={isLoading}
|
||||
// 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}
|
||||
id={id!}
|
||||
kategori="pengaduan"
|
||||
/>
|
||||
<DetailDataHistori
|
||||
data={data?.data?.pelayanan}
|
||||
id={id!}
|
||||
kategori="pelayanan"
|
||||
/>
|
||||
</Stack>
|
||||
@@ -68,13 +81,66 @@ export default function DetailWargaPage() {
|
||||
}
|
||||
|
||||
function DetailDataHistori({
|
||||
data,
|
||||
id,
|
||||
kategori,
|
||||
}: {
|
||||
data: any;
|
||||
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
|
||||
@@ -93,6 +159,36 @@ function DetailDataHistori({
|
||||
<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>
|
||||
@@ -110,7 +206,7 @@ function DetailDataHistori({
|
||||
{data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>
|
||||
{kategori == "pengaduan" ? item.title : item.category}
|
||||
</Table.Td>
|
||||
@@ -121,11 +217,11 @@ function DetailDataHistori({
|
||||
onClick={() => {
|
||||
kategori == "pengaduan"
|
||||
? navigate(
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
)
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
)
|
||||
: navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
);
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
@@ -147,7 +243,33 @@ function DetailDataHistori({
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
@@ -248,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> {
|
||||
|
||||
@@ -265,6 +265,7 @@ const PelayananRoute = new Elysia({
|
||||
select: {
|
||||
id: true,
|
||||
idCategory: true,
|
||||
file: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -307,7 +308,11 @@ const PelayananRoute = new Elysia({
|
||||
})
|
||||
|
||||
const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
|
||||
name: string;
|
||||
type: string;
|
||||
options?: {
|
||||
label: string,
|
||||
value: string
|
||||
}[]; name: string;
|
||||
desc: string;
|
||||
key: string;
|
||||
}[];
|
||||
@@ -326,7 +331,10 @@ const PelayananRoute = new Elysia({
|
||||
return {
|
||||
id: item.id,
|
||||
jenis: nama,
|
||||
key: ref?.key,
|
||||
value: item.value,
|
||||
type: ref?.type ?? "",
|
||||
options: ref?.options ?? [],
|
||||
order: ref?.order ?? Infinity,
|
||||
};
|
||||
})
|
||||
@@ -381,6 +389,7 @@ const PelayananRoute = new Elysia({
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
idSurat: dataSurat?.id,
|
||||
fileSurat: dataSurat?.file,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
@@ -676,17 +685,20 @@ const PelayananRoute = new Elysia({
|
||||
name: string;
|
||||
desc: string;
|
||||
key: string;
|
||||
required: boolean;
|
||||
}[];
|
||||
|
||||
const dataSyaratFix = dataSyarat.map((item) => {
|
||||
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
|
||||
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
|
||||
const required = syaratDokumen.find((v) => v.key == item.jenis)?.required
|
||||
return {
|
||||
id: item.id,
|
||||
key: item.jenis,
|
||||
value: item.value,
|
||||
name: name ?? '',
|
||||
desc: desc ?? ''
|
||||
desc: desc ?? '',
|
||||
required: required ?? true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -699,6 +711,7 @@ const PelayananRoute = new Elysia({
|
||||
name: string;
|
||||
desc: string;
|
||||
key: string;
|
||||
required: boolean;
|
||||
}[];
|
||||
|
||||
const refMap = new Map(
|
||||
@@ -721,6 +734,7 @@ const PelayananRoute = new Elysia({
|
||||
type: ref?.type ?? "",
|
||||
options: ref?.options ?? [],
|
||||
order: ref?.order ?? Infinity,
|
||||
required: ref?.required ?? true
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.order - b.order)
|
||||
@@ -1048,6 +1062,73 @@ const PelayananRoute = new Elysia({
|
||||
description: `tool untuk update data pengajuan pelayanan surat`,
|
||||
}
|
||||
})
|
||||
.post("/update-data-pelengkap", async ({ body }) => {
|
||||
const { id, value, jenis, idUser } = body
|
||||
|
||||
const dataPelengkap = await prisma.dataTextPelayanan.findUnique({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
idPengajuanLayanan: true,
|
||||
PelayananAjuan: {
|
||||
select: {
|
||||
status: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if (!dataPelengkap) {
|
||||
return { success: false, message: 'data pelengkap surat tidak ditemukan' }
|
||||
}
|
||||
|
||||
const upd = await prisma.dataTextPelayanan.update({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
data: {
|
||||
value: value,
|
||||
}
|
||||
})
|
||||
|
||||
const history = await prisma.historyPelayanan.create({
|
||||
data: {
|
||||
idPengajuanLayanan: dataPelengkap.idPengajuanLayanan,
|
||||
deskripsi: `Pengajuan surat diupdate oleh user (data yg diupdate: ${jenis})`,
|
||||
status: dataPelengkap.PelayananAjuan.status,
|
||||
idUser
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return { success: true, message: 'data pelengkap surat sudah diperbarui' }
|
||||
|
||||
}, {
|
||||
body: t.Object({
|
||||
id: t.String({
|
||||
error: "id harus diisi",
|
||||
description: "ID yang ingin diupdate"
|
||||
}),
|
||||
value: t.String({
|
||||
error: "value harus diisi",
|
||||
description: "Value yang ingin diupdate"
|
||||
}),
|
||||
jenis: t.String({
|
||||
error: "jenis harus diisi",
|
||||
description: "Jenis data yang ingin diupdate"
|
||||
}),
|
||||
idUser: t.String({
|
||||
error: "idUser harus diisi",
|
||||
description: "ID user yang melakukan update"
|
||||
})
|
||||
}),
|
||||
detail: {
|
||||
summary: "Update Data Pelengkap Pengajuan Pelayanan Surat oleh user admin",
|
||||
description: `tool untuk update data pelengkap pengajuan pelayanan surat oleh user admin`,
|
||||
}
|
||||
})
|
||||
.get("/list", async ({ query }) => {
|
||||
const { take, page, search, status } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import Elysia, { t } from "elysia"
|
||||
import type { StatusPengaduan } from "generated/prisma"
|
||||
import _ from "lodash"
|
||||
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, removeFile, uploadFile, uploadFileToFolder } 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",
|
||||
@@ -605,6 +607,43 @@ const PengaduanRoute = new Elysia({
|
||||
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;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const SuratRoute = new Elysia({
|
||||
noSurat: true,
|
||||
idCategory: true,
|
||||
createdAt: true,
|
||||
file: true,
|
||||
PelayananAjuan: {
|
||||
select: {
|
||||
DataTextPelayanan: true,
|
||||
@@ -44,6 +45,7 @@ const SuratRoute = new Elysia({
|
||||
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" }),
|
||||
},
|
||||
@@ -60,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
|
||||
|
||||
@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
.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