Compare commits

...

43 Commits

Author SHA1 Message Date
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
75758bcbe6 upd: pengajuan surat
Deskripsi:
- send wa penolakan + lik update
- send wa diterima
- upload ke seafile
- blm selesai ngirim link surat ke wa

No Issues
2026-01-05 17:24:53 +08:00
2d336ea467 Merge pull request 'amalia/02-jan-26' (#99) from amalia/02-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/99
2026-01-02 17:32:32 +08:00
112e931bad upd: form update pengajuan surat
Deskripsi:
- tampilan saat ada status ditolak

NO Issues
2026-01-02 17:31:56 +08:00
487395bdb3 upd: notif warga
Deskripsi:
- tolak pengaduan
- terima pengaduan
- kerjakan pengaduan
- pengaduan selesai

NO Issues
2026-01-02 14:52:00 +08:00
3944e1ee82 Merge pull request 'upd: seeder' (#98) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/98
2025-12-29 11:33:00 +08:00
a9b34547f0 upd: seeder 2025-12-29 11:30:50 +08:00
211aac3d5f Merge pull request 'upd dskripsi kategori surat' (#97) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/97
2025-12-29 11:19:09 +08:00
73a2a4367c upd dskripsi kategori surat 2025-12-29 11:18:01 +08:00
a01f394e43 Merge pull request 'upd: link update pengajuan surat pada api' (#96) from amalia/29-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/96
2025-12-29 10:56:30 +08:00
7dde0a4eb9 upd: link update pengajuan surat pada api 2025-12-29 10:55:23 +08:00
6debbf8c64 Merge pull request 'upd : update pengajuan surat' (#95) from amalia/24-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/95
2025-12-24 17:30:14 +08:00
c074e2bc0a upd : update pengajuan surat
deskripsi:
- update type form pada tambah dan update pengajuan surat
- tampilan klo tidak ada data yg dicari pada update pengajuan surat
- update seeder category pengajuan surat

No Issues
2025-12-24 17:29:25 +08:00
bab832b87f Merge pull request 'amalia/23-des-25' (#94) from amalia/23-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/94
2025-12-23 17:51:17 +08:00
8bafb88086 upd: template surat 2025-12-23 17:50:01 +08:00
777f2c04f1 upd: update kategori pelayanan surat
Deskripsi:
- update data seeder kategori pelayanan
- view more pada riwayat pengajuan surat dan pengaduan
- sort data pada api detail riwayat pengajuan surat dan pengaduan

No Issues
2025-12-23 14:33:38 +08:00
a81f6c4255 upd : setting kategori pelayanan surat
Deskripsi:
- disable tambah kategori pengajuan surat
- disable edit kategori pengajuan surat
- update json permission

No Issues
2025-12-23 11:42:30 +08:00
0f1b0196e7 Merge pull request 'amalia/22-des-25' (#93) from amalia/22-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/93
2025-12-22 17:39:31 +08:00
da86f5f10a upd: api kategori pengajuan surat
Deskripsi:
- menambahkan link tambah disetiap kategori
- perbaikan list kategori karena berubah struktur

NO Issues
2025-12-22 17:38:45 +08:00
91a3dfdb5d upd: update pelayanan surat
Deskripsi:
- pengaplikasian api
- modal konfirmasi update pelayanan surat
- modal konfirmasi create pelayanan surat

NO Issues
2025-12-22 14:37:42 +08:00
3904527c2a Merge pull request 'upd: update data pelayanan surat' (#92) from amalia/19-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/92
2025-12-19 17:28:49 +08:00
ff0413be5a upd: update data pelayanan surat
Deskripsi
- form pencarian
- detail data pengajuan
- api
- belom selesai submit

NO Issues
2025-12-19 17:27:39 +08:00
fda2b0977a Merge pull request 'upd: tambah surat' (#91) from amalia/18-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/91
2025-12-18 17:43:22 +08:00
55fbf4836d upd: tambah surat
deskripsi:
- layout form
- seeder category pelayanan
- api tambah surat

No Issues
2025-12-18 17:42:25 +08:00
c897063eb5 Merge pull request 'upd: form surat' (#90) from amalia/17-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/90
2025-12-17 17:40:40 +08:00
41 changed files with 4019 additions and 893 deletions

146
bak/ModalSurat.tsx.txt Normal file
View File

@@ -0,0 +1,146 @@
import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconDownload, IconX } from "@tabler/icons-react";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import { useRef } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
import SKDomisiliOrganisasi from "./surat/SKDomisiliOrganisasi";
import SKKelahiran from "./surat/SKKelahiran";
import SKKelakuanBaik from "./surat/SKKelakuanBaik";
import SKKematian from "./surat/SKKematian";
import SKPenghasilan from "./surat/SKPenghasilan";
import SKTempatUsaha from "./surat/SKTempatUsaha";
import SKTidakMampu from "./surat/SKTidakMampu";
import SKUsaha from "./surat/SKUsaha";
import SKYatim from "./surat/SKYatimPiatu";
export default function ModalSurat({
open,
onClose,
surat,
}: {
open: boolean;
onClose: () => void;
surat: string;
}) {
const A4Style = {
width: "210mm",
height: "297mm",
padding: "20mm",
background: "#fff",
color: "#000",
fontSize: "14px",
fontFamily: "Times New Roman",
};
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
query: {
id: surat,
},
}),
);
useShallowEffect(() => {
mutate();
}, []);
const downloadPDF = async () => {
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
};
return (
<>
<Modal
opened={open}
onClose={() => onClose()}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
},
title: {
width: "100%",
},
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8}>
<ActionIcon size={32} variant="default">
<IconDownload size={20} onClick={downloadPDF} />
</ActionIcon>
<ActionIcon size={32} variant="default" onClick={onClose}>
<IconX size={20} />
</ActionIcon>
</Flex>
</Flex>
}
>
<div ref={hiddenRef} style={A4Style}>
{data && data.data ? (
data.data.surat.idCategory == "skusaha" ? (
<SKUsaha data={data.data} />
) : data.data.surat.idCategory == "skkelahiran" ? (
<SKKelahiran data={data.data} />
) : data.data.surat.idCategory == "skkelakuanbaik" ? (
<SKKelakuanBaik data={data.data} />
) : data.data.surat.idCategory == "skpenghasilan" ? (
<SKPenghasilan data={data.data} />
) : data.data.surat.idCategory == "sktidakmampu" ? (
<SKTidakMampu data={data.data} />
) : data.data.surat.idCategory == "skyatimpiatu" ? (
<SKYatim data={data.data} />
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
<SKDomisiliOrganisasi data={data.data} />
) : data.data.surat.idCategory == "skbedabiodata" ? (
<SKBedaBiodataDiri data={data.data} />
) : data.data.surat.idCategory == "sktempatusaha" ? (
<SKTempatUsaha data={data.data} />
) : data.data.surat.idCategory == "skbelumkawin" ? (
<SKBelumKawin data={data.data} />
) : data.data.surat.idCategory == "skkematian" ? (
<SKKematian data={data.data} />
) : (
<></>
)
) : (
<></>
)}
</div>
</Modal>
</>
);
}

310
bak/listPermission.json.txt Normal file
View File

@@ -0,0 +1,310 @@
{
"menus": [
{
"key": "dashboard",
"label": "Dashboard",
"default": true,
"children": [
{
"key": "dashboard.view",
"label": "Melihat Dashboard",
"default": true
}
]
},
{
"key": "pengaduan",
"label": "Pengaduan",
"default": true,
"children": [
{
"key": "pengaduan.view",
"label": "Melihat List & Detail",
"default": true
},
{
"key": "pengaduan.antrian",
"label": "Detail pengaduan dengan status antrian",
"default": true,
"children": [
{
"key": "pengaduan.antrian.tolak",
"label": "Menolak pengaduan",
"default": true
},
{
"key": "pengaduan.antrian.terima",
"label": "Menerima pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.diterima",
"label": "Detail pengaduan dengan status diterima",
"default": true,
"children": [
{
"key": "pengaduan.diterima.dikerjakan",
"label": "Menegerjakan pengaduan",
"default": true
}
]
},
{
"key": "pengaduan.dikerjakan",
"label": "Detail pengaduan dengan status dikerjakan",
"default": true,
"children": [
{
"key": "pengaduan.dikerjakan.selesai",
"label": "Menyelesaikan pengaduan",
"default": true
}
]
}
]
},
{
"key": "pelayanan",
"label": "Pelayanan",
"default": true,
"children": [
{
"key": "pelayanan.view",
"label": "Melihat List & Detail",
"default": true
},
{
"key": "pelayanan.antrian",
"label": "Detail pelayanan dengan status antrian",
"default": true,
"children": [
{
"key": "pelayanan.antrian.tolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.antrian.terima",
"label": "Menerima pelayanan",
"default": true
}
]
},
{
"key": "pelayanan.diterima",
"label": "Detail pelayanan dengan status diterima",
"default": true,
"children": [
{
"key": "pelayanan.diterima.tolak",
"label": "Menolak pelayanan",
"default": true
},
{
"key": "pelayanan.diterima.setujui",
"label": "Menyetujui pelayanan",
"default": true
}
]
}
]
},
{
"key": "warga",
"label": "Warga",
"default": true,
"children": [
{
"key": "warga.view",
"label": "Melihat List & Detail",
"default": true
}
]
},
{
"key": "setting",
"label": "Setting",
"default": true,
"children": [
{
"key": "setting.profile",
"label": "Profile",
"default": true,
"children": [
{
"key": "setting.profile.view",
"label": "View",
"default": true
},
{
"key": "setting.profile.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.profile.password",
"label": "Ubah Password",
"default": true
}
]
},
{
"key": "setting.user",
"label": "User",
"default": true,
"children": [
{
"key": "setting.user.view",
"label": "View List",
"default": true
},
{
"key": "setting.user.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.user.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.user.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.user_role",
"label": "User Role",
"default": true,
"children": [
{
"key": "setting.user_role.view",
"label": "View List",
"default": true
},
{
"key": "setting.user_role.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.user_role.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.user_role.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.kategori_pengaduan",
"label": "Kategori Pengaduan",
"default": true,
"children": [
{
"key": "setting.kategori_pengaduan.view",
"label": "View List",
"default": true
},
{
"key": "setting.kategori_pengaduan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pengaduan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pengaduan.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.kategori_pelayanan",
"label": "Kategori Pelayanan Surat",
"default": true,
"children": [
{
"key": "setting.kategori_pelayanan.view",
"label": "View List",
"default": true
},
{
"key": "setting.kategori_pelayanan.detail",
"label": "View Detail",
"default": true
},
{
"key": "setting.kategori_pelayanan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pelayanan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pelayanan.delete",
"label": "Delete",
"default": true
}
]
},
{
"key": "setting.desa",
"label": "Desa",
"default": true,
"children": [
{
"key": "setting.desa.view",
"label": "View List",
"default": true
},
{
"key": "setting.desa.edit",
"label": "Edit",
"default": true
}
]
}
]
},
{
"key": "api_key",
"label": "API Key",
"default": true,
"children": [
{
"key": "api_key.view",
"label": "View List",
"default": true
}
]
},
{
"key": "credential",
"label": "Credential",
"default": true,
"children": [
{
"key": "credential.view",
"label": "View List",
"default": true
}
]
}
]
}

View File

@@ -22,6 +22,7 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",

View File

@@ -28,6 +28,7 @@
"@types/lodash": "^4.17.20",
"@types/uuid": "^11.0.0",
"add": "^2.0.6",
"dayjs": "^1.11.19",
"echarts": "^6.0.0",
"echarts-for-react": "^3.0.5",
"elysia": "^1.4.15",

View File

@@ -110,7 +110,8 @@ model CategoryPelayanan {
id String @id @default(cuid())
name String
syaratDokumen Json[]
dataText String[]
dataText String[] @default([])
dataPelengkap Json[] @default([])
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -186,6 +187,7 @@ model SuratPelayanan {
Warga Warga @relation(fields: [idWarga], references: [id])
idWarga String
noSurat String
file String?
dateExpired DateTime? @db.Date
status Int @default(0)
isActive Boolean @default(true)

View File

@@ -10,6 +10,7 @@ import FormKartuKeluarga from "./pages/darmasaba/form_kartu_keluarga";
import FormLaporanSampah from "./pages/darmasaba/form_laporan_sampah";
import FormSuratKeteranganPenghasilan from "./pages/darmasaba/form_surat_keterangan_penghasilan";
import FormSuratKeteranganDomisiliOrganisasi from "./pages/darmasaba/form_surat_keterangan_domisili_organisasi";
import UpdateDataSurat from "./pages/darmasaba/update_data_surat";
import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterangan_belum_kawin";
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
@@ -69,6 +70,10 @@ export default function AppRoutes() {
path="/darmasaba/surat-keterangan-domisili-organisasi"
element={<FormSuratKeteranganDomisiliOrganisasi />}
/>
<Route
path="/darmasaba/update-data-surat"
element={<UpdateDataSurat />}
/>
<Route
path="/darmasaba/surat-keterangan-belum-kawin"
element={<FormSuratKeteranganBelumKawin />}

View File

@@ -10,6 +10,7 @@ const clientRoutes = {
"/darmasaba/laporan-sampah": "/darmasaba/laporan-sampah",
"/darmasaba/surat-keterangan-penghasilan": "/darmasaba/surat-keterangan-penghasilan",
"/darmasaba/surat-keterangan-domisili-organisasi": "/darmasaba/surat-keterangan-domisili-organisasi",
"/darmasaba/update-data-surat": "/darmasaba/update-data-surat",
"/darmasaba/surat-keterangan-belum-kawin": "/darmasaba/surat-keterangan-belum-kawin",
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",

View File

@@ -0,0 +1,38 @@
import { Center, Loader, Overlay, Stack, Text } from "@mantine/core";
type FullScreenLoadingProps = {
visible: boolean;
text?: string;
};
export default function FullScreenLoading({
visible,
text = "Memproses data...",
}: FullScreenLoadingProps) {
if (!visible) return null;
return (
<Overlay fixed blur={6} backgroundOpacity={0.3} zIndex={10000}>
<Center h="100%">
<Stack align="center" justify="center">
<Loader size="lg" />
<Text size="sm" c="dimmed">
{text}
</Text>
</Stack>
</Center>
</Overlay>
);
}
const overlayStyle: React.CSSProperties = {
position: "fixed",
inset: 0,
zIndex: 9999,
backdropFilter: "blur(6px)",
backgroundColor: "rgba(255, 255, 255, 0.6)",
};
const contentStyle: React.CSSProperties = {
flexDirection: "column",
};

View File

@@ -11,7 +11,6 @@ import {
Modal,
Stack,
Table,
TagsInput,
Text,
Title,
Tooltip,
@@ -44,13 +43,13 @@ export default function KategoriPelayananSurat({
const [dataChoose, setDataChoose] = useState({
id: "",
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
const [dataTambah, setDataTambah] = useState({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
useShallowEffect(() => {
@@ -60,8 +59,8 @@ export default function KategoriPelayananSurat({
async function handleCreate() {
try {
setBtnLoading(true);
const cleanedDataText = dataTambah.dataText
.map((v) => v.trim())
const cleanedDataText = dataTambah.dataPelengkap
.map((v) => v.name.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataTambah.syaratDokumen
.map((item) => ({
@@ -82,8 +81,8 @@ export default function KategoriPelayananSurat({
closeTambah();
setDataTambah({
name: "",
syaratDokumen: [{ name: "", desc: "" }],
dataText: [""],
syaratDokumen: [{ key: "", name: "", desc: "" }],
dataPelengkap: [{ key: "", name: "", desc: "" }],
});
notification({
title: "Success",
@@ -112,8 +111,8 @@ export default function KategoriPelayananSurat({
async function handleEdit() {
try {
setBtnLoading(true);
const cleanedDataText = dataChoose.dataText
.map((v) => v.trim())
const cleanedDataText = dataChoose.dataPelengkap
.map((v) => v.name.trim())
.filter((v) => v !== "");
const cleanedSyarat = dataChoose.syaratDokumen
.map((item) => ({
@@ -191,7 +190,10 @@ export default function KategoriPelayananSurat({
function handleAddSyarat() {
setDataChoose({
...dataChoose,
syaratDokumen: [...dataChoose.syaratDokumen, { name: "", desc: "" }],
syaratDokumen: [
...dataChoose.syaratDokumen,
{ key: "", name: "", desc: "" },
],
});
}
@@ -204,7 +206,7 @@ export default function KategoriPelayananSurat({
function handleEditSyarat(
index: number,
data: { name: string; desc: string },
data: { key: string; name: string; desc: string },
) {
setDataChoose({
...dataChoose,
@@ -217,7 +219,7 @@ export default function KategoriPelayananSurat({
return (
<>
{/* Modal Edit */}
<Modal
{/* <Modal
opened={opened}
onClose={close}
title={"Edit"}
@@ -233,15 +235,85 @@ export default function KategoriPelayananSurat({
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataChoose.dataText}
onChange={(value) =>
setDataChoose({ ...dataChoose, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
Data Pelengkap
</Text>
<Tooltip label="Tambah Data Pelengkap">
<ActionIcon
variant="light"
size="sm"
color="blue"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={handleAddSyarat}
>
<IconPlus size={20} />
</ActionIcon>
</Tooltip>
</Group>
{dataChoose?.dataPelengkap?.map((v: any, i: number) => (
<Grid
key={i}
style={{
borderBottom: "1px solid gray",
paddingBottom: "10px",
}}
>
<Grid.Col
span={1}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Tooltip label="Delete Syarat Dokumen">
<ActionIcon
variant="light"
size="sm"
color="red"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => {
handleDeleteSyarat(i);
}}
>
<IconTrash size={20} />
</ActionIcon>
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Label">
<Input
value={v.name}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: e.target.value,
desc: v.desc,
})
}
/>
</Input.Wrapper>
</Grid.Col>
<Grid.Col span={6}>
<Input.Wrapper label="Deskripsi">
<Input
value={v.desc}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: v.name,
desc: e.target.value,
})
}
/>
</Input.Wrapper>
</Grid.Col>
</Grid>
))}
</Flex>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
@@ -290,11 +362,12 @@ export default function KategoriPelayananSurat({
</Tooltip>
</Grid.Col>
<Grid.Col span={5}>
<Input.Wrapper label="Nama">
<Input.Wrapper label="Label">
<Input
value={v.name}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: e.target.value,
desc: v.desc,
})
@@ -308,6 +381,7 @@ export default function KategoriPelayananSurat({
value={v.desc}
onChange={(e) =>
handleEditSyarat(i, {
key: v.key,
name: v.name,
desc: e.target.value,
})
@@ -328,10 +402,10 @@ export default function KategoriPelayananSurat({
</Button>
</Group>
</Stack>
</Modal>
</Modal> */}
{/* Modal Tambah */}
<Modal
{/* <Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
@@ -347,15 +421,6 @@ export default function KategoriPelayananSurat({
}
/>
</Input.Wrapper>
<TagsInput
label="Data Pelengkap"
placeholder="Tambah data pelengkap"
splitChars={[","]}
value={dataTambah.dataText}
onChange={(value) =>
setDataTambah({ ...dataTambah, dataText: value })
}
/>
<Flex direction={"column"} gap={"md"}>
<Group>
<Text size="sm" c={"white"}>
@@ -372,7 +437,7 @@ export default function KategoriPelayananSurat({
...dataTambah,
syaratDokumen: [
...dataTambah.syaratDokumen,
{ name: "", desc: "" },
{ key: "", name: "", desc: "" },
],
});
}}
@@ -465,7 +530,7 @@ export default function KategoriPelayananSurat({
</Button>
</Group>
</Stack>
</Modal>
</Modal> */}
{/* Modal Delete */}
<Modal
@@ -524,8 +589,8 @@ export default function KategoriPelayananSurat({
Data Pelengkap
</Text>
<List>
{dataChoose?.dataText?.map((v: any) => (
<List.Item key={v.id}>{v}</List.Item>
{dataChoose?.dataPelengkap?.map((v: any) => (
<List.Item key={v.id}>{v.name}</List.Item>
))}
</List>
</Flex>
@@ -538,7 +603,7 @@ export default function KategoriPelayananSurat({
<Title order={4} c="gray.2">
Kategori Pelayanan Surat
</Title>
{permissions.includes("setting.kategori_pelayanan.tambah") && (
{/* {permissions.includes("setting.kategori_pelayanan.tambah") && (
<Tooltip label="Tambah Kategori Pelayanan Surat">
<Button
variant="light"
@@ -548,7 +613,7 @@ export default function KategoriPelayananSurat({
Tambah
</Button>
</Tooltip>
)}
)} */}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
@@ -579,7 +644,7 @@ export default function KategoriPelayananSurat({
<IconEye size={20} />
</ActionIcon>
</Tooltip>
<Tooltip
{/* <Tooltip
label={
permissions.includes(
"setting.kategori_pelayanan.edit",
@@ -604,7 +669,7 @@ export default function KategoriPelayananSurat({
>
<IconEdit size={20} />
</ActionIcon>
</Tooltip>
</Tooltip> */}
<Tooltip
label={
permissions.includes(

View File

@@ -1,10 +1,9 @@
import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core";
import { Flex, Loader, Modal, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconDownload, IconX } from "@tabler/icons-react";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import { useRef } from "react";
import { useRef, useState } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
@@ -24,7 +23,7 @@ export default function ModalSurat({
surat,
}: {
open: boolean;
onClose: () => void;
onClose: (val: any) => void;
surat: string;
}) {
const A4Style = {
@@ -36,6 +35,7 @@ export default function ModalSurat({
fontSize: "14px",
fontFamily: "Times New Roman",
};
const [uploading, setUploading] = useState<"Menyiapkan" | "Mengupload" | "Selesai">("Menyiapkan")
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
@@ -49,38 +49,80 @@ export default function ModalSurat({
mutate();
}, []);
const downloadPDF = async () => {
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const uploadPdf = async () => {
try {
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
setUploading("Mengupload");
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
};
// ⬇️ ambil sebagai Blob
const pdfBlob = pdf.output("blob");
const pdfFile = new File(
[pdfBlob],
`${data?.data?.surat?.nameCategory}.pdf`,
{
type: "application/pdf",
lastModified: Date.now(),
}
);
const 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!,
});
setUploading("Selesai");
setTimeout(() => {
onClose(resUpdate.data?.link);
}, 1000)
}
} catch (error) {
console.error("Error uploading PDF:", error);
}
}
useShallowEffect(() => {
if (open) {
setTimeout(() => {
uploadPdf();
}, 5000);
}
}, [surat, open]);
return (
<>
<Modal
opened={open}
onClose={() => onClose()}
onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
closeOnClickOutside={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
@@ -97,14 +139,9 @@ export default function ModalSurat({
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8}>
<ActionIcon size={32} variant="default">
<IconDownload size={20} onClick={downloadPDF} />
</ActionIcon>
<ActionIcon size={32} variant="default" onClick={onClose}>
<IconX size={20} />
</ActionIcon>
<Flex gap={8} align="center">
<Loader color="blue" size="xs" />
<Text size="sm">{uploading}</Text>
</Flex>
</Flex>
}

View File

@@ -0,0 +1,45 @@
import { Button, Center, Group, Stack, Text, Title } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
export function DataNotFound({
onRetry,
backTo,
}: {
onRetry?: () => void;
backTo?: () => void;
}) {
return (
<Center mih={320}>
<Stack align="center" gap="sm">
<IconSearch size={64} opacity={0.5} />
<Title order={4}>Data Pengajuan Tidak Ditemukan</Title>
<Text size="sm" c="dimmed" ta="center" maw={380}>
Kami tidak dapat menemukan data pengajuan dengan nomor pengajuan yg
diinputkan. Silakan periksa kembali data Anda.
</Text>
<Group mt="md">
{/* {onRetry && (
<Button
variant="light"
leftSection={<IconSearch size={16} />}
onClick={onRetry}
>
Cari Ulang
</Button>
)} */}
<Button
variant="outline"
// leftSection={<IconArrowLeft size={16} />}
onClick={backTo}
>
Cari Kembali
</Button>
</Group>
</Stack>
</Center>
);
}

View File

@@ -0,0 +1,49 @@
import { Badge, Button, Card, Center, Stack, Text, Title } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
type SuccessPengajuanProps = {
noPengajuan: string;
onClose?: () => void;
category?: "create" | "update";
};
export default function SuccessPengajuan({
noPengajuan,
onClose,
category,
}: SuccessPengajuanProps) {
return (
<Center h="100vh">
<Card shadow="md" radius="md" p="xl" withBorder maw={520} w="100%">
<Stack align="center" gap="md">
<IconCheck size={56} color="green" />
<Title order={3} ta="center">
{category == "create"
? "Pengajuan Berhasil Dibuat"
: "Pengajuan Berhasil Diupdate"}
</Title>
<Text ta="center" size="sm" c="dimmed">
{category == "create"
? "Pengajuan layanan surat sudah dibuat dengan nomor:"
: "Pengajuan layanan surat sudah diupdate dengan nomor:"}
</Text>
<Badge size="xl" variant="light" color="green">
{noPengajuan}
</Badge>
<Text ta="center" size="sm">
Nomor ini akan digunakan untuk mengakses dan memantau status
pengajuan surat Anda.
</Text>
<Button fullWidth mt="md" onClick={onClose}>
Selesai
</Button>
</Stack>
</Card>
</Center>
);
}

View File

@@ -108,12 +108,12 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
@@ -144,36 +144,36 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>1. Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
<td style={{ width: "160px" }}>1. {getValue("data_dokumen")}</td>
<td style={{ width: "10px" }}></td>
<td>{/* {getValue("nama")} */}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
<td>{getValue("dokumen_a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
<td>{getValue("dokumen_b")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
{/* <div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{getValue("tempat_lahir")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
<td>{getValue("dokumen_a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
@@ -204,7 +204,7 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
</tr>
</tbody>
</table>
</div>
</div> */}
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena{" "}

View File

@@ -87,12 +87,12 @@ export default function SKBelumKawin({ data }: { data: any }) {
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Agama</td>

View File

@@ -95,27 +95,27 @@ export default function SKDomisiliOrganisasi({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Nama Organisasi</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
<td>{getValue("nama_organisasi")}</td>
</tr>
<tr>
<td>Jenis Organisasi</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_organisasi")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{getValue("alamat_organisasi")}</td>
</tr>
<tr>
<td>Nomor Telepon</td>
<td>:</td>
<td>{getValue("negara")}</td>
<td>{getValue("no_telepon")}</td>
</tr>
<tr>
<td>Nama Pimpinan</td>
<td>:</td>
<td>{getValue("agama")}</td>
<td>{getValue("nama_pimpinan")}</td>
</tr>
</tbody>
</table>

View File

@@ -78,32 +78,32 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr>
<td style={{ width: "200px" }}>Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tanggal lahir anak")}</td>
<td>{getValue("tanggal_lahir_anak")}</td>
</tr>
<tr>
<td>Pukul</td>
<td>:</td>
<td>{getValue("pukul lahir anak")}</td>
<td>{getValue("pukul_lahir")}</td>
</tr>
<tr>
<td>Tempat Kelahiran</td>
<td>:</td>
<td>{getValue("tempat lahir anak")}</td>
<td>{getValue("tempat_lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin anak")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Anak ke</td>
<td>:</td>
<td>{getValue("anak ke")}</td>
<td>{getValue("anak_ke")}</td>
</tr>
<tr>
<td>Nama Anak</td>
<td>:</td>
<td>{getValue("nama anak")}</td>
<td>{getValue("nama_anak")}</td>
</tr>
</tbody>
</table>
@@ -117,27 +117,27 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr>
<td style={{ width: "200px" }}>Nama Lengkap Ibu</td>
<td>:</td>
<td>{getValue("nama ibu")}</td>
<td>{getValue("nama_ibu")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik ibu")}</td>
<td>{getValue("nik_ibu")}</td>
</tr>
<tr>
<td>Tempat & Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir ibu")}</td>
<td>{`${getValue("tempat_lahir_ibu")}, ${getValue("tanggal_lahir_ibu")}`}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan ibu")}</td>
<td>{getValue("pekerjaan_ibu")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat ibu")}</td>
<td>{getValue("alamat_ibu")}</td>
</tr>
</tbody>
</table>
@@ -151,27 +151,27 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr>
<td style={{ width: "200px" }}>Nama Lengkap Ayah</td>
<td>:</td>
<td>{getValue("nama ayah")}</td>
<td>{getValue("nama_ayah")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik ayah")}</td>
<td>{getValue("nik_ayah")}</td>
</tr>
<tr>
<td>Tempat & Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir ayah")}</td>
<td>{`${getValue("tempat_lahir_ayah")}, ${"tanggal_lahir_ayah"}`}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan ayah")}</td>
<td>{getValue("pekerjaan_ayah")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat ayah")}</td>
<td>{getValue("alamat_ayah")}</td>
</tr>
</tbody>
</table>
@@ -185,17 +185,17 @@ export default function SKKelahiran({ data }: { data: any }) {
<tr>
<td style={{ width: "200px" }}>Nama Pelapor</td>
<td>:</td>
<td>{getValue("nama pelapor")}</td>
<td>{getValue("nama_pelapor")}</td>
</tr>
<tr>
<td>Hubungan dengan Anak</td>
<td>:</td>
<td>{getValue("hubungan pelapor")}</td>
<td>{getValue("hubungan_pelapor")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat pelapor")}</td>
<td>{getValue("alamat_pelapor")}</td>
</tr>
</tbody>
</table>

View File

@@ -71,12 +71,12 @@ export default function SKKelakuanBaik({ data }: { data: any }) {
<tr>
<td>Tempat/Tgl Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Agama</td>

View File

@@ -71,27 +71,27 @@ export default function SKKematian({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
<td>{getValue("nama_pelapor")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
<td>{getValue("nik_pelapor")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
<td>{getValue("pekerjaan_pelapor")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
<td>{getValue("alamat_pelapor")}</td>
</tr>
<tr>
<td>Hubungan dengan almarhum/almarhumah</td>
<td>:</td>
<td>{getValue("hubungan dengan almarhum")}</td>
<td>{getValue("hubungan_pelapor")}</td>
</tr>
</tbody>
</table>
@@ -104,32 +104,32 @@ export default function SKKematian({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
<td>{getValue("nama_almarhum")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
<td>{getValue("nik_almarhum")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin_almarhum")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir_almarhum")}, ${getValue("tanggal_lahir_almarhum")}`}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
<td>{getValue("agama_almarhum")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
<td>{getValue("alamat_almarhum")}</td>
</tr>
</tbody>
</table>
@@ -142,22 +142,22 @@ export default function SKKematian({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Tanggal Kematian</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tanggal kematian")}</td>
<td>{getValue("tanggal_kematian")}</td>
</tr>
<tr>
<td>Waktu Kematian</td>
<td>:</td>
<td>{getValue("waktu kematian")}</td>
<td>{getValue("waktu_kematian")}</td>
</tr>
<tr>
<td>Tempat Kematian</td>
<td>:</td>
<td>{getValue("tempat kematian")}</td>
<td>{getValue("tempat_kematian")}</td>
</tr>
<tr>
<td>Penyebab Kematian</td>
<td>:</td>
<td>{getValue("penyebab kematian")}</td>
<td>{getValue("penyebab_kematian")}</td>
</tr>
</tbody>
</table>
@@ -185,7 +185,7 @@ export default function SKKematian({ data }: { data: any }) {
<br />
<br />
<br /> <br />
<u>{getValue("nama")}</u> <br />
<u>{getValue("nama_pelapor")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />

View File

@@ -105,12 +105,12 @@ export default function SKPenghasilan({ data }: { data: any }) {
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Tempat / Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Pekerjaan</td>
@@ -135,10 +135,7 @@ export default function SKPenghasilan({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Penghasilan</td>
<td style={{ width: "10px" }}>:</td>
<td>
Rp {getValue("penghasilan")} (
{getValue("penghasilan terbilang")}) per bulan
</td>
<td>Rp. {getValue("penghasilan")} per bulan</td>
</tr>
</tbody>
</table>
@@ -146,8 +143,8 @@ export default function SKPenghasilan({ data }: { data: any }) {
{/* KEPERLUAN */}
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat untuk keperluan:{" "}
<b>{getValue("alasan permohonan")}</b>.
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan")}</b>
.
</div>
<div style={{ marginTop: "20px" }}>

View File

@@ -66,12 +66,15 @@ export default function SKTempatUsaha({ data }: { data: any }) {
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama")} />
<Row label="Nama Pemilik Usaha" value={getValue("nama_pemilik")} />
<Row
label="Tempat/Tanggal Lahir"
value={getValue("tempat tanggal lahir")}
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
/>
<Row
label="Alamat Pemilik Usaha"
value={getValue("alamat_pemilik")}
/>
<Row label="Alamat Pemilik Usaha" value={getValue("alamat")} />
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
@@ -83,23 +86,17 @@ export default function SKTempatUsaha({ data }: { data: any }) {
</div>
<div>
<Row label="Nama Usaha" value={getValue("nama usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat usaha")} />
<Row
label="Status Tempat Usaha"
value={getValue("status tempat usaha")}
/>
<Row
label="Luas Tempat Usaha"
value={getValue("luas tempat usaha")}
/>
<Row label="Jumlah Karyawan" value={getValue("jumlah karyawan")} />
<Row label="Nama Usaha" value={getValue("nama_usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang_usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat_usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status_tempat")} />
<Row label="Luas Tempat Usaha" value={getValue("luas_usaha")} />
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan{" "}
<b>{getValue("alasan permohonan")}.</b>
<b>{getValue("tujuan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>

View File

@@ -66,13 +66,13 @@ export default function SKTidakMampu({ data }: { data: any }) {
{/* DATA WARGA */}
<div>
<Row label="NIK" value={getValue("nik")} />
<Row label="Nama" value={getValue("nama")} />
<Row
label="Tempat Tgl Lahir"
value={getValue("tempat tanggal lahir")}
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
/>
<Row label="Alamat" value={getValue("alamat")} />
<Row label="NIK" value={getValue("nik")} />
</div>
<br />
@@ -80,7 +80,7 @@ export default function SKTidakMampu({ data }: { data: any }) {
<p style={{ textAlign: "justify" }}>
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan
termasuk keluarga tidak mampu. Surat keterangan ini dipergunakan untuk
<b>{getValue("alasan permohonan")}.</b>
<b>{getValue("alasan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>

View File

@@ -105,12 +105,12 @@ export default function SKUsaha({ data }: { data: any }) {
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
<td>{getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Tempat / Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
<td>{`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td>
</tr>
<tr>
<td>Warga Negara</td>
@@ -125,7 +125,7 @@ export default function SKUsaha({ data }: { data: any }) {
<tr>
<td>Status</td>
<td>:</td>
<td>{getValue("status perkawinan")}</td>
<td>{getValue("status_perkawinan")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
@@ -173,12 +173,12 @@ export default function SKUsaha({ data }: { data: any }) {
<tr>
<td style={{ width: "160px" }}>Jenis Usaha</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("jenis usaha")}</td>
<td>{getValue("jenis_usaha")}</td>
</tr>
<tr>
<td>Alamat Usaha</td>
<td>:</td>
<td>{getValue("alamat usaha")}</td>
<td>{getValue("alamat_usaha")}</td>
</tr>
</tbody>
</table>

View File

@@ -88,26 +88,28 @@ export default function SKYatim({ data }: { data: any }) {
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td>NIK</td>
<td>: {getValue("nik")}</td>
</tr>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {getValue("nama")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>: {getValue("tempat tanggal lahir")}</td>
<td>
: {`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>: {getValue("jenis kelamin")}</td>
<td>: {getValue("jenis_kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>: {getValue("alamat")}</td>
</tr>
<tr>
<td>NIK</td>
<td>: {getValue("nik")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>: {getValue("pekerjaan")}</td>
@@ -133,11 +135,11 @@ export default function SKYatim({ data }: { data: any }) {
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ayah</td>
<td>: {getValue("nama ayah")}</td>
<td>: {getValue("nama_ayah")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ayah")}</td>
<td>: {getValue("status_ayah")}</td>
</tr>
</tbody>
</table>
@@ -151,11 +153,11 @@ export default function SKYatim({ data }: { data: any }) {
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ibu</td>
<td>: {getValue("nama ibu")}</td>
<td>: {getValue("nama_ibu")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ibu")}</td>
<td>: {getValue("status_ibu")}</td>
</tr>
</tbody>
</table>

View File

@@ -15,6 +15,7 @@ import LayananRoute from "./server/routes/layanan_route";
import { MCPRoute } from "./server/routes/mcp_route";
import PelayananRoute from "./server/routes/pelayanan_surat_route";
import PengaduanRoute from "./server/routes/pengaduan_route";
import SendWaRoute from "./server/routes/send_wa_route";
import SuratRoute from "./server/routes/surat_route";
import TestPengaduanRoute from "./server/routes/test_pengaduan";
import UserRoute from "./server/routes/user_route";
@@ -45,7 +46,8 @@ const Api = new Elysia({
.use(CredentialRoute)
.use(UserRoute)
.use(LayananRoute)
.use(AduanRoute);
.use(AduanRoute)
.use(SendWaRoute);
const app = new Elysia()
.use(Api)

View File

@@ -1,109 +1,479 @@
import { enumAgama, enumJenisKelamin, enumStatusHidup, enumStatusPerkawinan, enumStatusTempatUsaha } from "./valueEnum";
export const categoryPelayananSurat = [
{
id: "skbedabiodata",
name: "Surat Keterangan Beda Biodata Diri",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "dokumen yang beda", desc: "Fotokopi dokumen bersangkutan yang terdapat perbedaan biodata diri, misalnya: Sertifikat Tanah, Ijazah, Polis Asuransi, dan lainnya." }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas di Wilayah Masing-masing"
},
{
key: "ktp_kk",
name: "KTP / KK",
desc: "Fotokopi KTP atau Kartu Keluarga"
},
{
key: "dokumen_beda",
name: "Dokumen Pendukung",
desc: "Fotokopi dokumen yang terdapat perbedaan biodata (ijazah, sertifikat, dll)"
}
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "dokumen", "tertulis pada dokumen a", "tertulis pada dokumen b"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin
},
{ key: "alamat", name: "Alamat", desc: "Alamat lengkap tempat tinggal", type: "text" },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" },
{
key: "dokumen",
name: "Nama Dokumen",
desc: "Jenis dokumen yang mengalami perbedaan biodata (cth : ijazah, sertifikat, dll)",
type: "text"
},
{
key: "data_dokumen",
name: "Data Dokumen",
desc: "Data dokumen yg berbeda (cth : nama, tempat lahir, tanggal lahir, jenis kelamin, alamat, pekerjaan)",
type: "text"
},
{
key: "dokumen_a",
name: "Data pada Dokumen A",
desc: "Data biodata yang tertulis pada dokumen pertama",
type: "text"
},
{
key: "dokumen_b",
name: "Data pada Dokumen B",
desc: "Data biodata yang tertulis pada dokumen kedua",
type: "text"
}
]
},
{
id: "skbelumkawin",
name: "Surat Keterangan Belum Kawin",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "akta cerai", desc: "Fotokopi Akta Cerai bagi yang berstatus janda/duda" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "agama", "pekerjaan"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
{
key: "agama",
name: "Agama",
desc: "Agama pemohon",
type: "enum",
options: enumAgama
},
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" }
]
},
{
id: "skdomisiliorganisasi",
name: "Surat Keterangan Domisili Organisasi",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "skt organisasi", desc: "Fotokopi Surat Keterangan Terdaftar (SKT) Organisasi atau Pengukuhan Kelompok" },
{ name: "susunan pengurus", desc: "Jika Pengajuan baru pembuatan SKT maka melengkapi Susunan Pengurus lengkap denganKop Organisasi" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nama organisasi", "jenis organisasi", "alamat organisasi/sekretariat", "no telepon", "nama pimpinan", "keperluan"]
dataText: [],
dataPelengkap: [
{ key: "nama_organisasi", name: "Nama Organisasi", desc: "Nama resmi organisasi", type: "text" },
{ key: "jenis_organisasi", name: "Jenis Organisasi", desc: "Jenis atau bentuk organisasi", type: "text" },
{ key: "alamat_organisasi", name: "Alamat Organisasi", desc: "Alamat sekretariat organisasi", type: "text" },
{ key: "no_telepon", name: "Nomor Telepon", desc: "Nomor telepon organisasi", type: "text" },
{ key: "nama_pimpinan", name: "Nama Pimpinan", desc: "Nama pimpinan organisasi", type: "text" },
{ key: "keperluan", name: "Keperluan", desc: "Keperluan pembuatan surat", type: "text" }
]
},
{
id: "skkelahiran",
name: "Surat Keterangan Kelahiran",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "surat lahir", desc: "Fotokopi Surat Keterangan Lahir dari Bidan/Dokter (jika ada)" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "surat_lahir",
name: "Surat Keterangan Lahir",
desc: "Surat keterangan lahir dari bidan/dokter (jika ada)"
}
],
dataText: ["nama ayah", "nama ibu", "nama anak", "tanggal lahir anak", "pukul lahir anak", "tempat lahir anak", "jenis kelamin anak", "anak ke", "nik ibu", "tempat tanggal lahir ibu", "pekerjaan ibu", "alamat ibu", "nik ayah", "tempat tanggal lahir ayah", "pekerjaan ayah", "alamat ayah", "nama pelapor", "hubungan pelapor", "alamat pelapor"]
dataText: [],
dataPelengkap: [
{ key: "nama_anak", name: "Nama Anak", desc: "Nama bayi/anak", type: "text" },
{ key: "tanggal_lahir_anak", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" },
{ key: "pukul_lahir", name: "Pukul Lahir", desc: "Waktu kelahiran anak", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat kelahiran anak", type: "text" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin Anak",
desc: "Jenis kelamin anak",
type: "enum",
options: enumJenisKelamin
},
{ key: "anak_ke", name: "Anak Ke-", desc: "Urutan kelahiran anak", type: "number" },
{ key: "nik_ibu", name: "NIK Ibu", desc: "NIK ibu kandung", type: "number" },
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama lengkap ibu", type: "text" },
{ key: "tempat_lahir_ibu", name: "Tempat Lahir Ibu", desc: "Tempat lahir ibu kandung", type: "text" },
{ key: "tanggal_lahir_ibu", name: "Tanggal Lahir Ibu", desc: "Tanggal lahir ibu kandung", type: "date" },
{ key: "pekerjaan_ibu", name: "Pekerjaan Ibu", desc: "Pekerjaan ibu kandung", type: "text" },
{ key: "alamat_ibu", name: "Alamat Ibu", desc: "Alamat ibu kandung", type: "text" },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama lengkap ayah", type: "text" },
{ key: "nik_ayah", name: "NIK Ayah", desc: "NIK ayah kandung", type: "number" },
{ key: "tempat_lahir_ayah", name: "Tempat Lahir Ayah", desc: "Tempat lahir ayah kandung", type: "text" },
{ key: "tanggal_lahir_ayah", name: "Tanggal Lahir Ayah", desc: "Tanggal lahir ayah kandung", type: "date" },
{ key: "pekerjaan_ayah", name: "Pekerjaan Ayah", desc: "Pekerjaan ayah kandung", type: "text" },
{ key: "alamat_ayah", name: "Alamat Ayah", desc: "Alamat ayah kandung", type: "text" },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama pihak yang melaporkan", type: "text" },
{ key: "hubungan_pelapor", name: "Hubungan Pelapor", desc: "Hubungan pelapor dengan anak", type: "text" },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat pelapor", type: "text" }
]
},
{
id: "skkelakuanbaik",
name: "Surat Keterangan Kelakuan Baik (Pengantar SKCK)",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kk",
name: "KTP / KK",
desc: "Fotokopi KTP atau Kartu Keluarga"
}
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "agama", "alamat", "pekerjaan", "polsek"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
{ key: "nama", name: "Nama Lengkap", desc: "Nama sesuai KTP", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin
},
{
key: "agama",
name: "Agama",
desc: "Agama pemohon",
type: "enum",
options: enumAgama
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon", type: "text" },
{ key: "polsek", name: "Polsek Tujuan", desc: "Polsek tujuan pembuatan SKCK", type: "text" }
]
},
{
id: "skkematian",
name: "Surat Keterangan Kematian",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "surat kematian", desc: "Surat Keterangan Kematian dari Rumah Sakit/Dokter (jika ada)" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nik pelapor", "nama pelapor", "pekerjaan pelapor", "alamat pelapor", "hubungan pelapor dengan almarhum", "nama almarhum", "nik almarhum", "tempat tanggal lahir almarhum", "alamat almarhum", "agama almarhum", "tanggal kematian", "waktu kematian", "tempat kematian", "penyebab kematian"]
dataText: [],
dataPelengkap: [
{ key: "nik_pelapor", name: "NIK Pelapor", desc: "Nomor Induk Kependudukan pelapor", type: "number" },
{ key: "nama_pelapor", name: "Nama Pelapor", desc: "Nama lengkap pelapor", type: "text" },
{ key: "pekerjaan_pelapor", name: "Pekerjaan Pelapor", desc: "Pekerjaan pelapor", type: "text" },
{ key: "alamat_pelapor", name: "Alamat Pelapor", desc: "Alamat tempat tinggal pelapor", type: "text" },
{ key: "hubungan_pelapor", name: "Hubungan dengan Almarhum", desc: "Hubungan pelapor dengan almarhum", type: "text" },
{ key: "nama_almarhum", name: "Nama Almarhum", desc: "Nama lengkap almarhum", type: "text" },
{ key: "nik_almarhum", name: "NIK Almarhum", desc: "Nomor Induk Kependudukan almarhum", type: "number" },
{ key: "tempat_lahir_almarhum", name: "Tempat Lahir Almarhum", desc: "Tempat lahir almarhum", type: "text" },
{ key: "tanggal_lahir_almarhum", name: "Tanggal Lahir Almarhum", desc: "Tanggal lahir almarhum", type: "date" },
{ key: "alamat_almarhum", name: "Alamat Almarhum", desc: "Alamat terakhir almarhum", type: "text" },
{
key: "agama_almarhum",
name: "Agama Almarhum",
desc: "Agama almarhum",
type: "enum",
options: enumAgama
},
{ key: "tanggal_kematian", name: "Tanggal Kematian", desc: "Tanggal meninggal dunia", type: "date" },
{ key: "waktu_kematian", name: "Waktu Kematian", desc: "Waktu meninggal dunia", type: "text" },
{ key: "tempat_kematian", name: "Tempat Kematian", desc: "Tempat meninggal dunia", type: "text" },
{ key: "penyebab_kematian", name: "Penyebab Kematian", desc: "Penyebab meninggal dunia", type: "text" }
]
},
{
id: "skpenghasilan",
name: "Surat Keterangan Penghasilan",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp ortu/kk", desc: "Fotokopi KTP orang tua atau Kartu Keluarga" },
{ name: "surat pernyataan", desc: "Surat Pernyataan Penghasilan bermaterai" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "penghasilan", "alasan permohonan"]
dataText: [],
dataPelengkap: [
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemohon", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemohon",
type: "enum",
options: enumJenisKelamin
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemohon/orang tua", type: "text" },
{ key: "penghasilan", name: "Penghasilan", desc: "Jumlah penghasilan per bulan", type: "number" },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan surat penghasilan", type: "text" }
]
},
{
id: "sktempatusaha",
name: "Surat Keterangan Tempat Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" },
{ name: "sppt/sertifikat/sewa", desc: "Fotokopi SPPT, Sertifikat Hak Milik, Surat Perjanjian Sewa, atau Kwitansi Pembayaran Sewa 3 bulan terakhir" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nik", "nama pemilik", "tempat tanggal lahir", "alamat pemilik", "nama usaha", "bidang usaha", "alamat usaha", "status tempat usaha", "luas tempat usaha", "jumlah karyawan", "tujuan pembuatan surat"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
{ key: "nama_pemilik", name: "Nama Pemilik", desc: "Nama pemilik usaha", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
{ key: "alamat_pemilik", name: "Alamat Pemilik", desc: "Alamat pemilik usaha", type: "text" },
{ key: "nama_usaha", name: "Nama Usaha", desc: "Nama usaha", type: "text" },
{ key: "bidang_usaha", name: "Bidang Usaha", desc: "Bidang atau jenis usaha", type: "text" },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" },
{ key: "status_tempat", name: "Status Tempat Usaha", desc: "Status kepemilikan tempat usaha", type: "enum", options: enumStatusTempatUsaha },
{ key: "luas_usaha", name: "Luas Tempat Usaha", desc: "Luas tempat usaha (m²)", type: "number" },
{ key: "jumlah_karyawan", name: "Jumlah Karyawan", desc: "Jumlah tenaga kerja", type: "number" },
{ key: "tujuan", name: "Tujuan Pembuatan Surat", desc: "Tujuan pembuatan surat keterangan", type: "text" }
]
},
{
id: "sktidakmampu",
name: "Surat Keterangan Tidak Mampu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kia_kk",
name: "KTP / KIA / KK",
desc: "Fotokopi KTP, KIA, atau Kartu Keluarga"
}
],
dataText: ["nik", "nama", "tempat tanggal lahir", "alamat", "alasan permohonan"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan pemohon", type: "number" },
{ key: "nama", name: "Nama Lengkap", desc: "Nama lengkap pemohon", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir pemohon", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir pemohon", type: "date" },
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal pemohon", type: "text" },
{ key: "alasan", name: "Alasan Permohonan", desc: "Alasan pengajuan Surat Keterangan Tidak Mampu", type: "text" }
]
},
{
id: "skusaha",
name: "Surat Keterangan Usaha",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kk", desc: "Fotokopi KTP atau Kartu Keluarga" },
{ name: "foto lokasi", desc: "Foto lokasi usaha dicetak dalam selembar kertas, diparaf dan distempel oleh Kelian" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
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: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"]
dataText: [],
dataPelengkap: [
{ key: "nama", name: "Nama Lengkap", desc: "Nama pemilik usaha", type: "text" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin pemilik usaha",
type: "enum",
options: enumJenisKelamin
},
{ key: "tempat_lahir", name: "Tempat Lahir", desc: "Tempat lahir", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir", desc: "Tanggal lahir", type: "date" },
{ key: "negara", name: "Kewarganegaraan", desc: "Kewarganegaraan pemilik usaha", type: "text" },
{
key: "agama",
name: "Agama",
desc: "Agama pemilik usaha",
type: "enum",
options: enumAgama
},
{
key: "status_perkawinan",
name: "Status Perkawinan",
desc: "Status perkawinan",
type: "enum",
options: enumStatusPerkawinan
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan pemilik usaha", type: "text" },
{ key: "jenis_usaha", name: "Jenis Usaha", desc: "Jenis usaha yang dijalankan", type: "text" },
{ key: "alamat_usaha", name: "Alamat Usaha", desc: "Alamat lokasi usaha", type: "text" }
]
},
{
id: "skyatimpiatu",
name: "Surat Keterangan Yatim / Piatu / Yatim Piatu",
syaratDokumen: [
{ name: "pengantar kelian", desc: "Surat Pengantar Kelian Banjar Dinas" },
{ name: "ktp/kia/kk", desc: "Fotokopi KTP, KIA, atau Kartu Keluarga" }
{
key: "pengantar_kelian",
name: "Pengantar Kelian",
desc: "Surat Pengantar Kelian Banjar Dinas"
},
{
key: "ktp_kia_kk",
name: "KTP / KIA / KK",
desc: "Fotokopi KTP, KIA, atau Kartu Keluarga"
}
],
dataText: ["nik", "nama", "tempat tanggal lahir", "jenis kelamin", "alamat", "pekerjaan", "nama ayah", "status ayah", "nama ibu", "status ibu"]
dataText: [],
dataPelengkap: [
{ key: "nik", name: "NIK", desc: "Nomor Induk Kependudukan", type: "number" },
{ key: "nama", name: "Nama Lengkap", desc: "Nama anak", type: "text" },
{ key: "tempat_lahir", name: "Tempat Lahir Anak", desc: "Tempat lahir anak", type: "text" },
{ key: "tanggal_lahir", name: "Tanggal Lahir Anak", desc: "Tanggal lahir anak", type: "date" },
{
key: "jenis_kelamin",
name: "Jenis Kelamin",
desc: "Jenis kelamin anak",
type: "enum",
options: enumJenisKelamin
},
{ key: "alamat", name: "Alamat", desc: "Alamat tempat tinggal", type: "text" },
{ key: "pekerjaan", name: "Pekerjaan", desc: "Pekerjaan (jika ada)", type: "text" },
{ key: "nama_ayah", name: "Nama Ayah", desc: "Nama ayah kandung", type: "text" },
{
key: "status_ayah",
name: "Status Ayah",
desc: "Status ayah (hidup / meninggal)",
type: "enum",
options: enumStatusHidup
},
{ key: "nama_ibu", name: "Nama Ibu", desc: "Nama ibu kandung", type: "text" },
{
key: "status_ibu",
name: "Status Ibu",
desc: "Status ibu (hidup / meninggal)",
type: "enum",
options: enumStatusHidup
}
]
}
];

View File

@@ -246,16 +246,6 @@
"label": "View Detail",
"default": true
},
{
"key": "setting.kategori_pelayanan.tambah",
"label": "Tambah",
"default": true
},
{
"key": "setting.kategori_pelayanan.edit",
"label": "Edit",
"default": true
},
{
"key": "setting.kategori_pelayanan.delete",
"label": "Delete",

31
src/lib/valueEnum.ts Normal file
View File

@@ -0,0 +1,31 @@
export const enumJenisKelamin = [
{ label: "Laki-laki", value: "Laki-laki" },
{ label: "Perempuan", value: "Perempuan" }
];
export const enumAgama = [
{ label: "Islam", value: "Islam" },
{ label: "Kristen", value: "Kristen" },
{ label: "Katolik", value: "Katolik" },
{ label: "Hindu", value: "Hindu" },
{ label: "Buddha", value: "Buddha" },
{ label: "Konghucu", value: "Konghucu" }
];
export const enumStatusHidup = [
{ label: "Hidup", value: "Hidup" },
{ label: "Meninggal", value: "Meninggal" }
];
export const enumStatusPerkawinan = [
{ label: "Belum Kawin", value: "Belum Kawin" },
{ label: "Kawin", value: "Kawin" },
{ label: "Cerai Hidup", value: "Cerai Hidup" },
{ label: "Cerai Mati", value: "Cerai Mati" }
];
export const enumStatusTempatUsaha = [
{ label: "Milik Sendiri", value: "Milik Sendiri" },
{ label: "Sewa", value: "Sewa" },
{ label: "Pinjam", value: "Pinjam" }
];

View File

@@ -1,12 +1,12 @@
import clientRoutes from "@/clientRoutes";
import {
Button,
Container,
Group,
Center,
Paper,
PasswordInput,
Stack,
Text,
TextInput,
TextInput
} from "@mantine/core";
import { useState } from "react";
import apiFetch from "../lib/apiFetch";
@@ -73,25 +73,73 @@ export default function Login() {
};
return (
<Container>
<Stack>
<Text>Login</Text>
<TextInput
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Group justify="right">
<Button onClick={handleSubmit} disabled={loading}>
<Center
h="100vh"
style={{
background:
"radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
}}
>
<Paper
radius="lg"
p="xl"
w={420}
style={{
background: "rgba(20, 20, 20, 0.75)",
backdropFilter: "blur(12px)",
border: "1px solid rgba(255, 255, 255, 0.08)",
boxShadow: "0 20px 60px rgba(0,0,0,0.6)",
}}
>
<Stack>
<Text
size="xl"
fw={700}
ta="center"
c="white"
>
Welcome Back
</Text>
<Text
size="sm"
ta="center"
c="dimmed"
>
Sign in to continue to your dashboard
</Text>
<TextInput
label="Email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<PasswordInput
label="Password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button
fullWidth
mt="md"
radius="md"
size="md"
variant="gradient"
gradient={{ from: "teal", to: "cyan", deg: 45 }}
style={{
transition: "all 0.2s ease",
}}
onClick={handleSubmit}
disabled={loading}
>
Login
</Button>
</Group>
</Stack>
</Container>
</Stack>
</Paper>
</Center>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,788 @@
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile";
import { DataNotFound } from "@/components/NotFoundPengajuanSurat";
import notification from "@/components/notificationGlobal";
import SuccessPengajuan from "@/components/SuccessPengajuanSurat";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Alert,
Anchor,
Badge,
Box,
Button,
Card,
Container,
Divider,
FileInput,
Flex,
Grid,
Group,
Modal,
Select,
Stack,
Text,
TextInput,
Tooltip,
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconBuildingCommunity,
IconFiles,
IconInfoCircle,
IconNotes,
IconUpload,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import "dayjs/locale/id";
import _ from "lodash";
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
type DataItem = {
key: string;
value: string;
};
type UpdateDataItem = {
id: string;
key: string;
value: any;
};
type FormSurat = {
kategoriId: string;
nama: string;
phone: string;
dataPelengkap: DataItem[];
syaratDokumen: DataItem[];
};
type FormUpdateSurat = {
dataPelengkap: UpdateDataItem[];
syaratDokumen: UpdateDataItem[];
};
type DataPengajuan = {
id: string;
noPengajuan: string;
category: string;
status: "antrian" | "diproses" | "selesai" | "ditolak";
createdAt: Date;
updatedAt: Date;
idSurat: string | undefined;
alasan: string | undefined | null;
};
export default function UpdateDataSurat() {
const navigate = useNavigate();
const { search } = useLocation();
const query = new URLSearchParams(search);
const noPengajuan = query.get("pengajuan");
const [found, setFound] = useState(true);
return (
<Container size="md" w={"100%"}>
<Box>
<Stack gap="lg">
{found && (
<Group justify="space-between" align="center" mt={"lg"}>
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Update Data Pengajuan Surat Administrasi
</Text>
<Text size="sm" c="dimmed">
Formulir ini digunakan untuk memperbarui data pengajuan
surat administrasi yang telah diajukan sebelumnya.
</Text>
</div>
</Group>
</Group>
)}
<Stack gap="lg" mb="lg">
{!noPengajuan ? (
<SearchData />
) : found ? (
<DataUpdate
noPengajuan={noPengajuan}
onValidate={(e) => {
setFound(e);
}}
/>
) : (
<DataNotFound
backTo={() => navigate("/darmasaba/update-data-surat")}
/>
)}
</Stack>
</Stack>
</Box>
</Container>
);
}
function FieldLabel({ label, hint }: { 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>
);
}
function FormSection({
title,
icon,
children,
description,
info,
}: {
title?: string;
icon?: React.ReactNode;
children: React.ReactNode;
description?: string;
info?: string;
}) {
return (
<Card radius="md" shadow="sm" withBorder>
<Box mb="xs">
<Group justify="apart" align="center">
<Group align="center" gap="xs">
{icon}
{title && <Text fw={700}>{title}</Text>}
</Group>
{description && <Badge variant="light">{description}</Badge>}
</Group>
{info && (
<Text size="sm" c="dimmed">
{info}
</Text>
)}
</Box>
{title && <Divider mb="sm" />}
<Stack gap="sm">{children}</Stack>
</Card>
);
}
function FileInputWrapper({
label,
placeholder,
accept,
onChange,
preview,
name,
description,
linkView,
disabled,
}: {
label: string;
placeholder?: string;
accept?: string;
linkView?: string;
onChange: (file: File | null) => void;
preview?: string | null;
name: string;
description?: string;
disabled?: boolean;
}) {
const [viewImg, setViewImg] = useState("");
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
}
}, [viewImg]);
return (
<>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
onClose={() => {
setOpenedPreviewFile(false);
}}
folder="syarat-dokumen"
fileName={viewImg}
/>
<Stack gap="xs">
<Flex direction={"column"}>
<Group justify="apart" align="center">
<Text fw={500}>{label}</Text>
</Group>
{description && (
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
{description}
</Text>
)}
{linkView && (
<Anchor onClick={() => setViewImg(linkView)} size="sm">
Lihat dokumen sebelumnya
</Anchor>
)}
</Flex>
<FileInput
accept={accept}
placeholder={placeholder}
onChange={(f) => onChange(f)}
leftSection={<IconUpload />}
aria-label={label}
name={name}
disabled={disabled}
/>
{preview ? (
<div>
<Text size="xs" color="dimmed">
Preview:
</Text>
<div style={{ marginTop: 6 }}>
<img
src={preview}
alt={`${label} preview`}
style={{ maxWidth: "200px", borderRadius: 4 }}
/>
</div>
</div>
) : null}
</Stack>
</>
);
}
function SearchData() {
const [submitLoading, setSubmitLoading] = useState(false);
const [searchPengajuan, setSearchPengajuan] = useState("");
const [searchPengajuanPhone, setSearchPengajuanPhone] = useState("");
const navigate = useNavigate();
async function handleSearch() {
try {
setSubmitLoading(true);
if (searchPengajuan == "" || searchPengajuanPhone == "") {
notification({
title: "Peringatan",
message: "Silakan isi nomor pengajuan atau nomor telephone",
type: "warning",
});
return;
}
const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({
phone: searchPengajuanPhone,
noPengajuan: searchPengajuan,
});
if (response.status === 200) {
if (response.data?.success) {
navigate(
`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`,
);
} else {
notification({
title: "Peringatan",
message: response.data?.message || "Data pengajuan tidak valid",
type: "warning",
});
}
} else {
notification({
title: "Error",
message: "Pengajuan tidak ditemukan atau gagal memuat data",
type: "error",
});
}
} catch (error) {
console.error("Error searching:", error);
notification({
title: "Error",
message: "Gagal mencari data pengajuan",
type: "error",
});
} finally {
setSubmitLoading(false);
}
}
return (
<FormSection
title="Cari Pengajuan Surat"
info="Masukkan nomor pengajuan dan nomor telepon yang digunakan saat pengajuan surat."
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Pengajuan"
hint="Nomor pengajuan surat"
/>
}
placeholder="PS-2025-000123"
onChange={(e) => {
setSearchPengajuan(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
type="number"
onChange={(e) => {
setSearchPengajuanPhone(e.target.value);
}}
/>
</Grid.Col>
<Grid.Col span={12}>
<Button
fullWidth
variant="light"
color="blue"
onClick={() => {
handleSearch();
}}
loading={submitLoading}
>
Cari Pengajuan
</Button>
</Grid.Col>
</Grid>
</FormSection>
);
}
function DataUpdate({
noPengajuan,
onValidate,
}: {
noPengajuan: string;
onValidate: (e: boolean) => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const navigate = useNavigate();
const [errors, setErrors] = useState<Record<string, string | null>>({});
const [sukses, setSukses] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
const [status, setStatus] = useState("");
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
dataPelengkap: [],
syaratDokumen: [],
});
async function fetchData() {
try {
const res = await apiFetch.api.pelayanan["detail-data"].post({
nomerPengajuan: noPengajuan,
});
if (res.data && res.data.success === true) {
onValidate(true);
setDataPelengkap(res.data.dataPelengkap || []);
setDataSyaratDokumen(res.data.syaratDokumen || []);
setDataPengajuan(res.data.pengajuan || {});
setStatus(
res.data.pengajuan && "status" in res.data.pengajuan
? res.data.pengajuan.status
: "",
);
} else {
// notification({
// title: "Error",
// message: res.data?.message || "Gagal memuat data",
// type: "error",
// });
onValidate(false);
setDataPelengkap([]);
setDataSyaratDokumen([]);
setDataPengajuan({});
}
} catch (error) {
console.error("Error fetching data:", error);
}
}
useShallowEffect(() => {
fetchData();
}, []);
function validateField(key: string, value: any) {
const stringValue = String(value ?? "").trim();
// wajib diisi
if (!stringValue) {
return "Field wajib diisi";
}
// 🔥 semua key yg mengandung "nik"
if (key.toLowerCase().includes("nik")) {
if (!/^\d+$/.test(stringValue)) {
return "NIK harus berupa angka";
}
if (stringValue.length !== 16) {
return "NIK harus 16 digit";
}
}
return null;
}
function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
const index = array.findIndex((v) => v.id === item.id);
// insert
if (index === -1) {
return [...array, item];
}
// ✏️ update
return array.map((v, i) => (i === index ? { ...v, ...item } : v));
}
function validationForm({
kategori,
value,
}: {
kategori: "dataPelengkap" | "syaratDokumen";
value: UpdateDataItem;
}) {
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,
}),
}));
}
function updateArrayByKey(
list: UpdateDataItem[],
id: string,
value: any,
): UpdateDataItem[] {
return list.map((item) => (item.id === id ? { ...item, value } : item));
}
function onChecking() {
const hasError = Object.values(errors).some((v) => v);
if (hasError) {
return notification({
title: "Gagal",
message: "Masih ada data yang belum valid",
type: "error",
});
}
if (
formSurat.dataPelengkap.length == 0 &&
formSurat.syaratDokumen.length == 0
)
return notification({
title: "Peringatan",
message: "Tidak ada data yang diupdate",
type: "warning",
});
const isFormKosong = Object.values(formSurat).some(
(value: UpdateDataItem[] | string) => {
if (Array.isArray(value)) {
return value.some(
(item) =>
typeof item.value === "string" && item.value.trim() === "",
);
}
if (typeof value === "string") {
return value.trim() === "";
}
return false;
},
);
if (isFormKosong) {
return notification({
title: "Gagal",
message: "Silahkan lengkapi form surat",
type: "error",
});
} else {
open();
}
}
async function onSubmit() {
try {
setSubmitLoading(true);
// 🔥 CLONE state SEKALI
let finalFormSurat = structuredClone(formSurat);
// 2⃣ Upload satu per satu
for (const itemUpload of finalFormSurat.syaratDokumen) {
const updImg = await apiFetch.api.pengaduan.upload.post({
file: itemUpload.value,
folder: "syarat-dokumen",
});
if (updImg.status === 200) {
// 🔥 UPDATE OBJECT LOKAL (BUKAN STATE)
finalFormSurat.syaratDokumen = updateArrayByKey(
finalFormSurat.syaratDokumen,
itemUpload.id,
updImg.data?.filename || "",
);
}
}
// 3⃣ SET STATE SEKALI (optional, untuk UI)
setFormSurat(finalFormSurat);
// 4⃣ SUBMIT KE API
const res = await apiFetch.api.pelayanan.update.post({
id: dataPengajuan && "id" in dataPengajuan ? dataPengajuan.id : "",
dataPelengkap: finalFormSurat.dataPelengkap,
syaratDokumen: finalFormSurat.syaratDokumen,
});
if (res.status === 200) {
setSukses(true);
} else {
notification({
title: "Gagal",
message:
"Pengajuan surat gagal dibuat, silahkan coba beberapa saat lagi",
type: "error",
});
}
} catch (error) {
notification({
title: "Gagal",
message: "Server Error",
type: "error",
});
} finally {
setSubmitLoading(false);
}
}
return (
<>
<FullScreenLoading visible={submitLoading} />
<Modal
opened={opened}
onClose={close}
title={"Konfirmasi"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="sm">
<Text>Apakah anda yakin ingin mengupdate pengajuan surat ini?</Text>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Tidak
</Button>
<Button
variant="filled"
color="green"
onClick={() => {
onSubmit();
close();
}}
>
Ya
</Button>
</Group>
</Stack>
</Modal>
{sukses ? (
<SuccessPengajuan
noPengajuan={noPengajuan}
onClose={() => {
navigate("/darmasaba/update-data-surat");
}}
category="update"
/>
) : (
<>
{status != "ditolak" && status != "antrian" && (
<Alert
variant="light"
color="yellow"
radius="lg"
title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
{status == "ditolak" && (
<Alert
variant="light"
color="yellow"
radius="lg"
title={`Data pengajuan surat ini ditolak, karena ${dataPengajuan && 'alasan' in dataPengajuan && dataPengajuan.alasan
? dataPengajuan.alasan
: "alasan tidak tersedia"
}. Silahkan perbaiki data pengajuan surat ini.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
<FormSection
title="Data Yang Diperlukan"
description="Data yang diperlukan"
icon={<IconNotes size={16} />}
>
<Grid>
{dataPelengkap.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
{item.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={item.name} hint={item.desc} />}
data={item.options ?? []}
placeholder={item.name}
onChange={(e) => {
validationForm({
kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: e },
});
}}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value ||
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
/>
) : item.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={item.name} hint={item.desc} />}
placeholder={item.name}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
validationForm({
kategori: "dataPelengkap",
value: {
id: item.id,
key: item.key,
value: formatted,
},
});
}}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key === item.key,
)?.value
? parseTanggalID(
formSurat.dataPelengkap.find(
(n: any) => n.key === item.key,
)?.value,
)
: parseTanggalID(item.value)
}
/>
) : (
<TextInput
error={errors[item.id]}
label={<FieldLabel label={item.name} hint={item.desc} />}
placeholder={item.name}
type={item.type}
onChange={(e) =>
validationForm({
kategori: "dataPelengkap",
value: {
id: item.id,
key: item.key,
value: e.target.value,
},
})
}
value={
formSurat.dataPelengkap.find((n) => n.id === item.id)
?.value ??
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
disabled={status != "ditolak" && status != "antrian"}
/>
)}
</Grid.Col>
))}
</Grid>
</FormSection>
<FormSection
title="Syarat Dokumen"
description="Syarat dokumen yang diperlukan"
icon={<IconFiles size={16} />}
>
<Grid>
{dataSyaratDokumen.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FileInputWrapper
label={item.desc}
placeholder={"Upload file terbaru untuk mengupdate"}
accept="image/*,application/pdf"
linkView={item.value}
onChange={(file) =>
validationForm({
kategori: "syaratDokumen",
value: { id: item.id, key: item.key, value: file },
})
}
name={item.name}
disabled={status != "ditolak" && status != "antrian"}
/>
</Grid.Col>
))}
</Grid>
</FormSection>
<Group justify="right" mt="md">
<Button
onClick={() => {
onChecking();
}}
disabled={status != "ditolak" && status != "antrian"}
>
Kirim
</Button>
</Group>
</>
)}
</>
);
}

View File

@@ -2,7 +2,9 @@ import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Anchor,
Badge,
Button,
@@ -14,23 +16,31 @@ import {
Group,
List,
Modal,
Select,
Spoiler,
Stack,
Table,
Text,
Textarea,
TextInput,
ThemeIcon,
Title,
Tooltip
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCheck,
IconEdit,
IconFileCertificate,
IconFileCheck,
IconInfoCircle,
IconMessageReport,
IconPhone,
IconUser,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
@@ -61,6 +71,7 @@ export default function DetailPengajuanPage() {
<Stack gap={"xl"}>
<DetailDataPengajuan
data={data?.data?.pengajuan}
warga={data && data.data && data.data.warga ? data.data.warga : undefined}
syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText}
onAction={() => {
@@ -80,15 +91,18 @@ export default function DetailPengajuanPage() {
function DetailDataPengajuan({
data,
warga,
syaratDokumen,
dataText,
onAction,
}: {
data: any;
warga?: { phone?: string | null } | null;
syaratDokumen: any;
dataText: any;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
const [keterangan, setKeterangan] = useState("");
@@ -97,7 +111,11 @@ function DetailDataPengajuan({
const [openedPreview, setOpenedPreview] = useState(false);
const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [viewImg, setViewImg] = useState("");
const [viewImg, setViewImg] = useState({ file: "", folder: "" });
const [uploading, setUploading] = useState({ ok: false, file: "" });
const [editValue, setEditValue] = useState({ id: "", jenis: "", val: "", option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
useEffect(() => {
async function fetchHost() {
@@ -114,22 +132,78 @@ function DetailDataPengajuan({
fetchHost();
}, []);
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try {
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "",
alasan: keterangan,
status,
linkSurat,
linkUpdate,
tlp: warga?.phone ?? "",
})
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} catch (error) {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
}
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
const statusFix = cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai"
const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai",
status: statusFix,
keterangan: keterangan,
idUser: host?.id ?? "",
noSurat: noSurat,
});
if (res?.status === 200) {
if (statusFix == "selesai") {
setTimeout(() => {
setOpenedPreview(true)
}, 1000)
} else {
sendWA({
status: statusFix,
linkSurat: "",
linkUpdate: statusFix == "ditolak" ? res.data?.linkUpdate ?? '' : '',
});
}
onAction();
close();
notification({
@@ -154,21 +228,146 @@ function DetailDataPengajuan({
}
};
async function updateDataText() {
try {
setLoadingUpdate(true)
const res = await apiFetch.api.pelayanan["update-data-pelengkap"].post({
id: editValue.id,
value: editValue.val,
jenis: editValue.key,
idUser: host?.id ?? "",
})
if (res?.status === 200) {
notification({
title: "Success",
message: "Success update data",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
}
} catch (error) {
console.error(error)
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
} finally {
setLoadingUpdate(false)
setOpenEdit(false)
onAction()
}
}
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
}
}, [viewImg]);
useShallowEffect(() => {
if (uploading.ok && uploading.file) {
sendWA({
status: "selesai",
linkSurat: uploading.file,
linkUpdate: "",
});
}
}, [uploading]);
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
return (
<>
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
onClose={() => setOpenEdit(false)}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{editValue.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={editValue.jenis} />}
data={editValue.option ?? []}
placeholder={editValue.jenis}
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
value={editValue.val}
/>
) : editValue.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
setEditValue({
...editValue,
val: formatted
})
}}
value={
editValue.val
? parseTanggalID(editValue.val)
: parseTanggalID(editValue.val)
}
/>
) : (
<TextInput
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
type={editValue.type}
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
value={editValue.val}
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
Batal
</Button>
<Button
variant="filled"
onClick={updateDataText}
disabled={loadingUpdate || !editValue.val}
loading={loadingUpdate}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
setOpenedPreviewFile(false);
}}
folder="syarat-dokumen"
fileName={viewImg}
folder={viewImg.folder}
fileName={viewImg.file}
/>
{/* MODAL KONFIRMASI */}
@@ -240,10 +439,13 @@ function DetailDataPengajuan({
)}
</Stack>
</Modal>
{data?.status == "selesai" && (
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat
open={openedPreview}
onClose={() => setOpenedPreview(false)}
onClose={(val) => {
setOpenedPreview(false)
setUploading({ ok: true, file: val })
}}
surat={data?.idSurat}
/>
)}
@@ -311,7 +513,7 @@ function DetailDataPengajuan({
<List.Item key={v.id}>
<Anchor
onClick={() => {
setViewImg(v.value);
setViewImg({ file: v.value, folder: "syarat-dokumen" });
}}
>
{v.jenis}
@@ -338,7 +540,25 @@ function DetailDataPengajuan({
</Table.Td>
<Table.Td>:</Table.Td>
<Table.Td style={{ width: "85%" }}>
{_.upperFirst(item.value)}
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Text>
{_.upperFirst(item.value)}
</Text>
<ActionIcon
variant="subtle"
aria-label="Edit"
onClick={() => {
setEditValue({ id: item.id, val: item.value, type: item.type, option: item.options, jenis: item.jenis, key: item.key })
setOpenEdit(true)
}}>
<IconEdit size={16} />
</ActionIcon>
</Flex>
</Table.Td>
</Table.Tr>
))}
@@ -400,7 +620,7 @@ function DetailDataPengajuan({
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button>
@@ -432,41 +652,48 @@ function DetailDataHistori({ data }: { data: any }) {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengajuan Surat
Riwayat Pengajuan Surat
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
<Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Spoiler>
</Stack>
</Card>
);

View File

@@ -12,6 +12,7 @@ import {
Grid,
Group,
Modal,
Spoiler,
Stack,
Table,
Text,
@@ -37,6 +38,7 @@ import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengaduanPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
@@ -60,6 +62,7 @@ export default function DetailPengaduanPage() {
<Stack gap={"xl"}>
<DetailDataPengaduan
data={data?.data?.pengaduan}
phone={data && data.data && data.data.warga ? data.data.warga.phone : null}
onAction={() => {
mutate();
}}
@@ -77,9 +80,11 @@ export default function DetailPengaduanPage() {
function DetailDataPengaduan({
data,
phone,
onAction,
}: {
data: any | null;
phone?: string | null;
onAction: () => void;
}) {
const [opened, { open, close }] = useDisclosure(false);
@@ -88,6 +93,7 @@ function DetailDataPengaduan({
const [keterangan, setKeterangan] = useState("");
const [host, setHost] = useState<User | null>(null);
const [permissions, setPermissions] = useState<JsonValue[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
async function fetchHost() {
@@ -106,6 +112,7 @@ function DetailDataPengaduan({
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setIsLoading(true);
const res = await apiFetch.api.pengaduan["update-status"].post({
id: data?.id,
status:
@@ -121,6 +128,21 @@ function DetailDataPengaduan({
});
if (res?.status === 200) {
const resWA = await apiFetch.api["send-wa"].pengaduan.post({
noPengaduan: data?.noPengaduan,
judulPengaduan: data?.title,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
alasan: keterangan,
tlp: String(phone),
})
onAction();
close();
notification({
@@ -128,6 +150,28 @@ function DetailDataPengaduan({
message: "Success update pengaduan",
type: "success",
});
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Error",
@@ -142,6 +186,8 @@ function DetailDataPengaduan({
message: "Failed to update pengaduan",
type: "error",
});
} finally {
setIsLoading(false);
}
};
@@ -176,6 +222,7 @@ function DetailDataPengaduan({
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
loading={isLoading}
>
Tolak
</Button>
@@ -200,6 +247,7 @@ function DetailDataPengaduan({
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
loading={isLoading}
>
Ya
</Button>
@@ -420,41 +468,48 @@ function DetailDataHistori({ data }: { data: any }) {
<Stack gap="md">
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Histori Pengaduan
Riwayat Pengaduan
</Title>
</Flex>
<Divider my={0} />
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
<Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table>
<Table.Thead>
<Table.Tr>
<Table.Th>Tanggal</Table.Th>
<Table.Th>Deskripsi</Table.Th>
<Table.Th>Status</Table.Th>
<Table.Th>User</Table.Th>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.Thead>
<Table.Tbody>
{data?.map((item: any) => (
<Table.Tr key={item.id}>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.createdAt.toLocaleString("id-ID", {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td style={{ whiteSpace: "nowrap" }}>
{item.nameUser ? item.nameUser : "-"}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Spoiler>
</Stack>
</Card>
);

View File

@@ -1,62 +1,66 @@
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import {
Avatar,
Box,
Button,
Card,
CloseButton,
Container,
Divider,
Flex,
Grid,
Group,
Input,
LoadingOverlay,
Pagination,
Stack,
Table,
Text,
Title,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconPhone } from "@tabler/icons-react";
import { IconPhone, IconSearch } from "@tabler/icons-react";
import _ from "lodash";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import useSwr from "swr";
export default function DetailWargaPage() {
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
const { data, mutate, isLoading } = useSwr("/", () =>
apiFetch.api.warga.detail.get({
query: {
id: id!,
},
}),
);
// const { data, mutate, isLoading } = useSwr("/", () =>
// apiFetch.api.warga.detail.get({
// query: {
// id: id!,
// },
// }),
// );
useShallowEffect(() => {
mutate();
}, []);
// useShallowEffect(() => {
// mutate();
// }, []);
return (
<>
<LoadingOverlay
visible={isLoading}
// visible={isLoading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={4}>
<DetailWarga data={data?.data?.warga} />
<DetailWarga id={id!} />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataHistori
data={data?.data?.pengaduan}
id={id!}
kategori="pengaduan"
/>
<DetailDataHistori
data={data?.data?.pelayanan}
id={id!}
kategori="pelayanan"
/>
</Stack>
@@ -68,13 +72,66 @@ export default function DetailWargaPage() {
}
function DetailDataHistori({
data,
id,
kategori,
}: {
data: any;
id: string;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate();
const [data, setData] = useState<any>([]);
const [totalPages, setTotalPages] = useState(1);
const [totalRows, setTotalRows] = useState(0);
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
async function getData() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id,
category: kategori,
page: String(page),
search
}
}) as { data: { success: boolean; data: any[]; totalPages: number, totalRows: number } };
if (res?.data?.success) {
setData(res.data.data)
setTotalPages(res?.data?.totalPages)
setTotalRows(res?.data?.totalRows)
} else {
setData([])
setTotalPages(1)
setTotalRows(0)
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data",
type: "error",
});
}
}
useShallowEffect(() => {
getData()
}, [page])
useShallowEffect(() => {
setPage(1)
if (page == 1) {
getData()
}
}, [search]);
return (
<Card
@@ -93,6 +150,36 @@ function DetailDataHistori({
<Title order={4} c="gray.2">
Histori {_.upperFirst(kategori)}
</Title>
<Flex
gap="md"
justify="flex-start"
align="center"
direction="row"
>
<Input
value={search}
placeholder="Cari data..."
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch size={16} />}
rightSectionPointerEvents="all"
rightSection={
<CloseButton
aria-label="Clear input"
onClick={() => setSearch("")}
style={{ display: search ? undefined : "none" }}
/>
}
/>
<Text size="sm" c="gray.5" >
{`${5 * (page - 1) + 1} ${Math.min(totalRows, 5 * page)} of ${totalRows}`}
</Text>
<Pagination
total={totalPages}
value={page}
onChange={setPage}
withPages={false}
/>
</Flex>
</Flex>
<Divider my={0} />
<Table>
@@ -110,7 +197,7 @@ function DetailDataHistori({
{data?.length > 0 ? (
data?.map((item: any, index: number) => (
<Table.Tr key={index}>
<Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
<Table.Td>
{kategori == "pengaduan" ? item.title : item.category}
</Table.Td>
@@ -121,11 +208,11 @@ function DetailDataHistori({
onClick={() => {
kategori == "pengaduan"
? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
)
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
)
: navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
);
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
);
}}
>
Detail
@@ -147,7 +234,33 @@ function DetailDataHistori({
);
}
function DetailWarga({ data }: { data: any }) {
function DetailWarga({ id }: { id: string }) {
const [data, setData] = useState<any>(null);
async function getWarga() {
try {
const res = await apiFetch.api.warga.detail.get({
query: {
id: id,
category: "warga",
page: "1",
search: "",
},
});
setData(res.data);
} catch (error) {
console.error(error);
notification({
title: "Failed",
message: "Failed to get data warga",
type: "error",
});
}
}
useShallowEffect(() => {
getWarga();
}, []);
return (
<Card
radius="md"

View File

@@ -8,16 +8,19 @@ export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }:
idCategory,
idWarga,
noSurat,
},
select: {
id: true
}
})
if (!surat.id) {
return { success: false, message: 'gagal membuat surat' }
return { success: false, message: 'gagal membuat surat', idSurat: '' }
}
return { success: true, message: 'surat sudah dibuat' }
return { success: true, message: 'surat sudah dibuat', idSurat: surat.id }
} catch (error) {
console.log(error)
console.error(error)
return { success: false, message: 'gagal membuat surat' }
}

View File

@@ -248,14 +248,23 @@ export async function moveFile(config: Config, oldName: string, newName: string)
return `✏️ Renamed ${oldName}${newName}`
}
export async function downloadFile(config: Config, remoteFile: string, localFile?: string): Promise<string> {
const localName = localFile || remoteFile;
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?p=/${remoteFile}`);
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
export async function downloadFile(config: Config, fileName: string, folder: string, localFile?: string): Promise<string> {
const localName = localFile || fileName;
// 🔹 gabungkan path folder + file
const filePath = `/${folder}/${fileName}`.replace(/\/+/g, "/");
// 🔹 encode path agar aman (spasi, dll)
const params = new URLSearchParams({
p: filePath,
});
const downloadUrlResponse = await fetchWithAuth(config, `${config.URL}/${config.REPO}/file/?${params.toString()}`);
if(!downloadUrlResponse.ok)
return 'gagal'
const downloadUrl = (await downloadUrlResponse.text()).replace(/"/g, '');
const buffer = Buffer.from(await (await fetchWithAuth(config, downloadUrl)).arrayBuffer());
await fs.writeFile(localName, buffer);
return `⬇️ Downloaded ${remoteFile}${localName}`
return `⬇️ Downloaded ${fileName}${localName}`
}
export async function getFileLink(config: Config, fileName: string): Promise<string> {

View File

@@ -0,0 +1,11 @@
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import "dayjs/locale/id";
dayjs.extend(customParseFormat);
export function parseTanggalID( value: string ): Date | null {
if (!value) return null;
const parsed = dayjs(value, "DD MMMM YYYY", "id", true);
return parsed.isValid() ? parsed.toDate() : null;
}

View File

@@ -5,6 +5,7 @@ import { getLastUpdated } from "../lib/get-last-updated"
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { toSlug } from "../lib/slug_converter"
const PelayananRoute = new Elysia({
@@ -20,13 +21,25 @@ const PelayananRoute = new Elysia({
},
orderBy: {
name: "asc"
},
select: {
id: true,
name: true,
syaratDokumen: true,
dataPelengkap: true,
}
})
return data
const dataFix = data.map(item => ({
...item,
link: `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/surat?jenis=${toSlug(item.name)}`
}));
return dataFix
}, {
detail: {
summary: "List Kategori Pelayanan Surat",
description: `tool untuk mendapatkan list kategori pelayanan surat beserta syaratnya untuk memenuhi syarat dokumen sesuai kategori yg dipilih saat melakukan pengajuan surat`,
description: `tool untuk mendapatkan list kategori pelayanan surat dan juga berisi link form pengajuan surat`,
tags: ["mcp"]
}
})
@@ -114,8 +127,14 @@ const PelayananRoute = new Elysia({
return;
}
const dataText: string[] = Array.isArray(data.dataText)
? data.dataText.filter((v): v is string => typeof v === "string")
const dataPelengkap: { key: string }[] = Array.isArray(data.dataPelengkap)
? data.dataPelengkap.filter(
(v): v is { key: string } =>
typeof v === "object" &&
v !== null &&
"key" in v &&
typeof (v as any).key === "string"
)
: [];
const syaratDokumen: { name: string }[] = Array.isArray(data.syaratDokumen)
@@ -132,7 +151,7 @@ const PelayananRoute = new Elysia({
return {
id: data.id,
name: data.name,
dataText,
dataPelengkap,
syaratDokumen,
};
}, {
@@ -208,8 +227,8 @@ const PelayananRoute = new Elysia({
CategoryPelayanan: {
select: {
name: true,
dataText: true,
syaratDokumen: true,
dataPelengkap: true
}
},
Warga: {
@@ -246,6 +265,7 @@ const PelayananRoute = new Elysia({
select: {
id: true,
idCategory: true,
file: true
}
})
@@ -275,10 +295,11 @@ const PelayananRoute = new Elysia({
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
name: string;
desc: string;
key: string;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
return {
id: item.id,
jenis: desc,
@@ -286,14 +307,40 @@ const PelayananRoute = new Elysia({
}
})
const dataTextFix = dataText.map((item) => {
const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
return {
id: item.id,
jenis: item.jenis,
value: item.value,
}
})
const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
type: string;
options?: {
label: string,
value: string
}[]; name: string;
desc: string;
key: string;
}[];
const refMap = new Map(
dataTextCategory.map((v, i) => [
v.key,
{ ...v, order: i }
])
);
const dataTextFix = dataText
.map((item) => {
const ref = refMap.get(item.jenis);
const nama = dataTextCategory.find((v) => v.key == item.jenis)?.name
return {
id: item.id,
jenis: nama,
key: ref?.key,
value: item.value,
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
};
})
.sort((a, b) => a.order - b.order)
.map(({ order, ...rest }) => rest); // hapus order
const dataHistory = await prisma.historyPelayanan.findMany({
where: {
@@ -310,6 +357,9 @@ const PelayananRoute = new Elysia({
name: true,
}
}
},
orderBy: {
createdAt: "desc"
}
})
@@ -339,6 +389,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
idSurat: dataSurat?.id,
fileSurat: dataSurat?.file,
}
const datafix = {
@@ -360,9 +411,9 @@ const PelayananRoute = new Elysia({
}
})
.post("/create", async ({ body, headers }) => {
const { kategoriId, dataText, syaratDokumen } = body
const namaWarga = headers['x-user'] || ""
const noTelepon = headers['x-phone'] || ""
const { kategoriId, dataPelengkap, syaratDokumen, nama, phone } = body
// const namaWarga = headers['x-user'] || ""
// const noTelepon = headers['x-phone'] || ""
const noPengajuan = await generateNoPengajuanSurat()
let idCategoryFix = kategoriId
let idWargaFix = ""
@@ -388,21 +439,21 @@ const PelayananRoute = new Elysia({
}
if (!isValidPhone(noTelepon)) {
if (!isValidPhone(phone)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
}
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
const nomorHP = normalizePhoneNumber({ phone: phone })
const dataWarga = await prisma.warga.upsert({
where: {
phone: nomorHP
},
create: {
name: namaWarga,
name: nama,
phone: nomorHP,
},
update: {
name: namaWarga,
name: nama,
},
select: {
id: true
@@ -433,16 +484,16 @@ const PelayananRoute = new Elysia({
dataInsertSyaratDokumen.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
jenis: item.key,
value: item.value,
})
}
for (const item of dataText) {
for (const item of dataPelengkap) {
dataInsertDataText.push({
idPengajuanLayanan: pengaduan.id,
idCategory: idCategoryFix,
jenis: item.jenis,
jenis: item.key,
value: item.value,
})
}
@@ -464,7 +515,7 @@ const PelayananRoute = new Elysia({
}
})
return { success: true, message: 'pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' }
return { success: true, message: 'Pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini', noPengajuan }
}, {
body: t.Object({
kategoriId: t.String({
@@ -472,21 +523,20 @@ const PelayananRoute = new Elysia({
examples: ["skusaha"],
error: "ID kategori harus diisi"
}),
// namaWarga: t.String({
// description: "Nama warga",
// examples: ["Budi Santoso"],
// error: "Nama warga harus diisi"
// }),
nama: t.String({
description: "Nama warga",
examples: ["Budi Santoso"],
error: "Nama warga harus diisi"
}),
phone: t.String({
error: "Nomor telepon harus diisi",
examples: ["08123456789", "+628123456789"],
description: "Nomor telepon warga pelapor"
}),
// noTelepon: t.String({
// error: "Nomor telepon harus diisi",
// examples: ["08123456789", "+628123456789"],
// description: "Nomor telepon warga pelapor"
// }),
dataText: t.Array(
dataPelengkap: t.Array(
t.Object({
jenis: t.String({
key: t.String({
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "jenis harus diisi"
@@ -501,25 +551,25 @@ const PelayananRoute = new Elysia({
description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [
[
{ jenis: "nama", value: "Budi Santoso" },
{ jenis: "jenis kelamin", value: "Laki-laki" },
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ jenis: "negara", value: "Indonesia" },
{ jenis: "agama", value: "Islam" },
{ jenis: "status perkawinan", value: "Belum menikah" },
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
{ jenis: "jenis usaha", value: "usaha makanan" },
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
{ key: "nama", value: "Budi Santoso" },
{ key: "jenis kelamin", value: "Laki-laki" },
{ key: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ key: "negara", value: "Indonesia" },
{ key: "agama", value: "Islam" },
{ key: "status perkawinan", value: "Belum menikah" },
{ key: "alamat", value: "Jl. Mawar No. 10" },
{ key: "pekerjaan", value: "Karyawan Swasta" },
{ key: "jenis usaha", value: "usaha makanan" },
{ key: "alamat usaha", value: "Jl. Melati No. 21" },
]
],
error: "dataText harus berupa array"
error: "Data Pelengkap harus berupa array"
}
),
syaratDokumen: t.Array(
t.Object({
jenis: t.String({
key: t.String({
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "jenis harus diisi"
@@ -534,26 +584,28 @@ const PelayananRoute = new Elysia({
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
examples: [
[
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ jenis: "ktp/kk", value: "kk_budi.png" },
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
{ key: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ key: "ktp/kk", value: "kk_budi.png" },
{ key: "foto lokasi", value: "foto_lokasi_budi.png" }
]
],
error: "syaratDokumen harus berupa array"
error: "Syarat Dokumen harus berupa array"
}
),
}),
detail: {
summary: "Buat Pengajuan Pelayanan Surat",
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
tags: ["mcp"]
}
})
.post("/detail-data", async ({ body }) => {
const { nomerPengajuan } = body
const data = await prisma.pelayananAjuan.findFirst({
where: {
noPengajuan: nomerPengajuan
noPengajuan: {
equals: nomerPengajuan,
mode: "insensitive"
}
},
select: {
id: true,
@@ -564,7 +616,7 @@ const PelayananRoute = new Elysia({
CategoryPelayanan: {
select: {
name: true,
dataText: true,
dataPelengkap: true,
syaratDokumen: true,
}
},
@@ -584,7 +636,15 @@ const PelayananRoute = new Elysia({
})
if (!data) {
return { success: false, message: "Data tidak ditemukan" }
return {
success: false,
message: "Data tidak ditemukan",
pengajuan: {},
history: [],
warga: {},
syaratDokumen: [],
dataPelengkap: [],
}
}
const dataSurat = await prisma.suratPelayanan.findFirst({
@@ -610,7 +670,7 @@ const PelayananRoute = new Elysia({
}
})
const dataText = await prisma.dataTextPelayanan.findMany({
const dataPelengkap = await prisma.dataTextPelayanan.findMany({
where: {
idPengajuanLayanan: data?.id,
isActive: true
@@ -624,25 +684,57 @@ const PelayananRoute = new Elysia({
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
name: string;
desc: string;
key: string;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
// const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
return {
id: item.id,
jenis: item.jenis,
key: item.jenis,
value: item.value,
name: name ?? '',
desc: desc ?? ''
}
})
const dataTextFix = dataText.map((item) => {
// const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
return {
id: item.id,
jenis: item.jenis,
value: item.value,
}
})
const dataPelengkapList = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
type: string;
options?: {
label: string,
value: string
}[];
name: string;
desc: string;
key: string;
}[];
const refMap = new Map(
dataPelengkapList.map((v, i) => [
v.key,
{ ...v, order: i }
])
);
const dataTextFix = dataPelengkap
.map((item) => {
const ref = refMap.get(item.jenis);
return {
id: item.id,
key: item.jenis,
value: item.value,
desc: ref?.desc ?? "",
name: ref?.name ?? "",
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
};
})
.sort((a, b) => a.order - b.order)
.map(({ order, ...rest }) => rest); // hapus order
const dataHistory = await prisma.historyPelayanan.findMany({
where: {
@@ -662,6 +754,19 @@ const PelayananRoute = new Elysia({
}
})
const alasanDitolak = await prisma.historyPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
status: "ditolak"
},
select: {
keteranganAlasan: true,
},
orderBy: {
createdAt: "desc"
}
})
const dataHistoryFix = dataHistory.map((item) => {
return {
id: item.id,
@@ -688,18 +793,20 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
idSurat: dataSurat?.id,
alasan: alasanDitolak?.keteranganAlasan,
}
const datafix = {
success: true,
message: 'sukses',
pengajuan: dataPengajuan,
linkUpdate: `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/update-data-surat?pengajuan=${data.noPengajuan}`,
history: dataHistoryFix,
warga: warga,
syaratDokumen: dataSyaratFix,
dataText: dataTextFix,
dataPelengkap: dataTextFix,
}
console.log('detail data syarat==', dataSyaratFix)
return datafix
}, {
@@ -736,9 +843,14 @@ const PelayananRoute = new Elysia({
})
if (!pengajuan) {
return { success: false, message: 'gagal update status pengajuan surat' }
return { success: false, message: 'gagal update status pengajuan surat', linkUpdate: '', idSurat: '' }
}
const dataPengajuan = await prisma.pelayananAjuan.findUnique({
where: { id: pengajuan.id },
select: { noPengajuan: true }
});
if (status === "diterima") {
deskripsi = "Pengajuan surat diterima"
} else if (status === "ditolak") {
@@ -757,11 +869,19 @@ const PelayananRoute = new Elysia({
}
})
let idSurat = "";
if (status === "selesai") {
await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
const result = await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
idSurat = result.idSurat ?? "";
}
return { success: true, message: 'pengajuan surat sudah diperbarui' }
return {
success: true,
message: 'pengajuan surat sudah diperbarui',
linkUpdate: status == "ditolak" ? `${process.env.BUN_PUBLIC_BASE_URL}/darmasaba/update-data-surat?pengajuan=${dataPengajuan?.noPengajuan}` : '',
idSurat: idSurat,
}
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
@@ -776,51 +896,34 @@ const PelayananRoute = new Elysia({
}
})
.post("/update", async ({ body }) => {
const { nomerPengajuan, syaratDokumen, dataText } = body
const { id, syaratDokumen, dataPelengkap } = body
let dataUpdate = []
console.log(body)
const pengajuan = await prisma.pelayananAjuan.findFirst({
const pengajuan = await prisma.pelayananAjuan.findUnique({
where: {
noPengajuan: nomerPengajuan,
id
}
})
if (!pengajuan) {
console.log("data pengajuan surat tidak ditemukan")
return { success: false, message: 'data pengajuan surat tidak ditemukan' }
}
if (pengajuan.status != "ditolak" && pengajuan.status != "antrian") {
console.log("pengajuan surat tidak dapat diupdate karena status " + pengajuan.status)
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
}
if (dataText && dataText.length > 0) {
console.log("dataText")
for (const item of dataText) {
dataUpdate.push(item.jenis)
const hasil = await prisma.dataTextPelayanan.findFirst({
where: {
idPengajuanLayanan: pengajuan.id,
jenis: item.jenis,
}
})
if (dataPelengkap && dataPelengkap.length > 0) {
for (const item of dataPelengkap) {
dataUpdate.push(item.key)
const upd = await prisma.dataTextPelayanan.upsert({
const upd = await prisma.dataTextPelayanan.update({
where: {
id: hasil?.id
id: item.id
},
update: {
data: {
value: item.value,
},
create: {
value: item.value,
jenis: item.jenis,
idPengajuanLayanan: pengajuan.id,
idCategory: pengajuan.idCategory,
}
})
@@ -842,38 +945,16 @@ const PelayananRoute = new Elysia({
if (syaratDokumen && syaratDokumen.length > 0) {
console.log("syaratDokumen")
for (const item of syaratDokumen) {
const pilih = syarat?.find((cat) => cat.desc.toLowerCase() == item.jenis.toLowerCase() || cat.name.toLowerCase() == item.jenis.toLowerCase())?.name;
console.log(syarat, pilih)
dataUpdate.push(pilih)
const hasil = await prisma.syaratDokumenPelayanan.findFirst({
dataUpdate.push(item.key)
const upd = await prisma.syaratDokumenPelayanan.update({
where: {
idPengajuanLayanan: pengajuan.id,
jenis: pilih,
id: item.id
},
data: {
value: item.value,
}
})
console.log(hasil, item)
if (hasil && hasil.id) {
const upd = await prisma.syaratDokumenPelayanan.upsert({
where: {
id: hasil.id
},
update: {
value: item.value,
},
create: {
value: item.value,
jenis: hasil.jenis,
idPengajuanLayanan: pengajuan.id,
idCategory: pengajuan.idCategory,
}
})
} else {
return { success: false, message: 'dokumen tidak dapat diupload' }
}
}
}
@@ -898,22 +979,25 @@ const PelayananRoute = new Elysia({
}
})
console.log("pengajuan surat sudah diperbarui")
return { success: true, message: 'pengajuan surat sudah diperbarui' }
}, {
body: t.Object({
nomerPengajuan: t.String({
error: "nomer pengajuan harus diisi",
description: "Nomer pengajuan yang ingin diupdate"
id: t.String({
error: "id harus diisi",
description: "ID yang ingin diupdate"
}),
dataText: t.Optional(t.Array(
dataPelengkap: t.Optional(t.Array(
t.Object({
jenis: t.String({
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
id: t.String({
description: "ID Data Pelengkap.",
error: "id harus diisi"
}),
key: t.String({
description: "Key field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
error: "jenis harus diisi"
error: "key harus diisi"
}),
value: t.String({
description: "Isi atau nilai dari jenis field terkait.",
@@ -925,26 +1009,30 @@ const PelayananRoute = new Elysia({
description: "Kumpulan data text dinamis sesuai kategori layanan.",
examples: [
[
{ jenis: "nama", value: "Budi Santoso" },
{ jenis: "jenis kelamin", value: "Laki-laki" },
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ jenis: "negara", value: "Indonesia" },
{ jenis: "agama", value: "Islam" },
{ jenis: "status perkawinan", value: "Belum menikah" },
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
{ jenis: "jenis usaha", value: "usaha makanan" },
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
{ id: "1", key: "nama", value: "Budi Santoso" },
{ id: "2", key: "jenis kelamin", value: "Laki-laki" },
{ id: "3", key: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
{ id: "4", key: "negara", value: "Indonesia" },
{ id: "5", key: "agama", value: "Islam" },
{ id: "6", key: "status perkawinan", value: "Belum menikah" },
{ id: "7", key: "alamat", value: "Jl. Mawar No. 10" },
{ id: "8", key: "pekerjaan", value: "Karyawan Swasta" },
{ id: "9", key: "jenis usaha", value: "usaha makanan" },
{ id: "10", key: "alamat usaha", value: "Jl. Melati No. 21" },
]
],
}
)),
syaratDokumen: t.Optional(t.Array(
t.Object({
jenis: t.String({
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
id: t.String({
description: "ID syarat dokumen",
error: "id harus diisi"
}),
key: t.String({
description: "Key dokumen persyaratan yang diminta oleh kategori layanan.",
examples: ["ktp", "kk", "surat_pengantar_rt"],
error: "jenis harus diisi"
error: "key harus diisi"
}),
value: t.String({
description: "Nama file atau identifier file dokumen yang diupload.",
@@ -956,9 +1044,9 @@ const PelayananRoute = new Elysia({
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
examples: [
[
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ jenis: "ktp/kk", value: "kk_budi.png" },
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
{ id: "1", key: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
{ id: "2", key: "ktp/kk", value: "kk_budi.png" },
{ id: "3", key: "foto lokasi", value: "foto_lokasi_budi.png" }
]
],
}
@@ -967,7 +1055,73 @@ const PelayananRoute = new Elysia({
detail: {
summary: "Update Data Pengajuan Pelayanan Surat",
description: `tool untuk update data pengajuan pelayanan surat`,
tags: ["mcp"]
}
})
.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 }) => {
@@ -1114,5 +1268,47 @@ const PelayananRoute = new Elysia({
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
}
})
.post("/get-no-pengajuan", async ({ body }) => {
const { phone, noPengajuan } = body;
if (!isValidPhone(phone)) {
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
}
const nomorHP = normalizePhoneNumber({ phone: phone })
const data = await prisma.pelayananAjuan.findMany({
where: {
noPengajuan: noPengajuan,
Warga: {
phone: nomorHP
}
},
select: {
id: true,
noPengajuan: true,
status: true,
createdAt: true,
}
});
if (data.length == 0) {
return { success: false, message: 'Data pengajuan tidak ditemukan' };
}
return {
success: true,
nomer: noPengajuan
};
}, {
body: t.Object({
phone: t.String({ minLength: 1, error: "Nomor telepon harus diisi" }),
noPengajuan: t.String({ minLength: 1, error: "Nomor pengajuan harus diisi" }),
}),
detail: {
summary: "Cek Nomor Pengajuan Surat",
description: `tool untuk memeriksa apakah nomor pengajuan surat valid dan terkait dengan nomor telepon warga. Jika valid, mengembalikan nomor pengajuan.`,
}
})
export default PelayananRoute

View File

@@ -1,14 +1,16 @@
import Elysia, { t } from "elysia"
import type { StatusPengaduan } from "generated/prisma"
import _ from "lodash"
import { v4 as uuidv4 } from "uuid"
import { getLastUpdated } from "../lib/get-last-updated"
import { mimeToExtension } from "../lib/mimetypeToExtension"
import { generateNoPengaduan } from "../lib/no-pengaduan"
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
import { prisma } from "../lib/prisma"
import { renameFile } from "../lib/rename-file"
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile"
import Elysia, { t } from "elysia";
import fs from 'fs';
import type { StatusPengaduan } from "generated/prisma";
import _ from "lodash";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { getLastUpdated } from "../lib/get-last-updated";
import { mimeToExtension } from "../lib/mimetypeToExtension";
import { generateNoPengaduan } from "../lib/no-pengaduan";
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone";
import { prisma } from "../lib/prisma";
import { renameFile } from "../lib/rename-file";
import { catFile, defaultConfigSF, downloadFile, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile";
const PengaduanRoute = new Elysia({
prefix: "pengaduan",
@@ -415,7 +417,7 @@ const PengaduanRoute = new Elysia({
const datafix = {
pengaduan: {},
history: [],
warga: {},
warga: null,
}
return datafix
@@ -436,6 +438,9 @@ const PengaduanRoute = new Elysia({
name: true,
}
}
},
orderBy: {
createdAt: "desc"
}
})
@@ -602,6 +607,43 @@ const PengaduanRoute = new Elysia({
consumes: ["multipart/form-data"]
},
})
.get("/download", async ({ query, set }) => {
const { file, folder } = query;
// Validasi file
if (!file) {
return { success: false, message: "File tidak ditemukan" };
}
// if (!folder) {
// return { success: false, message: "Folder tidak ditemukan" };
// }
const localPath = path.join("/tmp", file);
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer();
const result = await downloadFile(defaultConfigSF, file, 'surat', localPath);
if(result=="gagal") {
return { success: false, message: "Download gagal" };
}
set.headers["Content-Type"] = "application/pdf";
set.headers["Content-Disposition"] = `attachment; filename="${file}"`;
// 🔹 kirim file ke browser
return fs.createReadStream(localPath);
}, {
body: t.Object({
file: t.Any(),
folder: t.String(),
}),
detail: {
summary: "Download Surat",
description: "Tool untuk download surat dari Seafile",
},
})
.post("/upload-file-form-data", async ({ body }) => {
const { file } = body;

View File

@@ -0,0 +1,153 @@
import Elysia, { t } from "elysia";
const SendWaRoute = new Elysia({
prefix: "send-wa",
tags: ["send-wa"],
})
// --- KATEGORI PENGADUAN ---
.post("/pengaduan", async ({ body }) => {
const { noPengaduan, judulPengaduan, status, alasan, tlp } = body
let text = ""
if (status === "ditolak") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Kami informasikan bahwa aduan tersebut tidak dapat ditindaklanjuti (ditolak).
Alasan penolakan:${alasan}
Terima kasih atas pengertian Bapak/Ibu.`
} else if (status == "diterima") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Telah kami terima dan akan segera diproses sesuai ketentuan yang berlaku.
Terima kasih atas laporan Bapak/Ibu.`
} else if (status == "dikerjakan") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Saat ini sedang dalam proses penanganan oleh petugas terkait.
Mohon menunggu informasi selanjutnya.`
} else if (status == "selesai") {
text = `Pemberitahuan Aduan
Aduan dengan Nomor Pengaduan: ${noPengaduan}
Judul Pengaduan: ${judulPengaduan}
Telah selesai ditindaklanjuti.
Terima kasih atas partisipasi dan kepercayaan Bapak/Ibu.`
}
const textFix = encodeURIComponent(text)
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
}
);
if (res.status !== 200)
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
}, {
body: t.Object({
noPengaduan: t.String({ minLength: 1, error: "nomer pengaduan harus diisi" }),
judulPengaduan: t.String({ minLength: 1, error: "judul pengaduan harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
alasan: t.String({ optional: true }),
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
}),
detail: {
summary: "Send pemberitahuan pengaduan lewat WA",
description: `tool untuk send pemberitahuan pengaduan lewat WA`
}
})
.post("/pengajuan-surat", async ({ body }) => {
const { noPengajuan, jenisSurat, status, alasan, tlp, linkSurat, linkUpdate } = body
let text = ""
if (status === "ditolak") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut tidak dapat diproses (ditolak).
Alasan penolakan: ${alasan}
Bapak/Ibu dapat melakukan perbaikan atau pembaruan data melalui tautan berikut:
👉 ${linkUpdate}
Setelah data diperbarui, pengajuan akan diproses kembali sesuai ketentuan yang berlaku.
Terima kasih atas pengertian Bapak/Ibu.`
} else if (status == "diterima") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat yang Bapak/Ibu ajukan telah kami terima dan sedang menunggu proses verifikasi serta penanganan lebih lanjut.
Terima kasih atas kesabaran Bapak/Ibu.`
} else if (status == "selesai") {
text = `Pemberitahuan Pengajuan Surat
Nomor Pengajuan: ${noPengajuan}
Surat: ${jenisSurat}
Kami informasikan bahwa pengajuan surat tersebut telah selesai diproses.
Bapak/Ibu dapat mengunduh surat melalui tautan berikut:
👉 ${linkSurat}
Terima kasih atas kepercayaan Bapak/Ibu.`
}
const textFix = encodeURIComponent(text)
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
Authorization: `Bearer ${process.env.WA_SERVER_TOKEN}`,
},
}
);
if (res.status !== 200)
return { success: false, message: "Nomor Whatsapp Tidak Aktif" }
return { success: true, message: 'Pemberitahuan berhasil dikirim ke warga' }
}, {
body: t.Object({
noPengajuan: t.String({ minLength: 1, error: "nomer pengajuan harus diisi" }),
jenisSurat: t.String({ minLength: 1, error: "jenis surat harus diisi" }),
status: t.String({ minLength: 1, error: "status harus diisi" }),
alasan: t.String({ optional: true }),
linkSurat: t.String({ optional: true }),
linkUpdate: t.String({ optional: true }),
tlp: t.String({ minLength: 1, error: "nomor telepon harus diisi" }),
}),
detail: {
summary: "Send pemberitahuan pengajuan surat lewat WA",
description: `tool untuk send pemberitahuan pengajuan surat lewat WA`
}
})
;
export default SendWaRoute

View File

@@ -17,6 +17,7 @@ const SuratRoute = new Elysia({
noSurat: true,
idCategory: true,
createdAt: true,
file: true,
PelayananAjuan: {
select: {
DataTextPelayanan: true,
@@ -44,6 +45,7 @@ const SuratRoute = new Elysia({
idCategory: dataSurat?.idCategory,
nameCategory: dataSurat?.CategoryPelayanan?.name,
noSurat: dataSurat?.noSurat,
file: dataSurat?.file,
dataText: dataSurat?.PelayananAjuan?.DataTextPelayanan,
createdAt: dataSurat?.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
},
@@ -60,6 +62,33 @@ const SuratRoute = new Elysia({
}
})
.post("/update", async ({ body }) => {
const { id, filename } = body
await prisma.suratPelayanan.update({
where: {
id,
},
data: {
file: filename,
}
})
return {
success: true,
message: 'surat sudah diperbarui',
link: `${process.env.BUN_PUBLIC_BASE_URL}/api/pengaduan/download?file=${filename}`
}
}, {
body: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
filename: t.String({ minLength: 1, error: "filename harus diisi" }),
}),
detail: {
summary: "update file surat",
description: `tool untuk update file surat`
}
})
;
export default SuratRoute

View File

@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
}
})
.get("/detail", async ({ query }) => {
const { id } = query
const { id, category, search, page } = query
const skip = !page ? 0 : (Number(page) - 1) * 5
const dataWarga = await prisma.warga.findUnique({
where: {
id
}
})
const dataPengaduan = await prisma.pengaduan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
if (!dataWarga)
return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
if (category == "warga") {
return dataWarga
} else if (category == "pengaduan") {
const where: any = {
isActive: true,
idWarga: id
},
select: {
id: true,
status: true,
noPengaduan: true,
title: true
idWarga: id,
OR: [
{
title: {
contains: search ?? "",
mode: "insensitive"
},
},
{
noPengaduan: {
contains: search ?? "",
mode: "insensitive"
},
}
]
}
})
const totalData = await prisma.pengaduan.count({
where
});
const dataPengaduan = await prisma.pengaduan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
status: true,
noPengaduan: true,
title: true
}
})
const dataReturn = {
success: true,
message: "data pengaduan berhasil diambil",
data: dataPengaduan,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
}
const dataPelayanan = await prisma.pelayananAjuan.findMany({
orderBy: {
createdAt: "desc"
},
where: {
return dataReturn
} else if (category == "pelayanan") {
const where: any = {
isActive: true,
idWarga: id
},
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
idWarga: id,
OR: [
{
CategoryPelayanan: {
name: {
contains: search ?? "",
mode: "insensitive"
},
},
},
{
noPengajuan: {
contains: search ?? "",
mode: "insensitive"
},
},
]
}
const totalData = await prisma.pelayananAjuan.count({
where
});
const dataPelayanan = await prisma.pelayananAjuan.findMany({
skip,
take: 5,
orderBy: {
createdAt: "desc"
},
where,
select: {
id: true,
noPengajuan: true,
status: true,
CategoryPelayanan: {
select: {
name: true
}
}
}
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
const dataReturn = {
success: true,
message: "data pelayanan berhasil diambil",
data: dataPelayanFix,
totalRows: totalData,
totalPages: Math.ceil(totalData / 5)
}
})
const dataPelayanFix = dataPelayanan.map((v: any) => ({
..._.omit(v, ["CategoryPelayanan"]),
id: v.id,
noPengaduan: v.noPengajuan,
status: v.status,
category: v.CategoryPelayanan.name
}))
return {
warga: dataWarga,
pengaduan: dataPengaduan,
pelayanan: dataPelayanFix
return dataReturn
}
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" })
id: t.String({ minLength: 1, error: "id harus diisi" }),
category: t.String({ minLength: 1, error: "kategori harus diisi" }),
page: t.String({ optional: true }),
search: t.String({ optional: true }),
}),
detail: {
summary: "Detail Warga",