Compare commits

...

52 Commits

Author SHA1 Message Date
acb82388db upd: migrasi 2026-03-27 16:10:36 +08:00
f2a66beeb3 upd: url wa 2026-03-27 15:33:27 +08:00
724484b875 Merge pull request 'upd: api noc' (#118) from amalia/25-mar-26 into main
Reviewed-on: #118
2026-03-25 17:03:56 +08:00
1eb708ae59 upd: api noc 2026-03-25 17:01:50 +08:00
e4406fbcf0 Merge pull request 'upd: api noc' (#117) from amalia/17-mar-26 into main
Reviewed-on: #117
2026-03-17 15:39:55 +08:00
0c8b9d1667 upd: api noc 2026-03-17 15:38:44 +08:00
a8aa0f8d63 Merge pull request 'upd : api noc dashboard' (#116) from amalia/16-mar-26 into main
Reviewed-on: #116
2026-03-16 16:13:12 +08:00
39cb8d8391 upd : api noc dashboard 2026-03-16 16:06:08 +08:00
d6abc163fb Merge pull request 'upd: tambah satuan' (#115) from amalia/06-feb-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/115
2026-02-06 17:46:25 +08:00
9c08980bf1 upd: tambah satuan
Deskripsi:
- satuan luas tempat usaha
- satuan pendapatan perbulan
- pada tambah, edit, detail surat

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

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

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

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

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

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

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

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

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

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

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

nb: blm selesai

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

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

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

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

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

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

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

No Issues;
2026-01-06 17:00:08 +08:00
4ca5e4c4f3 Merge pull request 'upd: pengajuan surat' (#100) from amalia/05-jan-26 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/100
2026-01-05 17:25:27 +08:00
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
28 changed files with 2333 additions and 572 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>
</>
);
}

View File

@@ -0,0 +1,271 @@
-- CreateEnum
CREATE TYPE "StatusPengaduan" AS ENUM ('diterima', 'antrian', 'dikerjakan', 'ditolak', 'selesai');
-- CreateTable
CREATE TABLE "Role" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"permissions" JSONB,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Role_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"roleId" TEXT,
"name" TEXT,
"email" TEXT,
"password" TEXT,
"phone" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ApiKey" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"key" TEXT NOT NULL,
"description" TEXT,
"expiredAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Credential" (
"id" TEXT NOT NULL,
"name" TEXT,
"value" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Credential_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CategoryPengaduan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CategoryPengaduan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Pengaduan" (
"id" TEXT NOT NULL,
"idCategory" TEXT NOT NULL,
"idWarga" TEXT NOT NULL,
"noPengaduan" TEXT NOT NULL,
"title" TEXT,
"detail" TEXT,
"location" TEXT,
"image" TEXT,
"keterangan" TEXT,
"status" "StatusPengaduan" NOT NULL DEFAULT 'antrian',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Pengaduan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HistoryPengaduan" (
"id" TEXT NOT NULL,
"idPengaduan" TEXT NOT NULL,
"idUser" TEXT,
"deskripsi" TEXT,
"status" "StatusPengaduan" NOT NULL DEFAULT 'antrian',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HistoryPengaduan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Warga" (
"id" TEXT NOT NULL,
"name" TEXT,
"phone" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Warga_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CategoryPelayanan" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"syaratDokumen" JSONB[],
"dataText" TEXT[] DEFAULT ARRAY[]::TEXT[],
"dataPelengkap" JSONB[] DEFAULT ARRAY[]::JSONB[],
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CategoryPelayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PelayananAjuan" (
"id" TEXT NOT NULL,
"idWarga" TEXT NOT NULL,
"idCategory" TEXT NOT NULL,
"noPengajuan" TEXT NOT NULL,
"status" "StatusPengaduan" NOT NULL DEFAULT 'antrian',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PelayananAjuan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HistoryPelayanan" (
"id" TEXT NOT NULL,
"idPengajuanLayanan" TEXT NOT NULL,
"idUser" TEXT,
"deskripsi" TEXT,
"keteranganAlasan" TEXT,
"status" "StatusPengaduan" NOT NULL DEFAULT 'antrian',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HistoryPelayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SyaratDokumenPelayanan" (
"id" TEXT NOT NULL,
"idPengajuanLayanan" TEXT NOT NULL,
"idCategory" TEXT NOT NULL,
"jenis" TEXT NOT NULL,
"value" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SyaratDokumenPelayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DataTextPelayanan" (
"id" TEXT NOT NULL,
"idPengajuanLayanan" TEXT NOT NULL,
"idCategory" TEXT NOT NULL,
"jenis" TEXT NOT NULL,
"value" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DataTextPelayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SuratPelayanan" (
"id" TEXT NOT NULL,
"idPengajuanLayanan" TEXT NOT NULL,
"idCategory" TEXT NOT NULL,
"idWarga" TEXT NOT NULL,
"noSurat" TEXT NOT NULL,
"file" TEXT,
"dateExpired" DATE,
"status" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SuratPelayanan_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Configuration" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Configuration_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
-- CreateIndex
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
-- CreateIndex
CREATE UNIQUE INDEX "Warga_phone_key" ON "Warga"("phone");
-- AddForeignKey
ALTER TABLE "User" ADD CONSTRAINT "User_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pengaduan" ADD CONSTRAINT "Pengaduan_idCategory_fkey" FOREIGN KEY ("idCategory") REFERENCES "CategoryPengaduan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Pengaduan" ADD CONSTRAINT "Pengaduan_idWarga_fkey" FOREIGN KEY ("idWarga") REFERENCES "Warga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HistoryPengaduan" ADD CONSTRAINT "HistoryPengaduan_idPengaduan_fkey" FOREIGN KEY ("idPengaduan") REFERENCES "Pengaduan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HistoryPengaduan" ADD CONSTRAINT "HistoryPengaduan_idUser_fkey" FOREIGN KEY ("idUser") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PelayananAjuan" ADD CONSTRAINT "PelayananAjuan_idWarga_fkey" FOREIGN KEY ("idWarga") REFERENCES "Warga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PelayananAjuan" ADD CONSTRAINT "PelayananAjuan_idCategory_fkey" FOREIGN KEY ("idCategory") REFERENCES "CategoryPelayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HistoryPelayanan" ADD CONSTRAINT "HistoryPelayanan_idPengajuanLayanan_fkey" FOREIGN KEY ("idPengajuanLayanan") REFERENCES "PelayananAjuan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HistoryPelayanan" ADD CONSTRAINT "HistoryPelayanan_idUser_fkey" FOREIGN KEY ("idUser") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SyaratDokumenPelayanan" ADD CONSTRAINT "SyaratDokumenPelayanan_idPengajuanLayanan_fkey" FOREIGN KEY ("idPengajuanLayanan") REFERENCES "PelayananAjuan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SyaratDokumenPelayanan" ADD CONSTRAINT "SyaratDokumenPelayanan_idCategory_fkey" FOREIGN KEY ("idCategory") REFERENCES "CategoryPelayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DataTextPelayanan" ADD CONSTRAINT "DataTextPelayanan_idPengajuanLayanan_fkey" FOREIGN KEY ("idPengajuanLayanan") REFERENCES "PelayananAjuan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DataTextPelayanan" ADD CONSTRAINT "DataTextPelayanan_idCategory_fkey" FOREIGN KEY ("idCategory") REFERENCES "CategoryPelayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SuratPelayanan" ADD CONSTRAINT "SuratPelayanan_idPengajuanLayanan_fkey" FOREIGN KEY ("idPengajuanLayanan") REFERENCES "PelayananAjuan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SuratPelayanan" ADD CONSTRAINT "SuratPelayanan_idCategory_fkey" FOREIGN KEY ("idCategory") REFERENCES "CategoryPelayanan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SuratPelayanan" ADD CONSTRAINT "SuratPelayanan_idWarga_fkey" FOREIGN KEY ("idWarga") REFERENCES "Warga"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

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

View File

@@ -0,0 +1,44 @@
import { ActionIcon, Anchor, Breadcrumbs, Card, Group } from "@mantine/core";
import { IconChevronLeft } from "@tabler/icons-react";
import { useNavigate } from "react-router-dom";
export default function BreadCrumbs({ dataLink, back, linkBack }: { dataLink: { title: string, link: string, active: boolean }[], back?: boolean, linkBack?: string }) {
const navigate = useNavigate();
return (
<Card
radius="md"
p="sm"
withBorder
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}}
>
<Group>
{
back &&
<ActionIcon variant="outline" aria-label="Settings" radius={"lg"} onClick={() => window.history.back()}>
<IconChevronLeft size={20} stroke={1.5} />
</ActionIcon>
}
<Breadcrumbs>
{
dataLink.map((item, index) => (
<Anchor
c={item.active ? "gray.0" : "gray.5"}
onClick={() => item.active || item.link == "#" ? null : navigate(item.link)}
key={index}
>
{item.title}
</Anchor>
))
}
</Breadcrumbs>
</Group>
</Card>
)
}

View File

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

View File

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

View File

@@ -1,10 +1,9 @@
import apiFetch from "@/lib/apiFetch";
import { ActionIcon, Flex, Modal } from "@mantine/core";
import { Flex, Modal, Progress, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { IconDownload, IconX } from "@tabler/icons-react";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
import { useRef } from "react";
import { useRef, useState } from "react";
import useSWR from "swr";
import SKBedaBiodataDiri from "./surat/SKBedaBiodataDiri";
import SKBelumKawin from "./surat/SKBelumKawin";
@@ -24,7 +23,7 @@ export default function ModalSurat({
surat,
}: {
open: boolean;
onClose: () => void;
onClose: (val: { success: boolean, data: string }) => void;
surat: string;
}) {
const A4Style = {
@@ -36,6 +35,7 @@ export default function ModalSurat({
fontSize: "14px",
fontFamily: "Times New Roman",
};
const [uploading, setUploading] = useState<{ text: "Menyiapkan" | "Mengupload" | "Selesai" | "Gagal", value: number }>({ text: "Menyiapkan", value: 10 })
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
@@ -45,42 +45,97 @@ export default function ModalSurat({
}),
);
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 uploadPdf = async () => {
try {
if (data && data.data && data.data.surat && (data.data.surat.file == "" || data.data.surat.file == null)) {
setUploading({ text: "Mengupload", value: 75 });
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const imgData = canvas.toDataURL("image/jpeg", 1.0);
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
const imgWidth = pageWidth;
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
pdf.save(`${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!,
});
if (resUpdate?.data?.success) {
setUploading({ text: "Selesai", value: 100 });
setTimeout(() => {
onClose({ success: true, data: resUpdate.data?.link });
}, 1000)
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose({ success: false, data: "" });
}, 1000)
}
} else {
setUploading({ text: "Gagal", value: 100 });
setTimeout(() => {
onClose({ success: false, data: "" });
}, 1000)
}
} catch (error) {
console.error("Error uploading PDF:", error);
}
}
useShallowEffect(() => {
if (open) {
setTimeout(() => {
uploadPdf();
}, 3000);
}
}, [surat, open]);
return (
<>
<Modal
opened={open}
onClose={() => onClose()}
onClose={() => { }}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
closeOnClickOutside={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
@@ -94,19 +149,24 @@ export default function ModalSurat({
},
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<>
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
<Flex gap={8}>
<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}</Text>
</Flex> */}
</Flex>
</Flex>
<Stack
align="stretch"
justify="center"
gap="xs"
>
<Text size="sm" ta="center">{uploading.text} - Harap menunggu sampai selesai</Text>
<Progress radius="md" value={uploading.value} animated size="lg" />
</Stack>
</>
}
>
<div ref={hiddenRef} style={A4Style}>

View File

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

View File

@@ -90,7 +90,7 @@ export default function SKTempatUsaha({ data }: { data: any }) {
<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="Luas Tempat Usaha" value={getValue("luas_usaha") + " m2"} />
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
</div>

View File

@@ -20,6 +20,7 @@ import SuratRoute from "./server/routes/surat_route";
import TestPengaduanRoute from "./server/routes/test_pengaduan";
import UserRoute from "./server/routes/user_route";
import WargaRoute from "./server/routes/warga_route";
import NocRoute from "./server/routes/noc_route";
const Docs = new Elysia({
tags: ["docs"],
@@ -47,7 +48,8 @@ const Api = new Elysia({
.use(UserRoute)
.use(LayananRoute)
.use(AduanRoute)
.use(SendWaRoute);
.use(SendWaRoute)
.use(NocRoute);
const app = new Elysia()
.use(Api)

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,12 @@
import BreadCrumbs from "@/components/BreadCrumbs";
import FullScreenLoading from "@/components/FullScreenLoading";
import ModalFile from "@/components/ModalFile";
import ModalSurat from "@/components/ModalSurat";
import notification from "@/components/notificationGlobal";
import apiFetch from "@/lib/apiFetch";
import { parseTanggalID } from "@/server/lib/stringToDate";
import {
ActionIcon,
Anchor,
Badge,
Button,
@@ -14,24 +18,31 @@ import {
Group,
List,
Modal,
Select,
Spoiler,
Stack,
Table,
Text,
Textarea,
TextInput,
ThemeIcon,
Title,
Tooltip
} from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import {
IconAlignJustified,
IconCheck,
IconEdit,
IconFileCertificate,
IconFileCheck,
IconInfoCircle,
IconMessageReport,
IconPhone,
IconUser,
} from "@tabler/icons-react";
import dayjs from "dayjs";
import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library";
import _ from "lodash";
@@ -40,6 +51,11 @@ import { useLocation } from "react-router-dom";
import useSwr from "swr";
export default function DetailPengajuanPage() {
const dataMenu = [
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
{ title: "Pelayanan Surat", link: "/scr/dashboard/pelayanan-surat/list-pelayanan", active: false },
{ title: "Detail Pengajuan Surat", link: "#", active: true },
];
const { search } = useLocation();
const query = new URLSearchParams(search);
const id = query.get("id");
@@ -58,10 +74,14 @@ export default function DetailPengajuanPage() {
return (
<Container size="xl" py="xl" w={"100%"}>
<Grid>
<Grid.Col span={12}>
<BreadCrumbs dataLink={dataMenu} back />
</Grid.Col>
<Grid.Col span={8}>
<Stack gap={"xl"}>
<DetailDataPengajuan
data={data?.data?.pengajuan}
warga={data && data.data && data.data.warga ? data.data.warga : undefined}
syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText}
onAction={() => {
@@ -81,15 +101,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("");
@@ -98,7 +121,12 @@ 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: "", satuan: null as string | null, option: null as any, type: "", key: "" })
const [openEdit, setOpenEdit] = useState(false)
const [loadingUpdate, setLoadingUpdate] = useState(false)
const [loadingFS, setLoadingFS] = useState({ value: false, text: "" })
useEffect(() => {
async function fetchHost() {
@@ -115,22 +143,88 @@ function DetailDataPengajuan({
fetchHost();
}, []);
async function sendWA({ status, linkSurat, linkUpdate }: { status: string, linkSurat: string, linkUpdate: string }) {
try {
setLoadingFS({ value: true, text: "Sending message to warga" })
const resWA = await apiFetch.api["send-wa"]["pengajuan-surat"].post({
noPengajuan: data?.noPengajuan ?? "",
jenisSurat: data?.category ?? "",
alasan: keterangan,
status,
linkSurat,
linkUpdate,
tlp: warga?.phone ?? "",
})
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
if (status == "selesai") {
onAction()
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} catch (error) {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
finally {
setLoadingFS({ value: false, text: "" })
}
}
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
try {
setLoadingFS({ value: true, text: "Updating status" })
const statusFix = cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai"
const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai",
status: statusFix,
keterangan: keterangan,
idUser: host?.id ?? "",
noSurat: noSurat,
});
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({
@@ -152,24 +246,158 @@ function DetailDataPengajuan({
message: "Failed to update pengajuan surat",
type: "error",
});
} finally {
setLoadingFS({ value: false, text: "" })
}
};
async function updateDataText() {
try {
setLoadingUpdate(true)
const res = await apiFetch.api.pelayanan["update-data-pelengkap"].post({
id: editValue.id,
value: editValue.val,
jenis: editValue.key,
idUser: host?.id ?? "",
})
if (res?.status === 200) {
notification({
title: "Success",
message: "Success update data",
type: "success",
})
} else {
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
}
} catch (error) {
console.error(error)
notification({
title: "Error",
message: "Failed to update data",
type: "error",
})
} finally {
setLoadingUpdate(false)
setOpenEdit(false)
onAction()
}
}
useShallowEffect(() => {
if (viewImg) {
setOpenedPreviewFile(true);
}
}, [viewImg]);
useShallowEffect(() => {
if (uploading.ok && uploading.file) {
sendWA({
status: "selesai",
linkSurat: uploading.file,
linkUpdate: "",
});
}
}, [uploading]);
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
return (
<>
<FullScreenLoading visible={loadingFS.value} text={loadingFS.text} />
{/* MODAL EDIT DATA PELENGKAP */}
<Modal
opened={openEdit}
onClose={() => setOpenEdit(false)}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="ld">
{editValue.type == "enum" ? (
<Select
allowDeselect={false}
label={<FieldLabel label={editValue.jenis} />}
data={editValue.option ?? []}
placeholder={editValue.jenis}
onChange={(e) => { setEditValue({ ...editValue, val: e ?? "" }) }}
value={editValue.val}
/>
) : editValue.type == "date" ? (
<DateInput
locale="id"
valueFormat="DD MMMM YYYY"
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
onChange={(e) => {
const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY")
: "";
setEditValue({
...editValue,
val: formatted
})
}}
value={
editValue.val
? parseTanggalID(editValue.val)
: parseTanggalID(editValue.val)
}
/>
) : (
<TextInput
label={<FieldLabel label={editValue.jenis} />}
placeholder={editValue.jenis}
type={editValue.type}
onChange={(e) => { setEditValue({ ...editValue, val: e.target.value }) }}
value={editValue.val}
rightSection={
editValue.satuan != null &&
<Text mr={"lg"}>{editValue.satuan}</Text>
}
/>
)}
<Group justify="center" grow>
<Button variant="light" onClick={() => { setOpenEdit(false) }}>
Batal
</Button>
<Button
variant="filled"
onClick={updateDataText}
disabled={loadingUpdate || !editValue.val}
loading={loadingUpdate}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
<ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)}
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
onClose={() => {
setOpenedPreviewFile(false);
setViewImg({ file: "", folder: "" })
}}
folder="syarat-dokumen"
fileName={viewImg}
folder={viewImg.folder}
fileName={viewImg.file}
/>
{/* MODAL KONFIRMASI */}
@@ -241,10 +469,15 @@ function DetailDataPengajuan({
)}
</Stack>
</Modal>
{data?.status == "selesai" && (
{/* MODAL PREVIEW SURAT */}
{data?.status == "selesai" && !data?.fileSurat && (
<ModalSurat
open={openedPreview}
onClose={() => setOpenedPreview(false)}
onClose={(val) => {
setOpenedPreview(false)
setUploading({ ok: val.success, file: val.data })
}}
surat={data?.idSurat}
/>
)}
@@ -312,7 +545,7 @@ function DetailDataPengajuan({
<List.Item key={v.id}>
<Anchor
onClick={() => {
setViewImg(v.value);
setViewImg({ file: v.value, folder: "syarat-dokumen" });
}}
>
{v.jenis}
@@ -339,7 +572,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)} {item.satuan}
</Text>
<ActionIcon
variant="subtle"
aria-label="Edit"
onClick={() => {
setEditValue({ id: item.id, val: item.value, type: item.type, satuan: item.satuan, option: item.options, jenis: item.jenis, key: item.key })
setOpenEdit(true)
}}>
<IconEdit size={16} />
</ActionIcon>
</Flex>
</Table.Td>
</Table.Tr>
))}
@@ -397,18 +648,31 @@ function DetailDataPengajuan({
Setujui
</Button>
</Group>
) : data?.status === "selesai" ? (
<Group justify="center" grow>
<Button
variant="light"
onClick={() => setOpenedPreview(!openedPreview)}
>
Surat
</Button>
</Group>
) : (
<></>
)}
) : data?.status === "selesai" ?
!data?.fileSurat ?
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setOpenedPreview(true) }}
>
Kirim Ulang Surat
</Button>
</Group>
)
:
(
<Group justify="center" grow>
<Button
variant="light"
onClick={() => { setViewImg({ file: data?.fileSurat, folder: "surat" }) }}
>
Surat
</Button>
</Group>
) : (
<></>
)}
</Grid.Col>
</Grid>
</Stack>

View File

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

View File

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

View File

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

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,359 @@
import Elysia from "elysia";
import { prisma } from "../lib/prisma";
const NocRoute = new Elysia({
prefix: "noc",
tags: ["noc"],
})
.get("/surat-perminggu", async () => {
const now = new Date();
const startOfThisWeek = new Date(now);
const day = now.getDay();
const diff = (day === 0 ? 6 : day - 1); // Adjust for Monday as start (Sunday=0 becomes 6, Monday=1 becomes 0)
startOfThisWeek.setDate(now.getDate() - diff);
startOfThisWeek.setHours(0, 0, 0, 0);
const endOfThisWeek = new Date(startOfThisWeek);
endOfThisWeek.setDate(startOfThisWeek.getDate() + 7);
const startOfLastWeek = new Date(startOfThisWeek);
startOfLastWeek.setDate(startOfThisWeek.getDate() - 7);
const endOfLastWeek = new Date(startOfThisWeek);
const [thisWeekCount, lastWeekCount] = await Promise.all([
prisma.suratPelayanan.count({
where: {
isActive: true,
createdAt: {
gte: startOfThisWeek,
lt: endOfThisWeek,
}
}
}),
prisma.suratPelayanan.count({
where: {
isActive: true,
createdAt: {
gte: startOfLastWeek,
lt: endOfLastWeek,
}
}
})
]);
let percentageIncrease = 0;
if (lastWeekCount > 0) {
percentageIncrease = ((thisWeekCount - lastWeekCount) / lastWeekCount) * 100;
} else if (thisWeekCount > 0) {
percentageIncrease = 100;
}
return {
jumlah: thisWeekCount, // jumlah surat minggu ini
persentase_kenaikan: Number(percentageIncrease.toFixed(2)) // persentase kenaikan dari minggu lalu
};
}, {
detail: {
summary: "Jumlah surat minggu ini",
description: `Menu beranda - tool untuk mendapatkan jumlah surat minggu ini dan persentase kenaikan dibandingkan minggu lalu`,
}
})
.get("/pengaduan-count", async () => {
const [antrian, diterima, dikerjakan, ditolak, selesai] = await Promise.all([
prisma.pengaduan.count({
where: {
isActive: true,
status: "antrian",
}
}),
prisma.pengaduan.count({
where: {
isActive: true,
status: "dikerjakan",
}
}),
prisma.pengaduan.count({
where: {
isActive: true,
status: "diterima",
}
}),
prisma.pengaduan.count({
where: {
isActive: true,
status: "ditolak",
}
}),
prisma.pengaduan.count({
where: {
isActive: true,
status: "selesai",
}
})
]);
return {
antrian,
diterima,
dikerjakan,
ditolak,
selesai,
aktif: antrian + diterima + dikerjakan,
total: antrian + diterima + dikerjakan + ditolak + selesai
};
}, {
detail: {
summary: "Jumlah pengaduan berdasarkan status",
description: `Menu beranda dan pengaduan layanan publik - Menghitung jumlah pengaduan yang sedang aktif (antrian, diterima, dikerjakan), dan total (termasuk ditolak dan selesai)`,
}
})
.get("/pelayanan-count", async () => {
const now = new Date();
// Bulan ini
const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const endOfThisMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
// Bulan lalu
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const [thisMonthCount, lastMonthCount] = await Promise.all([
prisma.pelayananAjuan.count({
where: {
isActive: true,
status: "selesai",
createdAt: {
gte: startOfThisMonth,
lt: endOfThisMonth,
}
}
}),
prisma.pelayananAjuan.count({
where: {
isActive: true,
status: "selesai",
createdAt: {
gte: startOfLastMonth,
lt: endOfLastMonth,
}
}
})
]);
let percentageIncrease = 0;
if (lastMonthCount > 0) {
percentageIncrease = ((thisMonthCount - lastMonthCount) / lastMonthCount) * 100;
} else if (thisMonthCount > 0) {
percentageIncrease = 100;
}
return {
total_bulan_ini: thisMonthCount,
persentase_kenaikan: Number(percentageIncrease.toFixed(2))
};
}, {
detail: {
summary: "Total pelayanan selesai bulan ini dan kenaikan dari bulan lalu",
description: `Menu beranda - Menampilkan total pelayanan yang telah berstatus selesai bulan ini dan persentase kenaikan dari bulan lalu`,
}
})
.get("/pengajuan-history", async ({ query }) => {
const { period = "6months" } = query as { period?: string };
const now = new Date();
const results: { label: string; total: number }[] = [];
if (period === "6weeks") {
// Get the most recent Monday
const currentDay = now.getDay();
const diffToMonday = (currentDay === 0 ? 6 : currentDay - 1);
const startOfCurrentWeek = new Date(now);
startOfCurrentWeek.setDate(now.getDate() - diffToMonday);
startOfCurrentWeek.setHours(0, 0, 0, 0);
for (let i = 5; i >= 0; i--) {
const startOfWeek = new Date(startOfCurrentWeek);
startOfWeek.setDate(startOfCurrentWeek.getDate() - (i * 7));
const endOfWeek = new Date(startOfWeek);
endOfWeek.setDate(startOfWeek.getDate() + 7);
const count = await prisma.pelayananAjuan.count({
where: {
isActive: true,
createdAt: {
gte: startOfWeek,
lt: endOfWeek,
}
}
});
const label = `Minggu ${6 - i}`;
results.push({ label, total: count });
}
} else {
// Default 6 months
for (let i = 5; i >= 0; i--) {
const startOfMonth = new Date(now.getFullYear(), now.getMonth() - i, 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
const count = await prisma.pelayananAjuan.count({
where: {
isActive: true,
createdAt: {
gte: startOfMonth,
lt: endOfMonth,
}
}
});
const monthName = startOfMonth.toLocaleString('id-ID', { month: 'long' });
results.push({ label: monthName, total: count });
}
}
return results;
}, {
detail: {
summary: "Statistik pengajuan surat 6 bulan / 6 minggu",
description: `Menu beranda - Menampilkan statistik pengajuan surat selama 6 bulan terakhir atau 6 minggu terakhir`,
}
})
.get("/pengaduan-history", async ({ query }) => {
const { period = "6months" } = query as { period?: string };
const now = new Date();
const results: { label: string; total: number }[] = [];
if (period === "7days") {
for (let i = 6; i >= 0; i--) {
const startOfDay = new Date(now);
startOfDay.setDate(now.getDate() - i);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(startOfDay);
endOfDay.setDate(startOfDay.getDate() + 1);
const count = await prisma.pengaduan.count({
where: {
isActive: true,
createdAt: {
gte: startOfDay,
lt: endOfDay,
}
}
});
const label = startOfDay.toLocaleDateString('id-ID', { weekday: 'long' });
results.push({ label, total: count });
}
} else {
// Default 6 months
for (let i = 5; i >= 0; i--) {
const startOfMonth = new Date(now.getFullYear(), now.getMonth() - i, 1);
const endOfMonth = new Date(now.getFullYear(), now.getMonth() - i + 1, 1);
const count = await prisma.pengaduan.count({
where: {
isActive: true,
createdAt: {
gte: startOfMonth,
lt: endOfMonth,
}
}
});
const monthName = startOfMonth.toLocaleString('id-ID', { month: 'long' });
results.push({ label: monthName, total: count });
}
}
return results;
}, {
detail: {
summary: "Statistik total pengaduan 6 bulan / 7 hari",
description: `Menu pengaduan layanan publik - Menampilkan statistik total pengaduan selama 6 bulan terakhir atau 7 hari terakhir`,
}
})
.get("/pelayanan-perjenis", async () => {
const categories = await prisma.categoryPelayanan.findMany({
where: {
isActive: true
},
select: {
name: true,
_count: {
select: {
SuratPelayanan: {
where: {
isActive: true
}
}
}
}
}
});
return categories
.map(cat => ({
jenis: cat.name,
jumlah: cat._count.SuratPelayanan
}))
.sort((a, b) => b.jumlah - a.jumlah);
}, {
detail: {
summary: "Jumlah surat terbanyak berdasarkan jenis surat",
description: `Menu pengaduan layanan publik - Menampilkan jumlah surat berdasarkan jenis/kategori pelayanan`,
}
})
.get("/pengajuan-terbaru", async () => {
const applications = await prisma.pelayananAjuan.findMany({
where: {
isActive: true
},
take: 5,
orderBy: {
createdAt: "desc"
},
include: {
Warga: true,
CategoryPelayanan: true
}
});
const formatDuration = (date: Date) => {
const diff = Math.floor((new Date().getTime() - date.getTime()) / 1000);
if (diff < 60) return `${diff} detik yang lalu`;
if (diff < 3600) return `${Math.floor(diff / 60)} menit yang lalu`;
if (diff < 86400) return `${Math.floor(diff / 3600)} jam yang lalu`;
if (diff < 604800) return `${Math.floor(diff / 86400)} hari yang lalu`;
return date.toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric"
});
};
return applications.map(app => ({
jenis: app.CategoryPelayanan.name,
status: app.status,
namaWarga: app.Warga.name,
durasi: formatDuration(app.createdAt)
}));
}, {
detail: {
summary: "5 data pengajuan surat terbaru",
description: `Menu pengaduan layanan publik - Menampilkan 5 data pengajuan surat terbaru beserta status, nama warga, dan durasi pengajuan`,
}
})
export default NocRoute

View File

@@ -265,6 +265,7 @@ const PelayananRoute = new Elysia({
select: {
id: true,
idCategory: true,
file: true
}
})
@@ -307,9 +308,14 @@ const PelayananRoute = new Elysia({
})
const dataTextCategory = (data?.CategoryPelayanan?.dataPelengkap ?? []) as {
name: string;
type: string;
options?: {
label: string,
value: string
}[]; name: string;
desc: string;
key: string;
satuan?: string;
}[];
const refMap = new Map(
@@ -326,8 +332,12 @@ const PelayananRoute = new Elysia({
return {
id: item.id,
jenis: nama,
key: ref?.key,
value: item.value,
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
satuan: ref?.satuan ?? null
};
})
.sort((a, b) => a.order - b.order)
@@ -381,6 +391,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt,
updatedAt: data?.updatedAt,
idSurat: dataSurat?.id,
fileSurat: dataSurat?.file,
}
const datafix = {
@@ -676,17 +687,20 @@ const PelayananRoute = new Elysia({
name: string;
desc: string;
key: string;
required: boolean;
}[];
const dataSyaratFix = dataSyarat.map((item) => {
const desc = syaratDokumen.find((v) => v.key == item.jenis)?.desc
const name = syaratDokumen.find((v) => v.key == item.jenis)?.name
const required = syaratDokumen.find((v) => v.key == item.jenis)?.required
return {
id: item.id,
key: item.jenis,
value: item.value,
name: name ?? '',
desc: desc ?? ''
desc: desc ?? '',
required: required ?? true
}
})
@@ -699,6 +713,8 @@ const PelayananRoute = new Elysia({
name: string;
desc: string;
key: string;
required: boolean;
satuan?: string;
}[];
const refMap = new Map(
@@ -721,6 +737,8 @@ const PelayananRoute = new Elysia({
type: ref?.type ?? "",
options: ref?.options ?? [],
order: ref?.order ?? Infinity,
required: ref?.required ?? true,
satuan: ref?.satuan ?? null
};
})
.sort((a, b) => a.order - b.order)
@@ -834,9 +852,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") {
@@ -855,11 +878,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" }),
@@ -1035,6 +1066,73 @@ const PelayananRoute = new Elysia({
description: `tool untuk update data pengajuan pelayanan surat`,
}
})
.post("/update-data-pelengkap", async ({ body }) => {
const { id, value, jenis, idUser } = body
const dataPelengkap = await prisma.dataTextPelayanan.findUnique({
where: {
id
},
select: {
idPengajuanLayanan: true,
PelayananAjuan: {
select: {
status: true
}
}
}
})
if (!dataPelengkap) {
return { success: false, message: 'data pelengkap surat tidak ditemukan' }
}
const upd = await prisma.dataTextPelayanan.update({
where: {
id
},
data: {
value: value,
}
})
const history = await prisma.historyPelayanan.create({
data: {
idPengajuanLayanan: dataPelengkap.idPengajuanLayanan,
deskripsi: `Pengajuan surat diupdate oleh user (data yg diupdate: ${jenis})`,
status: dataPelengkap.PelayananAjuan.status,
idUser
}
})
return { success: true, message: 'data pelengkap surat sudah diperbarui' }
}, {
body: t.Object({
id: t.String({
error: "id harus diisi",
description: "ID yang ingin diupdate"
}),
value: t.String({
error: "value harus diisi",
description: "Value yang ingin diupdate"
}),
jenis: t.String({
error: "jenis harus diisi",
description: "Jenis data yang ingin diupdate"
}),
idUser: t.String({
error: "idUser harus diisi",
description: "ID user yang melakukan update"
})
}),
detail: {
summary: "Update Data Pelengkap Pengajuan Pelayanan Surat oleh user admin",
description: `tool untuk update data pelengkap pengajuan pelayanan surat oleh user admin`,
}
})
.get("/list", async ({ query }) => {
const { take, page, search, status } = query
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))

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",
@@ -605,6 +607,43 @@ const PengaduanRoute = new Elysia({
consumes: ["multipart/form-data"]
},
})
.get("/download", async ({ query, set }) => {
const { file, folder } = query;
// Validasi file
if (!file) {
return { success: false, message: "File tidak ditemukan" };
}
// if (!folder) {
// return { success: false, message: "Folder tidak ditemukan" };
// }
const localPath = path.join("/tmp", file);
// Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
// const buffer = await file.arrayBuffer();
const result = await downloadFile(defaultConfigSF, file, 'surat', localPath);
if(result=="gagal") {
return { success: false, message: "Download gagal" };
}
set.headers["Content-Type"] = "application/pdf";
set.headers["Content-Disposition"] = `attachment; filename="${file}"`;
// 🔹 kirim file ke browser
return fs.createReadStream(localPath);
}, {
body: t.Object({
file: t.Any(),
folder: t.String(),
}),
detail: {
summary: "Download Surat",
description: "Tool untuk download surat dari Seafile",
},
})
.post("/upload-file-form-data", async ({ body }) => {
const { file } = body;

View File

@@ -49,7 +49,7 @@ 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}`,
`${process.env.WA_SERVER_URL}/api/wa/code?nom=${tlp}&text=${textFix}`,
{
cache: "no-cache",
headers: {
@@ -76,6 +76,78 @@ Terima kasih atas partisipasi dan kepercayaan Bapak/Ibu.`
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(
`${process.env.WA_SERVER_URL}/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",