Compare commits

...

42 Commits

Author SHA1 Message Date
d6abc163fb Merge pull request 'upd: tambah satuan' (#115) from amalia/06-feb-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/115
2026-02-06 17:46:25 +08:00
9c08980bf1 upd: tambah satuan
Deskripsi:
- satuan luas tempat usaha
- satuan pendapatan perbulan
- pada tambah, edit, detail surat

No Issues
2026-02-06 14:39:58 +08:00
a2af3fbe36 Merge pull request 'upd: form tambah surat' (#114) from amalia/21-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/114
2026-01-21 11:55:14 +08:00
ec8722ffba upd: form tambah surat
Deskripsi:
- fix select jenis surat pada saat selesai input

No Issue
2026-01-21 09:02:25 +08:00
b0752dac8d Merge pull request 'amalia/20-jan-26' (#113) from amalia/20-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/113
2026-01-20 17:28:34 +08:00
a8d3a3a9ff fix: pelayanan surat
Deskripsi:
- mandatory pada form tambah pelayanan surat
- mandatory pada form update pelayanan surat

No Issues
2026-01-20 17:15:16 +08:00
f86703e7d1 rev: button cancel
Deskripsi:
- tambah button clear pada form file tambah pengajuan surat
- tambah button clear pada form file update data pengajuan surat

NO Issues
2026-01-20 10:48:37 +08:00
d8bb33cc93 Merge pull request 'amalia/15-jan-26' (#112) from amalia/15-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/112
2026-01-15 14:33:16 +08:00
5807e98069 fix: tambah pengajuan surat pengantar skck
Deskripsi:
- link nya

No Issues
2026-01-15 14:32:28 +08:00
59b4f1d73f fix: surat keterangan kelahiran
Deskripsi:
- update value tanggal_lahir_ayah

No Issues
2026-01-15 14:19:05 +08:00
8bd552ac22 fix: pelayanan surat
Deskripsi:
- link surat

NO Issues
2026-01-15 14:09:32 +08:00
5d48d06513 Merge pull request 'fix : error surat' (#111) from amalia/15-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/111
2026-01-15 11:54:25 +08:00
3da163ea1d fix : error surat 2026-01-15 11:53:10 +08:00
57e4f34eb6 Merge pull request 'amalia/14-jan-26' (#110) from amalia/14-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/110
2026-01-14 17:43:11 +08:00
3348cbe8e3 fix: pelayanan surat
Deskripsi:
- pelayanan surat

No Issues
2026-01-14 17:41:27 +08:00
727984a076 fix: update data pengajuan surat
Deskripsi:
- loading saat melakukan pencarian
- disable select dan input date saat status selesai

No Issues
2026-01-14 15:58:51 +08:00
a7a0ad7e37 Merge pull request 'amalia/13-jan-26' (#109) from amalia/13-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/109
2026-01-13 17:18:01 +08:00
9fed41cbe8 fix: testing surat 2026-01-13 17:10:59 +08:00
fc387fe8e6 fix: menu setting
deskiripsi:
- navigate
- list length

No Issues
2026-01-13 15:38:29 +08:00
80df579499 qc: nomer 2 dan 4
Deskripsi:
- button back
- breadcrumb
- active menu

No Issues
2026-01-13 15:10:08 +08:00
5bbbc15c27 Merge pull request 'fix: qc' (#108) from amalia/12-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/108
2026-01-12 17:47:04 +08:00
82765f6ef0 fix: qc
Deskripsi:
- breadcumbs
- back
- active menu

nb: blm selesai

No Issues
2026-01-12 17:43:04 +08:00
e8b5720118 Merge pull request 'amalia/09-jan-26' (#107) from amalia/09-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/107
2026-01-09 17:23:26 +08:00
01334ec573 upd: login
Deskripsi:
- update design login page

No Issues
2026-01-09 16:43:19 +08:00
98ad9b0d72 upd: loading saat melakukan aksi pada detail pengaduan
- mencegah 2x klik

NO Issues
2026-01-09 15:53:41 +08:00
c0471f47f3 upd: detail warga
Deskripsi:
- pagination pada list pengaduan dan list pengajuan surat
- search pada list pengaduan dan list pengajuan surat

No Issues
2026-01-09 15:46:30 +08:00
3d641d2035 Merge pull request 'upd: hapus console log' (#106) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/106
2026-01-08 17:38:19 +08:00
694115dbfb upd: hapus console log 2026-01-08 17:37:37 +08:00
7de5078868 Merge pull request 'upd: console log server2' (#105) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/105
2026-01-08 16:25:03 +08:00
7a3faa5719 upd: console log server2 2026-01-08 16:24:13 +08:00
ea5072d9ab Merge pull request 'upd: console log server' (#104) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/104
2026-01-08 16:03:03 +08:00
e8bb4f5a41 upd: console log server 2026-01-08 16:02:01 +08:00
d63bf024d3 Merge pull request 'upd: console log send wa' (#103) from amalia/08-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/103
2026-01-08 15:50:37 +08:00
46f7dbf7bb upd: console log send wa 2026-01-08 15:49:51 +08:00
1adea29990 Merge pull request 'amalia/07-jan-26' (#102) from amalia/07-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/102
2026-01-07 17:11:50 +08:00
2a5b6e7b7c fix: validasi form tambah pengajuan surat 2026-01-07 17:10:28 +08:00
2117612337 upd: pengajuan surat
Deskripsi:
- form edit data pelengkap

No Issues
2026-01-07 15:23:34 +08:00
8f33ec2ffa pelayanan surat
deskripsi:
- update validasi form tambah pengajuan layanan surat
- update validasi form update pengajuan layanan surat

No Issues
2026-01-07 12:02:27 +08:00
411f61ec15 Merge pull request 'amalia/06-jan-26' (#101) from amalia/06-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/101
2026-01-06 17:45:44 +08:00
476319945e upd: validasi form create dan update pengajuan surat
Deskripsi:
- deselect false pada input select form tambah dan update pengajuan surat

No Issues
2026-01-06 17:44:16 +08:00
8480cec6ae upd: notif wa pengajian surat
Deskripsi:
- upload surat ke seafile
- update struktur db
- notif wa kirim link download surat
- api download surat

No Issues;
2026-01-06 17:00:08 +08:00
4ca5e4c4f3 Merge pull request 'upd: pengajuan surat' (#100) from amalia/05-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/100
2026-01-05 17:25:27 +08:00
21 changed files with 1367 additions and 577 deletions

View File

@@ -187,6 +187,7 @@ model SuratPelayanan {
Warga Warga @relation(fields: [idWarga], references: [id]) Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String idWarga String
noSurat String noSurat String
file String?
dateExpired DateTime? @db.Date dateExpired DateTime? @db.Date
status Int @default(0) status Int @default(0)
isActive Boolean @default(true) isActive Boolean @default(true)

View 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>
)
}

View File

@@ -207,7 +207,7 @@ export default function DesaSetting({
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{list?.map((v: any) => ( {list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}> <Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Table.Td>{v.name}</Table.Td>
<Table.Td> <Table.Td>

View File

@@ -4,19 +4,17 @@ import {
Button, Button,
Divider, Divider,
Flex, Flex,
Grid,
Group, Group,
Input,
List, List,
Modal, Modal,
Stack, Stack,
Table, Table,
Text, Text,
Title, Title,
Tooltip, Tooltip
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; 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 type { JsonValue } from "generated/prisma/runtime/library";
import { useState } from "react"; import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
@@ -625,7 +623,7 @@ export default function KategoriPelayananSurat({
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{list?.map((v: any) => ( {list.length > 0 && list?.map((v: any) => (
<Table.Tr key={v.id}> <Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Table.Td>{v.name}</Table.Td>
<Table.Td> <Table.Td>

View File

@@ -1,5 +1,5 @@
import apiFetch from "@/lib/apiFetch"; 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 { useShallowEffect } from "@mantine/hooks";
import html2canvas from "html2canvas"; import html2canvas from "html2canvas";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
@@ -23,7 +23,7 @@ export default function ModalSurat({
surat, surat,
}: { }: {
open: boolean; open: boolean;
onClose: () => void; onClose: (val: { success: boolean, data: string }) => void;
surat: string; surat: string;
}) { }) {
const A4Style = { const A4Style = {
@@ -35,7 +35,7 @@ export default function ModalSurat({
fontSize: "14px", fontSize: "14px",
fontFamily: "Times New Roman", 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 hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () => const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({ apiFetch.api.surat.detail.get({
@@ -45,75 +45,97 @@ export default function ModalSurat({
}), }),
); );
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
const uploadPdf = async () => { const uploadPdf = async () => {
try { try {
setUploading("Mengupload"); if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
const element = hiddenRef.current; setUploading({ text: "Mengupload", value: 75 });
const canvas = await html2canvas(element, { const element = hiddenRef.current;
scale: 2, const canvas = await html2canvas(element, {
useCORS: true, scale: 2,
allowTaint: true, useCORS: true,
width: element.offsetWidth, allowTaint: true,
height: element.offsetHeight, 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 pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth; const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width; 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 // ⬇️ ambil sebagai Blob
const pdfBlob = pdf.output("blob"); const pdfBlob = pdf.output("blob");
const pdfFile = new File( const pdfFile = new File(
[pdfBlob], [pdfBlob],
`${data?.data?.surat?.nameCategory}.pdf`, `${data?.data?.surat?.nameCategory}.pdf`,
{ {
type: "application/pdf", type: "application/pdf",
lastModified: Date.now(), 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) { } catch (error) {
console.error("Error uploading PDF:", error); console.error("Error uploading PDF:", error);
} finally {
setUploading("Selesai");
setTimeout(() => {
onClose();
}, 1000)
} }
} }
useShallowEffect(() => { useShallowEffect(() => {
setTimeout(() => { if (open) {
uploadPdf(); setTimeout(() => {
}, 5000); uploadPdf();
}, [surat]); }, 3000);
}
}, [surat, open]);
return ( return (
<> <>
<Modal <Modal
opened={open} opened={open}
onClose={() => onClose()} onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto" size="auto"
withCloseButton={false} withCloseButton={false}
closeOnClickOutside={false}
removeScrollProps={{ allowPinchZoom: true }} removeScrollProps={{ allowPinchZoom: true }}
styles={{ styles={{
header: { header: {
@@ -127,14 +149,24 @@ export default function ModalSurat({
}, },
}} }}
title={ 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" /> <Loader color="blue" size="xs" />
<Text size="sm">{uploading}</Text> <Text size="sm">{uploading.text}</Text>
</Flex> */}
</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}> <div ref={hiddenRef} style={A4Style}>

View File

@@ -7,7 +7,7 @@ export default function SKKelahiran({ data }: { data: any }) {
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"", "",
); );
const loadImage = async () => { const loadImage = async () => {
@@ -161,7 +161,7 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr> <tr>
<td>Tempat & Tanggal Lahir</td> <td>Tempat & Tanggal Lahir</td>
<td>:</td> <td>:</td>
<td>{`${getValue("tempat_lahir_ayah")}, ${"tanggal_lahir_ayah"}`}</td> <td>{`${getValue("tempat_lahir_ayah")}, ${getValue("tanggal_lahir_ayah")}`}</td>
</tr> </tr>
<tr> <tr>
<td>Pekerjaan</td> <td>Pekerjaan</td>

View File

@@ -90,7 +90,7 @@ export default function SKTempatUsaha({ data }: { data: any }) {
<Row label="Bidang Usaha" value={getValue("bidang_usaha")} /> <Row label="Bidang Usaha" value={getValue("bidang_usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat_usaha")} /> <Row label="Alamat Usaha" value={getValue("alamat_usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status_tempat")} /> <Row label="Status Tempat Usaha" value={getValue("status_tempat")} />
<Row label="Luas Tempat Usaha" value={getValue("luas_usaha")} /> <Row label="Luas Tempat Usaha" value={getValue("luas_usaha") + " m2"} />
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} /> <Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
</div> </div>

View File

@@ -8,472 +8,377 @@ export const categoryPelayananSurat = [
{ {
key: "pengantar_kelian", key: "pengantar_kelian",
name: "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, satuan: null
}, },
{ {
key: "ktp_kk", key: "ktp_kk",
name: "KTP / KK", name: "KTP / KK",
desc: "Fotokopi KTP atau Kartu Keluarga" desc: "Fotokopi KTP atau Kartu Keluarga",
required: true, satuan: null
}, },
{ {
key: "dokumen_beda", key: "dokumen_beda",
name: "Dokumen Pendukung", 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, satuan: null
} }
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" }, { key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" }, { key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon", desc: "Jenis kelamin pemohon",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" }, { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
{ {
key: "dokumen", key: "dokumen",
name: "Nama Dokumen", name: "Nama Dokumen",
desc: "Jenis dokumen yang mengalami perbedaan biodata (cth : ijazah, sertifikat, dll)", desc: "Jenis dokumen yang mengalami perbedaan biodata",
type: "text" type: "text",
required: true, satuan: null
}, },
{ {
key: "data_dokumen", key: "data_dokumen",
name: "Data Dokumen", name: "Data Dokumen",
desc: "Data dokumen yg berbeda (cth : nama, tempat lahir, tanggal lahir, jenis kelamin, alamat, pekerjaan)", desc: "Data dokumen yg berbeda",
type: "text" type: "text",
required: true, satuan: null
}, },
{ { key: "dokumen_a", name: "Data pada Dokumen A", desc: "Data biodata pada dokumen pertama", type: "text", required: true, satuan: null },
key: "dokumen_a", { key: "dokumen_b", name: "Data pada Dokumen B", desc: "Data biodata pada dokumen kedua", type: "text", required: true, satuan: null }
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"
}
] ]
}, },
{ {
id: "skbelumkawin", id: "skbelumkawin",
name: "Surat Keterangan Belum Kawin", name: "Surat Keterangan Belum Kawin",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau Kartu Keluarga", required: true, satuan: null },
name: "Pengantar Kelian", { key: "akta_cerai", name: "Akta Cerai", desc: "Fotokopi akta cerai (jika berstatus janda/duda)", required: false, satuan: null }
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)"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" }, { key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" }, { key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon", desc: "Jenis kelamin pemohon",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{ {
key: "agama", key: "agama",
name: "Agama", name: "Agama",
desc: "Agama pemohon", desc: "Agama pemohon",
type: "enum", type: "enum",
options: enumAgama options: enumAgama,
required: true, satuan: null
}, },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" } { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skdomisiliorganisasi", id: "skdomisiliorganisasi",
name: "Surat Keterangan Domisili Organisasi", name: "Surat Keterangan Domisili Organisasi",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "skt_organisasi", name: "SKT Organisasi", desc: "Fotokopi SKT Organisasi", required: true, satuan: null },
name: "Pengantar Kelian", { key: "susunan_pengurus", name: "Susunan Pengurus", desc: "Susunan pengurus organisasi", required: true, satuan: null }
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"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text" }, { key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text", required: true, satuan: null },
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis atau bentuk organisasi", type: "text" }, { key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis organisasi", type: "text", required: true, satuan: null },
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat organisasi", type: "text" }, { key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat", type: "text", required: true, satuan: null },
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text" }, { key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text", required: true, satuan: null },
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan organisasi", type: "text" }, { key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan", type: "text", required: true, satuan: null },
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text" } { key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skkelahiran", id: "skkelahiran",
name: "Surat Keterangan Kelahiran", name: "Surat Keterangan Kelahiran",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "surat_lahir", name: "Surat Keterangan Lahir", desc: "Surat keterangan lahir dari bidan/dokter (jika ada)", required: false, satuan: null }
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)"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text" }, { key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" }, { key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date", required: true, satuan: null },
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran anak", type: "text" }, { key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran anak", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran", type: "text", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin Anak", name: "Jenis Kelamin Anak",
desc: "Jenis kelamin anak", desc: "Jenis kelamin anak",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran anak", type: "number" }, { key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran", type: "number", required: true, satuan: null },
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number" }, { key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number", required: true, satuan: null },
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama lengkap ibu", type: "text" }, { key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text", required: true, satuan: null },
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu kandung", type: "text" }, { key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu kandung", type: "date" }, { key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu", type: "date", required: true, satuan: null },
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu kandung", type: "text" }, { key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu", type: "text", required: true, satuan: null },
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu kandung", type: "text" }, { key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu", type: "text", required: true, satuan: null },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama lengkap ayah", type: "text" }, { key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text", required: true, satuan: null },
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number" }, { key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number", required: true, satuan: null },
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah kandung", type: "text" }, { key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah kandung", type: "date" }, { key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah", type: "date", required: true, satuan: null },
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah kandung", type: "text" }, { key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah", type: "text", required: true, satuan: null },
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah kandung", type: "text" }, { key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah", type: "text", required: true, satuan: null },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pihak yang melaporkan", type: "text" }, { key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan pelapor dengan anak", type: "text" }, { key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan anak", type: "text", required: true, satuan: null },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text" } { key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skkelakuanbaik", id: "skkelakuanbaik",
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)", name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null }
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kk",
name: "KTP / KK",
desc: "Fotokopi KTP atau Kartu Keluarga"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" }, { key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" }, { key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon", desc: "Jenis kelamin",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ {
key: "agama", key: "agama",
name: "Agama", name: "Agama",
desc: "Agama pemohon", desc: "Agama",
type: "enum", type: "enum",
options: enumAgama options: enumAgama,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" }, { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan pembuatan SKCK", type: "text" } { key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skkematian", id: "skkematian",
name: "Surat Keterangan Kematian", name: "Surat Keterangan Kematian",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP atau KK", required: true, satuan: null },
name: "Pengantar Kelian", { key: "surat_kematian", name: "Surat Keterangan Kematian", desc: "Surat keterangan kematian dari rumah sakit/dokter (jika ada)", required: false, satuan: null }
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)"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "Nomor Induk Kependudukan pelapor", type: "number" }, { key: "nik_pelapor", name: "NIK Pelapor", desc: "NIK pelapor", type: "number", required: true, satuan: null },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama lengkap pelapor", type: "text" }, { key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pelapor", type: "text", required: true, satuan: null },
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text" }, { key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text", required: true, satuan: null },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat tempat tinggal pelapor", type: "text" }, { key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text", required: true, satuan: null },
{ key: "hubungan_pelapor", name: "Hubungan dengan Almarhum", desc: "Hubungan pelapor dengan almarhum", type: "text" }, { key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan dengan almarhum", type: "text", required: true, satuan: null },
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama lengkap almarhum", type: "text" }, { key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama almarhum", type: "text", required: true, satuan: null },
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "Nomor Induk Kependudukan almarhum", type: "number" }, { key: "nik_almarhum", name: "NIK Almarhum", desc: "NIK almarhum", type: "number", required: true, satuan: null },
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir Almarhum", desc: "Tempat lahir almarhum", type: "text" }, { key: "tempat_lahir_almarhum", name: "Tempat Lahir", desc: "Tempat lahir almarhum", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir Almarhum", desc: "Tanggal lahir almarhum", type: "date" }, { key: "tanggal_lahir_almarhum", name: "Tanggal Lahir", desc: "Tanggal lahir almarhum", type: "date", required: true, satuan: null },
{ key: "alamat_almarhum", name: "Alamat Almarhum", desc: "Alamat terakhir almarhum", type: "text" }, { key: "alamat_almarhum", name: "Alamat", desc: "Alamat terakhir", type: "text", required: true, satuan: null },
{ {
key: "agama_almarhum", key: "agama_almarhum",
name: "Agama Almarhum", name: "Agama Almarhum",
desc: "Agama almarhum", desc: "Agama almarhum",
type: "enum", type: "enum",
options: enumAgama options: enumAgama,
required: true, satuan: null
}, },
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date" }, { key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date", required: true, satuan: null },
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text" }, { key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text", required: true, satuan: null },
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text" }, { key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text", required: true, satuan: null },
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text" } { key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skpenghasilan", id: "skpenghasilan",
name: "Surat Keterangan Penghasilan", name: "Surat Keterangan Penghasilan",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_ortu_kk", name: "KTP Orang Tua / KK", desc: "Fotokopi KTP orang tua/KK", required: true, satuan: null },
name: "Pengantar Kelian", { key: "surat_pernyataan", name: "Surat Pernyataan Penghasilan", desc: "Surat pernyataan penghasilan bermaterai", required: true, satuan: null }
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"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text" }, { key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon", desc: "Jenis kelamin",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon/orang tua", type: "text" }, { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text", required: true, satuan: null },
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number" }, { key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number", required: true, satuan: "/Bulan" },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text" } { key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "sktempatusaha", id: "sktempatusaha",
name: "Surat Keterangan Tempat Usaha", name: "Surat Keterangan Tempat Usaha",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
name: "Pengantar Kelian", { key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null },
desc: "Surat Pengantar Kelian Banjar Dinas" { key: "dokumen_lahan", name: "Dokumen Lahan", desc: "SPPT/Sertifikat/surat sewa tempat usaha", required: true, satuan: null }
},
{
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"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" }, { key: "nik", name: "NIK", desc: "NIK pemilik", type: "number", required: true, satuan: null },
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text" }, { key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik usaha", type: "text" }, { key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik", type: "text", required: true, satuan: null },
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text" }, { key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text", required: true, satuan: null },
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang atau jenis usaha", type: "text" }, { key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang usaha", type: "text", required: true, satuan: null },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" }, { key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null },
{ key: "status_tempat", name: "Status Tempat Usaha", desc: "Status kepemilikan tempat usaha", type: "enum", options: enumStatusTempatUsaha }, {
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number" }, key: "status_tempat",
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah tenaga kerja", type: "number" }, name: "Status Tempat Usaha",
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text" } desc: "Status kepemilikan tempat usaha",
type: "enum",
options: enumStatusTempatUsaha,
required: true, satuan: null
},
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number", required: true, satuan: "m²" },
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah karyawan", type: "number", required: true, satuan: null },
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "sktidakmampu", id: "sktidakmampu",
name: "Surat Keterangan Tidak Mampu", name: "Surat Keterangan Tidak Mampu",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kia_kk",
name: "KTP / KIA / KK",
desc: "Fotokopi KTP, KIA, atau Kartu Keluarga"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan pemohon", type: "number" }, { key: "nik", name: "NIK", desc: "NIK pemohon", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama lengkap pemohon", type: "text" }, { key: "nama Lengkap", name: "Nama", desc: "Nama pemohon", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal pemohon", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat pemohon", type: "text", required: true, satuan: null },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan Surat Keterangan Tidak Mampu", type: "text" } { key: "alasan", name: "Alasan Permohonan", desc: "Alasan permohonan", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skusaha", id: "skusaha",
name: "Surat Keterangan Usaha", name: "Surat Keterangan Usaha",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kk", name: "KTP / KK", desc: "Fotokopi KTP/KK", required: true, satuan: null },
name: "Pengantar Kelian", { key: "foto_lokasi", name: "Foto Lokasi Usaha", desc: "Foto lokasi usaha", required: true, satuan: null }
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"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ 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, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin pemilik usaha", desc: "Jenis kelamin pemilik usaha",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan pemilik usaha", type: "text" }, { key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan", type: "text", required: true, satuan: null },
{ {
key: "agama", key: "agama",
name: "Agama", name: "Agama",
desc: "Agama pemilik usaha", desc: "Agama",
type: "enum", type: "enum",
options: enumAgama options: enumAgama,
required: true, satuan: null
}, },
{ {
key: "status_perkawinan", key: "status_perkawinan",
name: "Status Perkawinan", name: "Status Perkawinan",
desc: "Status perkawinan", desc: "Status perkawinan",
type: "enum", type: "enum",
options: enumStatusPerkawinan options: enumStatusPerkawinan,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemilik usaha", type: "text" }, { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan", type: "text", required: true, satuan: null },
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha yang dijalankan", type: "text" }, { key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha", type: "text", required: true, satuan: null },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" } { key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat usaha", type: "text", required: true, satuan: null }
] ]
}, },
{ {
id: "skyatimpiatu", id: "skyatimpiatu",
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu", name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
syaratDokumen: [ syaratDokumen: [
{ { key: "pengantar_kelian", name: "Pengantar Kelian", desc: "Surat Pengantar Kelian Banjar Dinas", required: true, satuan: null },
key: "pengantar_kelian", { key: "ktp_kia_kk", name: "KTP / KIA / KK", desc: "Fotokopi KTP/KIA/KK", required: true, satuan: null }
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kia_kk",
name: "KTP / KIA / KK",
desc: "Fotokopi KTP, KIA, atau Kartu Keluarga"
}
], ],
dataText: [], dataText: [],
dataPelengkap: [ dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" }, { key: "nik", name: "NIK", desc: "NIK anak", type: "number", required: true, satuan: null },
{ key: "nama", name: "Nama Lengkap", desc: "Nama anak", type: "text" }, { key: "nama", name: "Nama", desc: "Nama anak", type: "text", required: true, satuan: null },
{ key: "tempat_lahir", name: "Tempat Lahir Anak", desc: "Tempat lahir anak", type: "text" }, { key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text", required: true, satuan: null },
{ key: "tanggal_lahir", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" }, { key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date", required: true, satuan: null },
{ {
key: "jenis_kelamin", key: "jenis_kelamin",
name: "Jenis Kelamin", name: "Jenis Kelamin",
desc: "Jenis kelamin anak", desc: "Jenis kelamin anak",
type: "enum", type: "enum",
options: enumJenisKelamin options: enumJenisKelamin,
required: true, satuan: null
}, },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" }, { key: "alamat", name: "Alamat", desc: "Alamat", type: "text", required: true, satuan: null },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text" }, { key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text", required: false, satuan: null },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah", type: "text", required: true, satuan: null },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text" },
{ {
key: "status_ayah", key: "status_ayah",
name: "Status Ayah", name: "Status Ayah",
desc: "Status ayah (hidup / meninggal)", desc: "Status ayah",
type: "enum", type: "enum",
options: enumStatusHidup options: enumStatusHidup,
required: true, satuan: null
}, },
{ 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, satuan: null },
{ {
key: "status_ibu", key: "status_ibu",
name: "Status Ibu", name: "Status Ibu",
desc: "Status ibu (hidup / meninggal)", desc: "Status ibu",
type: "enum", type: "enum",
options: enumStatusHidup options: enumStatusHidup,
required: true, satuan: null
} }
] ]
} }
]; ];

View File

@@ -1,12 +1,12 @@
import clientRoutes from "@/clientRoutes"; import clientRoutes from "@/clientRoutes";
import { import {
Button, Button,
Container, Center,
Group, Paper,
PasswordInput, PasswordInput,
Stack, Stack,
Text, Text,
TextInput, TextInput
} from "@mantine/core"; } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
import apiFetch from "../lib/apiFetch"; import apiFetch from "../lib/apiFetch";
@@ -73,25 +73,73 @@ export default function Login() {
}; };
return ( return (
<Container> <Center
<Stack> h="100vh"
<Text>Login</Text> style={{
<TextInput background:
placeholder="Email" "radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
value={email} }}
onChange={(e) => setEmail(e.target.value)} >
/> <Paper
<PasswordInput radius="lg"
placeholder="Password" p="xl"
value={password} w={420}
onChange={(e) => setPassword(e.target.value)} style={{
/> background: "rgba(20, 20, 20, 0.75)",
<Group justify="right"> backdropFilter: "blur(12px)",
<Button onClick={handleSubmit} disabled={loading}> 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 Login
</Button> </Button>
</Group> </Stack>
</Stack> </Paper>
</Container> </Center>
); );
} }

View File

@@ -31,7 +31,7 @@ import {
IconInfoCircle, IconInfoCircle,
IconNotes, IconNotes,
IconPhone, IconPhone,
IconUpload, IconUpload
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/id"; import "dayjs/locale/id";
@@ -42,6 +42,7 @@ import useSWR from "swr";
type DataItem = { type DataItem = {
key: string; key: string;
value: string; value: string;
required: boolean;
}; };
type FormSurat = { type FormSurat = {
@@ -52,7 +53,10 @@ type FormSurat = {
syaratDokumen: DataItem[]; syaratDokumen: DataItem[];
}; };
type ErrorState = Record<string, string | null>;
export default function FormSurat() { export default function FormSurat() {
const [errors, setErrors] = useState<ErrorState>({});
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [noPengajuan, setNoPengajuan] = useState(""); const [noPengajuan, setNoPengajuan] = useState("");
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
@@ -93,7 +97,7 @@ export default function FormSurat() {
setJenisSuratFix({ name: "", id: "" }); setJenisSuratFix({ name: "", id: "" });
} else { } else {
const namaJenis = fromSlug(jenisSurat); 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; if (!data) return;
setJenisSuratFix(data); setJenisSuratFix(data);
} }
@@ -116,15 +120,17 @@ export default function FormSurat() {
nama: "", nama: "",
phone: "", phone: "",
dataPelengkap: (get.data?.dataPelengkap || []).map( dataPelengkap: (get.data?.dataPelengkap || []).map(
(item: { key: string }) => ({ (item: { key: string, required: boolean }) => ({
key: item.key, key: item.key,
value: "", value: "",
required: item.required
}), }),
), ),
syaratDokumen: (get.data?.syaratDokumen || []).map( syaratDokumen: (get.data?.syaratDokumen || []).map(
(item: { key: string }) => ({ (item: { key: string, required: boolean }) => ({
key: item.key, key: item.key,
value: "", value: "",
required: item.required
}), }),
), ),
}); });
@@ -150,22 +156,24 @@ export default function FormSurat() {
}, [jenisSuratFix.id]); }, [jenisSuratFix.id]);
function onChecking() { 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) => { const isFormKosong = Object.values(formSurat).some((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return ( return value.some(
value.length === 0 || (item) =>
value.some( (typeof item.value === "string" && item.value.trim() === "" && item.required) || (typeof item.value === "object" && item.value === null && item.required),
(item) =>
typeof item.value === "string" && item.value.trim() === "",
)
); );
} }
return typeof value === "string" && value.trim() === "";
if (typeof value === "string") {
return value.trim() === "";
}
return false;
}); });
if (isFormKosong) { if (isFormKosong) {
@@ -174,9 +182,9 @@ export default function FormSurat() {
message: "Silahkan lengkapi form surat", message: "Silahkan lengkapi form surat",
type: "error", type: "error",
}); });
} else {
open();
} }
open();
} }
async function onSubmit() { 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({ function validationForm({
key, key,
value, value,
@@ -246,12 +278,28 @@ export default function FormSurat() {
key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen"; key: "nama" | "phone" | "dataPelengkap" | "syaratDokumen";
value: any; 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) => ({ setFormSurat((prev) => ({
...prev, ...prev,
[key]: updateArrayByKey(prev[key], value.key, value.value), [key]: updateArrayByKey(prev[key], value.key, value.value),
})); }));
} else { } else {
const keyFix = key == "nama" ? "nama_kontak" : key;
const errorMsg = validateField(keyFix, value);
setErrors((prev) => ({
...prev,
[keyFix]: errorMsg,
}));
setFormSurat({ setFormSurat({
...formSurat, ...formSurat,
[key]: value, [key]: value,
@@ -259,6 +307,7 @@ export default function FormSurat() {
} }
} }
return ( return (
<Container size="md" w={"100%"} pb={"lg"}> <Container size="md" w={"100%"} pb={"lg"}>
<Modal <Modal
@@ -324,6 +373,7 @@ export default function FormSurat() {
<Grid> <Grid>
<Grid.Col span={12}> <Grid.Col span={12}>
<Select <Select
allowDeselect={false}
label={ label={
<FieldLabel <FieldLabel
label="Jenis Surat" label="Jenis Surat"
@@ -335,7 +385,7 @@ export default function FormSurat() {
value: item.name, value: item.name,
label: item.name, label: item.name,
}))} }))}
value={jenisSuratFix.name} value={jenisSuratFix.name == "" ? null : jenisSuratFix.name}
onChange={(value) => { onChange={(value) => {
const slug = toSlug(String(value)); const slug = toSlug(String(value));
navigate("/darmasaba/surat?jenis=" + slug); navigate("/darmasaba/surat?jenis=" + slug);
@@ -354,9 +404,10 @@ export default function FormSurat() {
<Grid> <Grid>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
label={<FieldLabel label="Nama" hint="Nama kontak" />} label={<FieldLabel label="Nama" hint="Nama kontak" required />}
placeholder="Budi Setiawan" placeholder="Budi Setiawan"
value={formSurat.nama} value={formSurat.nama}
error={errors.nama_kontak}
onChange={(e) => onChange={(e) =>
validationForm({ key: "nama", value: e.target.value }) validationForm({ key: "nama", value: e.target.value })
} }
@@ -367,12 +418,15 @@ export default function FormSurat() {
<TextInput <TextInput
label={ label={
<FieldLabel <FieldLabel
required
label="Nomor Telephone" label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp" hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/> />
} }
placeholder="08123456789" placeholder="08123456789"
value={formSurat.phone} value={formSurat.phone}
error={errors.phone}
type="number"
onChange={(e) => onChange={(e) =>
validationForm({ key: "phone", value: e.target.value }) validationForm({ key: "phone", value: e.target.value })
} }
@@ -396,10 +450,12 @@ export default function FormSurat() {
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
{item.type == "enum" ? ( {item.type == "enum" ? (
<Select <Select
allowDeselect={false}
label={ label={
<FieldLabel <FieldLabel
label={item.name} label={item.name}
hint={item.desc} hint={item.desc}
required={item.required}
/> />
} }
data={item.options ?? []} data={item.options ?? []}
@@ -407,7 +463,7 @@ export default function FormSurat() {
onChange={(e) => { onChange={(e) => {
validationForm({ validationForm({
key: "dataPelengkap", key: "dataPelengkap",
value: { key: item.key, value: e }, value: { key: item.key, value: e, required: item.required },
}); });
}} }}
value={ value={
@@ -424,31 +480,35 @@ export default function FormSurat() {
<FieldLabel <FieldLabel
label={item.name} label={item.name}
hint={item.desc} hint={item.desc}
required={item.required}
/> />
} }
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
const formatted = e const formatted = e
? dayjs(e) ? dayjs(e)
.locale("id") .locale("id")
.format("DD MMMM YYYY") .format("DD MMMM YYYY")
: ""; : "";
validationForm({ validationForm({
key: "dataPelengkap", key: "dataPelengkap",
value: { value: {
key: item.key, key: item.key,
value: formatted, value: formatted,
required: item.required,
}, },
}); });
}} }}
/> />
) : ( ) : (
<TextInput <TextInput
error={errors[item.key]}
type={item.type} type={item.type}
label={ label={
<FieldLabel <FieldLabel
label={item.name} label={item.name}
hint={item.desc} hint={item.desc}
required={item.required}
/> />
} }
placeholder={item.name} placeholder={item.name}
@@ -458,6 +518,7 @@ export default function FormSurat() {
value: { value: {
key: item.key, key: item.key,
value: e.target.value, value: e.target.value,
required: item.required,
}, },
}) })
} }
@@ -466,6 +527,10 @@ export default function FormSurat() {
(n: any) => n.key == item.key, (n: any) => n.key == item.key,
)?.value )?.value
} }
rightSection={
item.satuan != null &&
<Text mr={"lg"}>{item.satuan}</Text>
}
/> />
)} )}
</Grid.Col> </Grid.Col>
@@ -494,6 +559,7 @@ export default function FormSurat() {
}) })
} }
name={item.name} name={item.name}
required={item.required}
/> />
</Grid.Col> </Grid.Col>
), ),
@@ -518,10 +584,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 ( return (
<Group justify="apart" gap="xs" align="center"> <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 && ( {hint && (
<Tooltip label={hint} withArrow> <Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle"> <ActionIcon size={24} variant="subtle">
@@ -533,6 +609,7 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
); );
} }
function FormSection({ function FormSection({
title, title,
icon, icon,
@@ -568,6 +645,7 @@ function FileInputWrapper({
preview, preview,
name, name,
description, description,
required = false,
}: { }: {
label: string; label: string;
placeholder?: string; placeholder?: string;
@@ -576,12 +654,20 @@ function FileInputWrapper({
preview?: string | null; preview?: string | null;
name: string; name: string;
description?: string; description?: string;
required?: boolean;
}) { }) {
return ( return (
<Stack gap="xs"> <Stack gap="xs">
<Flex direction={"column"}> <Flex direction={"column"}>
<Group justify="apart" align="center"> <Group justify="apart" align="center">
<Text fw={500}>{label}</Text> <Text fw={500}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group> </Group>
{description && ( {description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}> <Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
@@ -597,6 +683,7 @@ function FileInputWrapper({
leftSection={<IconUpload />} leftSection={<IconUpload />}
aria-label={label} aria-label={label}
name={name} name={name}
clearable={true}
/> />
{preview ? ( {preview ? (

View File

@@ -50,6 +50,7 @@ type UpdateDataItem = {
id: string; id: string;
key: string; key: string;
value: any; value: any;
required: boolean;
}; };
type FormSurat = { 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 ( return (
<Group justify="apart" gap="xs" align="center"> <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 && ( {hint && (
<Tooltip label={hint} withArrow> <Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle"> <ActionIcon size={24} variant="subtle">
@@ -139,7 +150,6 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
</Group> </Group>
); );
} }
function FormSection({ function FormSection({
title, title,
icon, icon,
@@ -185,6 +195,8 @@ function FileInputWrapper({
description, description,
linkView, linkView,
disabled, disabled,
required = false,
}: { }: {
label: string; label: string;
placeholder?: string; placeholder?: string;
@@ -195,6 +207,7 @@ function FileInputWrapper({
name: string; name: string;
description?: string; description?: string;
disabled?: boolean; disabled?: boolean;
required?: boolean;
}) { }) {
const [viewImg, setViewImg] = useState(""); const [viewImg, setViewImg] = useState("");
const [openedPreviewFile, setOpenedPreviewFile] = useState(false); const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
@@ -219,7 +232,14 @@ function FileInputWrapper({
<Stack gap="xs"> <Stack gap="xs">
<Flex direction={"column"}> <Flex direction={"column"}>
<Group justify="apart" align="center"> <Group justify="apart" align="center">
<Text fw={500}>{label}</Text> <Text fw={500}>
{label}
{required && (
<Text span c="red" ml={4}>
*
</Text>
)}
</Text>
</Group> </Group>
{description && ( {description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}> <Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
@@ -241,6 +261,7 @@ function FileInputWrapper({
aria-label={label} aria-label={label}
name={name} name={name}
disabled={disabled} disabled={disabled}
clearable={true}
/> />
{preview ? ( {preview ? (
@@ -317,57 +338,60 @@ function SearchData() {
} }
return ( return (
<FormSection <>
title="Cari Pengajuan Surat" <FullScreenLoading visible={submitLoading} text="Mencari Data" />
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat." <FormSection
> title="Cari Pengajuan Surat"
<Grid> info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
<Grid.Col span={6}> >
<TextInput <Grid>
label={ <Grid.Col span={6}>
<FieldLabel <TextInput
label="Nomor Pengajuan" label={
hint="Nomor pengajuan surat" <FieldLabel
/> label="Nomor Pengajuan"
} hint="Nomor pengajuan surat"
placeholder="PS-2025-000123" />
onChange={(e) => { }
setSearchPengajuan(e.target.value); placeholder="PS-2025-000123"
}} onChange={(e) => {
/> setSearchPengajuan(e.target.value);
</Grid.Col> }}
/>
</Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
label={ label={
<FieldLabel <FieldLabel
label="Nomor Telephone" label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp" hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/> />
} }
placeholder="08123456789" placeholder="08123456789"
type="number" type="number"
onChange={(e) => { onChange={(e) => {
setSearchPengajuanPhone(e.target.value); setSearchPengajuanPhone(e.target.value);
}} }}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
<Button <Button
fullWidth fullWidth
variant="light" variant="light"
color="blue" color="blue"
onClick={() => { onClick={() => {
handleSearch(); handleSearch();
}} }}
loading={submitLoading} loading={submitLoading}
> >
Cari Pengajuan Cari Pengajuan
</Button> </Button>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</FormSection> </FormSection>
</>
); );
} }
@@ -380,12 +404,14 @@ function DataUpdate({
}) { }) {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const navigate = useNavigate(); const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string | null>>({});
const [sukses, setSukses] = useState(false); const [sukses, setSukses] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false); const [submitLoading, setSubmitLoading] = useState(false);
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]); const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]); const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({}); const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [loadingFetchData, setLoadingFetchData] = useState(false);
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({ const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
dataPelengkap: [], dataPelengkap: [],
syaratDokumen: [], syaratDokumen: [],
@@ -393,6 +419,7 @@ function DataUpdate({
async function fetchData() { async function fetchData() {
try { try {
setLoadingFetchData(true);
const res = await apiFetch.api.pelayanan["detail-data"].post({ const res = await apiFetch.api.pelayanan["detail-data"].post({
nomerPengajuan: noPengajuan, nomerPengajuan: noPengajuan,
}); });
@@ -420,6 +447,8 @@ function DataUpdate({
} }
} catch (error) { } catch (error) {
console.error("Error fetching data:", error); console.error("Error fetching data:", error);
} finally {
setLoadingFetchData(false);
} }
} }
@@ -427,6 +456,29 @@ function DataUpdate({
fetchData(); 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[] { function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
const index = array.findIndex((v) => v.id === item.id); const index = array.findIndex((v) => v.id === item.id);
@@ -446,16 +498,36 @@ function DataUpdate({
kategori: "dataPelengkap" | "syaratDokumen"; kategori: "dataPelengkap" | "syaratDokumen";
value: UpdateDataItem; value: UpdateDataItem;
}) { }) {
setFormSurat((prev) => ({ if (kategori == "syaratDokumen" && value.value == null) {
...prev, setFormSurat((prev) => ({
[kategori]: upsertById(prev[kategori], { ...prev,
id: value.id, syaratDokumen: prev.syaratDokumen.filter(
key: value.key, (item) => item.id !== value.id
value: value.value, ),
}), }));
})); } 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( function updateArrayByKey(
list: UpdateDataItem[], list: UpdateDataItem[],
id: string, id: string,
@@ -465,6 +537,16 @@ function DataUpdate({
} }
function onChecking() { 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 ( if (
formSurat.dataPelengkap.length == 0 && formSurat.dataPelengkap.length == 0 &&
formSurat.syaratDokumen.length == 0 formSurat.syaratDokumen.length == 0
@@ -479,7 +561,7 @@ function DataUpdate({
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.some( return value.some(
(item) => (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 ( return (
<> <>
<FullScreenLoading visible={submitLoading} /> <FullScreenLoading visible={submitLoading || loadingFetchData} />
<Modal <Modal
opened={opened} opened={opened}
onClose={close} onClose={close}
@@ -625,13 +707,15 @@ function DataUpdate({
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
{item.type == "enum" ? ( {item.type == "enum" ? (
<Select <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 ?? []} data={item.options ?? []}
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
validationForm({ validationForm({
kategori: "dataPelengkap", kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: e }, value: { id: item.id, key: item.key, value: e, required: item.required },
}); });
}} }}
value={ value={
@@ -643,9 +727,10 @@ function DataUpdate({
/> />
) : item.type == "date" ? ( ) : item.type == "date" ? (
<DateInput <DateInput
disabled={status != "ditolak" && status != "antrian"}
locale="id" locale="id"
valueFormat="DD MMMM YYYY" 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} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
const formatted = e const formatted = e
@@ -657,6 +742,7 @@ function DataUpdate({
id: item.id, id: item.id,
key: item.key, key: item.key,
value: formatted, value: formatted,
required: item.required
}, },
}); });
}} }}
@@ -674,8 +760,10 @@ function DataUpdate({
/> />
) : ( ) : (
<TextInput <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} placeholder={item.name}
type={item.type}
onChange={(e) => onChange={(e) =>
validationForm({ validationForm({
kategori: "dataPelengkap", kategori: "dataPelengkap",
@@ -683,6 +771,7 @@ function DataUpdate({
id: item.id, id: item.id,
key: item.key, key: item.key,
value: e.target.value, value: e.target.value,
required: item.required,
}, },
}) })
} }
@@ -692,6 +781,10 @@ function DataUpdate({
dataPelengkap.find((n: any) => n.key == item.key)?.value dataPelengkap.find((n: any) => n.key == item.key)?.value
} }
disabled={status != "ditolak" && status != "antrian"} disabled={status != "ditolak" && status != "antrian"}
rightSection={
item.satuan != null &&
<Text mr={"lg"}>{item.satuan}</Text>
}
/> />
)} )}
</Grid.Col> </Grid.Col>
@@ -700,7 +793,7 @@ function DataUpdate({
</FormSection> </FormSection>
<FormSection <FormSection
title="Syarat Dokumen" title="Syarat Dokumen hjh"
description="Syarat dokumen yang diperlukan" description="Syarat dokumen yang diperlukan"
icon={<IconFiles size={16} />} icon={<IconFiles size={16} />}
> >
@@ -708,6 +801,7 @@ function DataUpdate({
{dataSyaratDokumen.map((item: any, index: number) => ( {dataSyaratDokumen.map((item: any, index: number) => (
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
<FileInputWrapper <FileInputWrapper
required={item.required}
label={item.desc} label={item.desc}
placeholder={"Upload file terbaru untuk mengupdate"} placeholder={"Upload file terbaru untuk mengupdate"}
accept="image/*,application/pdf" accept="image/*,application/pdf"
@@ -715,7 +809,7 @@ function DataUpdate({
onChange={(file) => onChange={(file) =>
validationForm({ validationForm({
kategori: "syaratDokumen", 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} name={item.name}

View File

@@ -73,7 +73,7 @@ export default function DashboardLayout() {
<AppShell <AppShell
padding="lg" padding="lg"
navbar={{ navbar={{
width: 260, width: 300,
breakpoint: "sm", breakpoint: "sm",
collapsed: { mobile: !opened, desktop: !opened }, collapsed: { mobile: !opened, desktop: !opened },
}} }}
@@ -290,22 +290,31 @@ function NavigationDashboard() {
.map((item) => ( .map((item) => (
<NavLink <NavLink
key={item.path} 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} leftSection={item.icon}
label={ label={
<Flex align="center" gap={6}> <Flex align="center" gap={6}>
<Text fw={500}>{item.label}</Text> <Text fw={500}>{item.label}</Text>
{isActive(item.path as keyof typeof clientRoute) && ( {(
<Badge isActive(item.path as keyof typeof clientRoute) ||
variant="light" (location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
color="teal" (location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
radius="sm" (location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
size="xs" )
style={{ textTransform: "none" }} && (
> <Badge
Active variant="light"
</Badge> color="teal"
)} radius="sm"
size="xs"
style={{ textTransform: "none" }}
>
Active
</Badge>
)}
</Flex> </Flex>
} }
description={item.description} description={item.description}
@@ -313,7 +322,10 @@ function NavigationDashboard() {
navigate(clientRoutes[item.path as keyof typeof clientRoute]) navigate(clientRoutes[item.path as keyof typeof clientRoute])
} }
style={{ 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)" ? "rgba(0,255,200,0.1)"
: "transparent", : "transparent",
borderRadius: "8px", borderRadius: "8px",

View File

@@ -1,8 +1,12 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile"; import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat"; import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal"; import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import { import {
ActionIcon,
Anchor, Anchor,
Badge, Badge,
Button, Button,
@@ -14,24 +18,31 @@ import {
Group, Group,
List, List,
Modal, Modal,
Select,
Spoiler, Spoiler,
Stack, Stack,
Table, Table,
Text, Text,
Textarea, Textarea,
TextInput,
ThemeIcon, ThemeIcon,
Title, Title,
Tooltip
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { import {
IconAlignJustified, IconAlignJustified,
IconCheck, IconCheck,
IconEdit,
IconFileCertificate, IconFileCertificate,
IconFileCheck, IconFileCheck,
IconInfoCircle,
IconMessageReport, IconMessageReport,
IconPhone, IconPhone,
IconUser, IconUser,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import dayjs from "dayjs";
import type { User } from "generated/prisma"; import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library"; import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash"; import _ from "lodash";
@@ -40,6 +51,11 @@ import { useLocation } from "react-router-dom";
import useSwr from "swr"; import useSwr from "swr";
export default function DetailPengajuanPage() { 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 { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const id = query.get("id"); const id = query.get("id");
@@ -58,6 +74,9 @@ export default function DetailPengajuanPage() {
return ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengajuan <DetailDataPengajuan
@@ -93,6 +112,7 @@ function DetailDataPengajuan({
dataText: any; dataText: any;
onAction: () => void; onAction: () => void;
}) { }) {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak"); const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [keterangan, setKeterangan] = useState(""); const [keterangan, setKeterangan] = useState("");
@@ -101,8 +121,12 @@ function DetailDataPengajuan({
const [openedPreview, setOpenedPreview] = useState(false); const [openedPreview, setOpenedPreview] = useState(false);
const [openedPreviewFile, setOpenedPreviewFile] = useState(false); const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState(""); const [viewImg, setViewImg] = useState({ file: "", folder: "" });
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState({ ok: false, file: "" });
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", satuan: null as string | null, option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
const [loadingFS, setLoadingFS] = useState({ value: false, text: "" })
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -121,6 +145,7 @@ function DetailDataPengajuan({
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) { async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try { try {
setLoadingFS({ value: true, text: "Sending message to warga" })
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({ const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "", noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "", jenisSurat: data?.category ?? "",
@@ -138,6 +163,11 @@ function DetailDataPengajuan({
message: "Success send message to warga", message: "Success send message to warga",
type: "success", type: "success",
}); });
if (status == "selesai") {
onAction()
}
} else { } else {
notification({ notification({
title: "Failed", title: "Failed",
@@ -159,10 +189,14 @@ function DetailDataPengajuan({
type: "error", type: "error",
}); });
} }
finally {
setLoadingFS({ value: false, text: "" })
}
} }
const handleKonfirmasi = async (cat: "terima" | "tolak") => { const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try { try {
setLoadingFS({ value: true, text: "Updating status" })
const statusFix = cat == "tolak" const statusFix = cat == "tolak"
? "ditolak" ? "ditolak"
: data.status == "antrian" : data.status == "antrian"
@@ -212,9 +246,48 @@ function DetailDataPengajuan({
message: "Failed to update pengajuan surat", message: "Failed to update pengajuan surat",
type: "error", 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(() => { useShallowEffect(() => {
if (viewImg) { if (viewImg) {
setOpenedPreviewFile(true); setOpenedPreviewFile(true);
@@ -222,25 +295,109 @@ function DetailDataPengajuan({
}, [viewImg]); }, [viewImg]);
useShallowEffect(() => { useShallowEffect(() => {
if (uploading) { if (uploading.ok && uploading.file) {
sendWA({ sendWA({
status: "selesai", status: "selesai",
linkSurat: "", linkSurat: uploading.file,
linkUpdate: "", linkUpdate: "",
}); });
} }
}, [uploading]); }, [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 ( return (
<> <>
<FullScreenLoading visible={loadingFS.value} text={loadingFS.text} />
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
onClose={() => setOpenEdit(false)}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{editValue.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={editValue.jenis} />}
data={editValue.option ?? []}
placeholder={editValue.jenis}
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
value={editValue.val}
/>
) : editValue.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
setEditValue({
...editValue,
val: formatted
})
}}
value={
editValue.val
? parseTanggalID(editValue.val)
: parseTanggalID(editValue.val)
}
/>
) : (
<TextInput
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
type={editValue.type}
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
value={editValue.val}
rightSection={
editValue.satuan != null &&
<Text mr={"lg"}>{editValue.satuan}</Text>
}
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
Batal
</Button>
<Button
variant="filled"
onClick={updateDataText}
disabled={loadingUpdate || !editValue.val}
loading={loadingUpdate}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<ModalFile <ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)} open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => { onClose={() => {
setOpenedPreviewFile(false); setOpenedPreviewFile(false);
setViewImg({ file: "", folder: "" })
}} }}
folder="syarat-dokumen" folder={viewImg.folder}
fileName={viewImg} fileName={viewImg.file}
/> />
{/* MODAL KONFIRMASI */} {/* MODAL KONFIRMASI */}
@@ -312,12 +469,14 @@ function DetailDataPengajuan({
)} )}
</Stack> </Stack>
</Modal> </Modal>
{data?.status == "selesai" && (
{/* MODAL PREVIEW SURAT */}
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat <ModalSurat
open={openedPreview} open={openedPreview}
onClose={() => { onClose={(val) => {
setOpenedPreview(false) setOpenedPreview(false)
setUploading(true) setUploading({ ok: val.success, file: val.data })
}} }}
surat={data?.idSurat} surat={data?.idSurat}
/> />
@@ -386,7 +545,7 @@ function DetailDataPengajuan({
<List.Item key={v.id}> <List.Item key={v.id}>
<Anchor <Anchor
onClick={() => { onClick={() => {
setViewImg(v.value); setViewImg({ file: v.value, folder: "syarat-dokumen" });
}} }}
> >
{v.jenis} {v.jenis}
@@ -413,7 +572,25 @@ function DetailDataPengajuan({
</Table.Td> </Table.Td>
<Table.Td>:</Table.Td> <Table.Td>:</Table.Td>
<Table.Td style={{ width: "85%" }}> <Table.Td style={{ width: "85%" }}>
{_.upperFirst(item.value)} <Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Text>
{_.upperFirst(item.value)} {item.satuan}
</Text>
<ActionIcon
variant="subtle"
aria-label="Edit"
onClick={() => {
setEditValue({ id: item.id, val: item.value, type: item.type, satuan: item.satuan, option: item.options, jenis: item.jenis, key: item.key })
setOpenEdit(true)
}}>
<IconEdit size={16} />
</ActionIcon>
</Flex>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
))} ))}
@@ -471,18 +648,31 @@ function DetailDataPengajuan({
Setujui Setujui
</Button> </Button>
</Group> </Group>
) : data?.status === "selesai" ? ( ) : data?.status === "selesai" ?
<Group justify="center" grow> !data?.fileSurat ?
{/* <Button (
variant="light" <Group justify="center" grow>
onClick={() => setOpenedPreview(!openedPreview)} <Button
> variant="light"
Surat onClick={() => { setOpenedPreview(true) }}
</Button> */} >
</Group> 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.Col>
</Grid> </Grid>
</Stack> </Stack>

View File

@@ -1,3 +1,4 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import ModalFile from "@/components/ModalFile"; import ModalFile from "@/components/ModalFile";
import notification from "@/components/notificationGlobal"; import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
@@ -40,6 +41,11 @@ import useSwr from "swr";
export default function DetailPengaduanPage() { 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 { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const id = query.get("id"); const id = query.get("id");
@@ -58,6 +64,9 @@ export default function DetailPengaduanPage() {
return ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengaduan <DetailDataPengaduan
@@ -93,6 +102,7 @@ function DetailDataPengaduan({
const [keterangan, setKeterangan] = useState(""); const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null); const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]); const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
async function fetchHost() { async function fetchHost() {
@@ -111,6 +121,7 @@ function DetailDataPengaduan({
const handleKonfirmasi = async (cat: "terima" | "tolak") => { const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try { try {
setIsLoading(true);
const res = await apiFetch.api.pengaduan["update-status"].post({ const res = await apiFetch.api.pengaduan["update-status"].post({
id: data?.id, id: data?.id,
status: status:
@@ -184,6 +195,8 @@ function DetailDataPengaduan({
message: "Failed to update pengaduan", message: "Failed to update pengaduan",
type: "error", type: "error",
}); });
} finally {
setIsLoading(false);
} }
}; };
@@ -218,6 +231,7 @@ function DetailDataPengaduan({
color="red" color="red"
disabled={keterangan.length < 1} disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")} onClick={() => handleKonfirmasi("tolak")}
loading={isLoading}
> >
Tolak Tolak
</Button> </Button>
@@ -242,6 +256,7 @@ function DetailDataPengaduan({
variant="filled" variant="filled"
color="green" color="green"
onClick={() => handleKonfirmasi("terima")} onClick={() => handleKonfirmasi("terima")}
loading={isLoading}
> >
Ya Ya
</Button> </Button>

View File

@@ -1,3 +1,4 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import DesaSetting from "@/components/DesaSetting"; import DesaSetting from "@/components/DesaSetting";
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat"; import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
import KategoriPengaduan from "@/components/KategoriPengaduan"; import KategoriPengaduan from "@/components/KategoriPengaduan";
@@ -15,14 +16,21 @@ import {
IconUsersGroup, IconUsersGroup,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library"; import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
export default function DetailSettingPage() { export default function DetailSettingPage() {
const { search } = useLocation(); const { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const type = query.get("type"); const type = query.get("type");
const navigate = useNavigate();
const [permissions, setPermissions] = useState<JsonValue[]>([]); 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(() => { useEffect(() => {
async function fetchPermissions() { async function fetchPermissions() {
@@ -87,6 +95,9 @@ export default function DetailSettingPage() {
return ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={3}> <Grid.Col span={3}>
<Card <Card
radius="md" radius="md"
@@ -104,7 +115,7 @@ export default function DetailSettingPage() {
.map((item) => ( .map((item) => (
<NavLink <NavLink
key={item.key} key={item.key}
href={"?type=" + item.path} onClick={()=>{navigate("?type=" + item.path)}}
label={item.label} label={item.label}
leftSection={item.icon} leftSection={item.icon}
active={ active={

View File

@@ -1,62 +1,75 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
Avatar, Avatar,
Box, Box,
Button, Button,
Card, Card,
CloseButton,
Container, Container,
Divider, Divider,
Flex, Flex,
Grid, Grid,
Group, Group,
Input,
LoadingOverlay, LoadingOverlay,
Pagination,
Stack, Stack,
Table, Table,
Text, Text,
Title, Title,
} from "@mantine/core"; } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconPhone } from "@tabler/icons-react"; import { IconPhone, IconSearch } from "@tabler/icons-react";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() { 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 { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
const id = query.get("id"); const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () => // const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.warga.detail.get({ // apiFetch.api.warga.detail.get({
query: { // query: {
id: id!, // id: id!,
}, // },
}), // }),
); // );
useShallowEffect(() => { // useShallowEffect(() => {
mutate(); // mutate();
}, []); // }, []);
return ( return (
<> <>
<LoadingOverlay <LoadingOverlay
visible={isLoading} // visible={isLoading}
zIndex={1000} zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }} overlayProps={{ radius: "sm", blur: 2 }}
/> />
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={4}> <Grid.Col span={4}>
<DetailWarga data={data?.data?.warga} /> <DetailWarga id={id!} />
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataHistori <DetailDataHistori
data={data?.data?.pengaduan} id={id!}
kategori="pengaduan" kategori="pengaduan"
/> />
<DetailDataHistori <DetailDataHistori
data={data?.data?.pelayanan} id={id!}
kategori="pelayanan" kategori="pelayanan"
/> />
</Stack> </Stack>
@@ -68,13 +81,66 @@ export default function DetailWargaPage() {
} }
function DetailDataHistori({ function DetailDataHistori({
data, id,
kategori, kategori,
}: { }: {
data: any; id: string;
kategori: "pengaduan" | "pelayanan"; kategori: "pengaduan" | "pelayanan";
}) { }) {
const navigate = useNavigate(); 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 ( return (
<Card <Card
@@ -93,6 +159,36 @@ function DetailDataHistori({
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
Histori {_.upperFirst(kategori)} Histori {_.upperFirst(kategori)}
</Title> </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> </Flex>
<Divider my={0} /> <Divider my={0} />
<Table> <Table>
@@ -110,7 +206,7 @@ function DetailDataHistori({
{data?.length > 0 ? ( {data?.length > 0 ? (
data?.map((item: any, index: number) => ( data?.map((item: any, index: number) => (
<Table.Tr key={index}> <Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td> <Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
<Table.Td> <Table.Td>
{kategori == "pengaduan" ? item.title : item.category} {kategori == "pengaduan" ? item.title : item.category}
</Table.Td> </Table.Td>
@@ -121,11 +217,11 @@ function DetailDataHistori({
onClick={() => { onClick={() => {
kategori == "pengaduan" kategori == "pengaduan"
? navigate( ? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`, `/scr/dashboard/pengaduan/detail?id=${item.id}`,
) )
: navigate( : navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`, `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
); );
}} }}
> >
Detail 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 ( return (
<Card <Card
radius="md" radius="md"

View File

@@ -248,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string)
return `✏️ Renamed ${oldName}${newName}` return `✏️ Renamed ${oldName}${newName}`
} }
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> { export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
const localName = localFile || remoteFile; const localName = localFile || fileName;
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`); // 🔹 gabungkan path folder + file
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, ''); 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()); const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
await fs.writeFile(localName, buffer); await fs.writeFile(localName, buffer);
return `⬇️ Downloaded ${remoteFile}${localName}` return `⬇️ Downloaded ${fileName}${localName}`
} }
export async function getFileLink(config: Config, fileName: string): Promise<string> { export async function getFileLink(config: Config, fileName: string): Promise<string> {

View File

@@ -265,6 +265,7 @@ const PelayananRoute = new Elysia({
select: { select: {
id: true, id: true,
idCategory: true, idCategory: true,
file: true
} }
}) })
@@ -307,9 +308,14 @@ const PelayananRoute = new Elysia({
}) })
const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as { const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
name: string; type: string;
options?: {
label: string,
value: string
}[]; name: string;
desc: string; desc: string;
key: string; key: string;
satuan?: string;
}[]; }[];
const refMap = new Map( const refMap = new Map(
@@ -326,8 +332,12 @@ const PelayananRoute = new Elysia({
return { return {
id: item.id, id: item.id,
jenis: nama, jenis: nama,
key: ref?.key,
value: item.value, value: item.value,
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity, order: ref?.order ?? Infinity,
satuan: ref?.satuan ?? null
}; };
}) })
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
@@ -381,6 +391,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt, createdAt: data?.createdAt,
updatedAt: data?.updatedAt, updatedAt: data?.updatedAt,
idSurat: dataSurat?.id, idSurat: dataSurat?.id,
fileSurat: dataSurat?.file,
} }
const datafix = { const datafix = {
@@ -676,17 +687,20 @@ const PelayananRoute = new Elysia({
name: string; name: string;
desc: string; desc: string;
key: string; key: string;
required: boolean;
}[]; }[];
const dataSyaratFix = dataSyarat.map((item) => { const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
const required = syaratDokumen.find((v) => v.key == item.jenis)?.required
return { return {
id: item.id, id: item.id,
key: item.jenis, key: item.jenis,
value: item.value, value: item.value,
name: name ?? '', name: name ?? '',
desc: desc ?? '' desc: desc ?? '',
required: required ?? true
} }
}) })
@@ -699,6 +713,8 @@ const PelayananRoute = new Elysia({
name: string; name: string;
desc: string; desc: string;
key: string; key: string;
required: boolean;
satuan?: string;
}[]; }[];
const refMap = new Map( const refMap = new Map(
@@ -721,6 +737,8 @@ const PelayananRoute = new Elysia({
type: ref?.type ?? "", type: ref?.type ?? "",
options: ref?.options ?? [], options: ref?.options ?? [],
order: ref?.order ?? Infinity, order: ref?.order ?? Infinity,
required: ref?.required ?? true,
satuan: ref?.satuan ?? null
}; };
}) })
.sort((a, b) => a.order - b.order) .sort((a, b) => a.order - b.order)
@@ -1048,6 +1066,73 @@ const PelayananRoute = new Elysia({
description: `tool untuk update data pengajuan pelayanan surat`, 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 }) => { .get("/list", async ({ query }) => {
const { take, page, search, status } = query const { take, page, search, status } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take)) const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))

View File

@@ -1,14 +1,16 @@
import Elysia, { t } from "elysia" import Elysia, { t } from "elysia";
import type { StatusPengaduan } from "generated/prisma" import fs from 'fs';
import _ from "lodash" import type { StatusPengaduan } from "generated/prisma";
import { v4 as uuidv4 } from "uuid" import _ from "lodash";
import { getLastUpdated } from "../lib/get-last-updated" import path from "path";
import { mimeToExtension } from "../lib/mimetypeToExtension" import { v4 as uuidv4 } from "uuid";
import { generateNoPengaduan } from "../lib/no-pengaduan" import { getLastUpdated } from "../lib/get-last-updated";
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone" import { mimeToExtension } from "../lib/mimetypeToExtension";
import { prisma } from "../lib/prisma" import { generateNoPengaduan } from "../lib/no-pengaduan";
import { renameFile } from "../lib/rename-file" import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone";
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile" 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({ const PengaduanRoute = new Elysia({
prefix: "pengaduan", prefix: "pengaduan",
@@ -605,6 +607,43 @@ const PengaduanRoute = new Elysia({
consumes: ["multipart/form-data"] 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 }) => { .post("/upload-file-form-data", async ({ body }) => {
const { file } = body; const { file } = body;

View File

@@ -17,6 +17,7 @@ const SuratRoute = new Elysia({
noSurat: true, noSurat: true,
idCategory: true, idCategory: true,
createdAt: true, createdAt: true,
file: true,
PelayananAjuan: { PelayananAjuan: {
select: { select: {
DataTextPelayanan: true, DataTextPelayanan: true,
@@ -44,6 +45,7 @@ const SuratRoute = new Elysia({
idCategory: dataSurat?.idCategory, idCategory: dataSurat?.idCategory,
nameCategory: dataSurat?.CategoryPelayanan?.name, nameCategory: dataSurat?.CategoryPelayanan?.name,
noSurat: dataSurat?.noSurat, noSurat: dataSurat?.noSurat,
file: dataSurat?.file,
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan, dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }), 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 export default SuratRoute

View File

@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
} }
}) })
.get("/detail", async ({ query }) => { .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({ const dataWarga = await prisma.warga.findUnique({
where: { where: {
id id
} }
}) })
const dataPengaduan = await prisma.pengaduan.findMany({ if (!dataWarga)
orderBy: { return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
createdAt: "desc"
}, if (category == "warga") {
where: { return dataWarga
} else if (category == "pengaduan") {
const where: any = {
isActive: true, isActive: true,
idWarga: id idWarga: id,
}, OR: [
select: { {
id: true, title: {
status: true, contains: search ?? "",
noPengaduan: true, mode: "insensitive"
title: true },
},
{
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({ return dataReturn
orderBy: { } else if (category == "pelayanan") {
createdAt: "desc" const where: any = {
},
where: {
isActive: true, isActive: true,
idWarga: id idWarga: id,
}, OR: [
select: { {
id: true, CategoryPelayanan: {
noPengajuan: true, name: {
status: true, contains: search ?? "",
CategoryPelayanan: { mode: "insensitive"
select: { },
name: true },
},
{
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) => ({ return dataReturn
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
return {
warga: dataWarga,
pengaduan: dataPengaduan,
pelayanan: dataPelayanFix
} }
}, { }, {
query: t.Object({ 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: { detail: {
summary: "Detail Warga", summary: "Detail Warga",