Merge pull request 'upd: form surat' (#90) from amalia/17-des-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/90
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { capitalizeWords, fromSlug, toSlug } from "@/server/lib/slug_converter";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
@@ -7,28 +9,300 @@ import {
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
FileButton,
|
||||
Grid,
|
||||
Group,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Textarea,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconBuildingCommunity,
|
||||
IconInfoCircle,
|
||||
IconMapPin,
|
||||
IconUpload,
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
// =========================
|
||||
// Reusable UI components
|
||||
// =========================
|
||||
type DataItem = {
|
||||
jenis: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
kategoryId: string;
|
||||
nama: string;
|
||||
phone: string;
|
||||
dataText: DataItem[];
|
||||
syaratDokumen: DataItem[];
|
||||
};
|
||||
|
||||
|
||||
export default function FormSurat() {
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const jenisSurat = query.get("jenis");
|
||||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||||
apiFetch.api.pelayanan.category.get(),
|
||||
);
|
||||
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
||||
const [dataSurat, setDataSurat] = useState<any>({})
|
||||
const [formSurat, setFormSurat] = useState<FormSurat>({
|
||||
nama: "",
|
||||
phone: "",
|
||||
kategoryId: "",
|
||||
dataText: [],
|
||||
syaratDokumen: [],
|
||||
})
|
||||
|
||||
const listCategory = data?.data || [];
|
||||
|
||||
function onGetJenisSurat() {
|
||||
try {
|
||||
if (!jenisSurat || jenisSurat == "null") {
|
||||
setJenisSuratFix({ name: "", id: "" });
|
||||
} else {
|
||||
const namaJenis = fromSlug(jenisSurat);
|
||||
const data = listCategory.find((item: any) => item.name == namaJenis);
|
||||
if (!data) return;
|
||||
setJenisSuratFix(data);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDetailJenisSurat() {
|
||||
try {
|
||||
const get: any = await apiFetch.api.pelayanan.category.detail.get({
|
||||
query: {
|
||||
id: jenisSuratFix.id,
|
||||
},
|
||||
})
|
||||
setDataSurat(get.data)
|
||||
setFormSurat({
|
||||
kategoryId: jenisSuratFix.id,
|
||||
nama: "",
|
||||
phone: "",
|
||||
dataText: (get.data?.dataText || []).map((item: string) => ({
|
||||
jenis: item,
|
||||
value: "",
|
||||
})),
|
||||
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||||
(item: { name: string }) => ({
|
||||
jenis: item.name,
|
||||
value: "",
|
||||
})
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (listCategory.length > 0) {
|
||||
onGetJenisSurat();
|
||||
}
|
||||
}, [jenisSurat, listCategory]);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (jenisSuratFix.id != "") {
|
||||
getDetailJenisSurat();
|
||||
}
|
||||
}, [jenisSuratFix.id]);
|
||||
|
||||
function onSubmit() {
|
||||
const isFormKosong = Object.values(formSurat).some((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
value.length === 0 ||
|
||||
value.some((item) => !item.value?.trim())
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isFormKosong) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Silahkan lengkapi form surat",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("READY SUBMIT", formSurat);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"}>
|
||||
<Box>
|
||||
<Stack gap="lg">
|
||||
<Group justify="apart" align="center">
|
||||
<Group align="center">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<div>
|
||||
<Text fw={800} size="xl">
|
||||
Surat Keterangan Tidak Mampu (SKTM)
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu —
|
||||
digunakan untuk keperluan pendidikan, kesehatan, atau
|
||||
administrasi.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group>
|
||||
<Badge radius="sm">Form Length: 3 Sections</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
{/* Header Section */}
|
||||
<FormSection
|
||||
title="Pemohon"
|
||||
icon={<IconUser size={16} />}
|
||||
description="Informasi identitas pemohon"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nama Lengkap"
|
||||
hint="Nama lengkap pemohon"
|
||||
/>
|
||||
}
|
||||
placeholder="Budi Setiawan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nomor Telephone"
|
||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||||
/>
|
||||
}
|
||||
placeholder="08123456789"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
label={<FieldLabel label="Jenis Surat" hint="Jenis surat yang ingin diajukan" />}
|
||||
placeholder="Pilih jenis surat"
|
||||
data={listCategory.map((item: any) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}))}
|
||||
value={jenisSuratFix.name}
|
||||
onChange={(value) => {
|
||||
const slug = toSlug(String(value))
|
||||
navigate("/darmasaba/surat?jenis=" + slug)
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{
|
||||
jenisSuratFix.id != "" && dataSurat && dataSurat.dataText &&
|
||||
<>
|
||||
<FormSection
|
||||
title="Data Pelengkap"
|
||||
description="Data pelengkap yang diperlukan"
|
||||
>
|
||||
<Grid>
|
||||
{
|
||||
dataSurat.dataText.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
|
||||
/>
|
||||
}
|
||||
placeholder={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Syarat Dokumen"
|
||||
description="Syarat dokumen yang diperlukan"
|
||||
>
|
||||
<Grid>
|
||||
{
|
||||
dataSurat.syaratDokumen.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<FieldLabelUpload
|
||||
label={item.desc}
|
||||
/>
|
||||
<FileButton
|
||||
onChange={async (file) => {
|
||||
if (!file) return;
|
||||
// const base64 = await fileToBase64(file);
|
||||
// form.setFieldValue("foto", base64);
|
||||
// setFotoName(file.name);
|
||||
}}
|
||||
accept="image/*"
|
||||
>
|
||||
{(props) => (
|
||||
<Button
|
||||
leftSection={<IconUpload size={16} />}
|
||||
{...props}
|
||||
mt="sm"
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
</Grid.Col>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{/* Actions */}
|
||||
<Group justify="right" mt="md">
|
||||
<Button variant="default" onClick={() => { }}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>Kirim</Button>
|
||||
</Group>
|
||||
</>
|
||||
|
||||
}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
return (
|
||||
@@ -45,6 +319,36 @@ function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabelUpload({
|
||||
label,
|
||||
description,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Group justify="apart" style={{ width: "100%" }}>
|
||||
<Group gap={6}>
|
||||
<Text size="sm" fw={600}>
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<ActionIcon size={18} variant="subtle" aria-hidden>
|
||||
<IconInfoCircle size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
@@ -71,261 +375,3 @@ function FormSection({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// Main form component
|
||||
// =========================
|
||||
|
||||
export default function FormSurat() {
|
||||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||||
apiFetch.api.pelayanan.category.get(),
|
||||
);
|
||||
|
||||
const listCategory = data?.data || [];
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"}>
|
||||
<Box>
|
||||
<Stack gap="lg">
|
||||
<Group justify="apart" align="center">
|
||||
<Group align="center">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<div>
|
||||
<Text fw={800} size="xl">
|
||||
Surat Keterangan Tidak Mampu (SKTM)
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu —
|
||||
digunakan untuk keperluan pendidikan, kesehatan, atau
|
||||
administrasi.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group>
|
||||
<Badge radius="sm">Form Length: 3 Sections</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<form>
|
||||
<Stack gap="lg">
|
||||
{/* Header Section */}
|
||||
<FormSection
|
||||
title="Pemohon"
|
||||
icon={<IconUser size={16} />}
|
||||
description="Informasi identitas pemohon"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nama Lengkap"
|
||||
hint="Nama lengkap pemohon"
|
||||
/>
|
||||
}
|
||||
placeholder="Budi Setiawan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nomor Telephone"
|
||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||||
/>
|
||||
}
|
||||
placeholder="08123456789"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
label={<FieldLabel label="Jenis Surat" hint="Jenis surat yang ingin diajukan" />}
|
||||
placeholder="Pilih jenis surat"
|
||||
data={listCategory.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: item.name,
|
||||
}))}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Syarat Dokumen"
|
||||
description="Syarat dokumen yang diperlukan"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nama Lengkap"
|
||||
hint="Sesuai KTP"
|
||||
/>
|
||||
}
|
||||
placeholder="Nama lengkap"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="NIK"
|
||||
hint="16 digit, tanpa spasi"
|
||||
/>
|
||||
}
|
||||
placeholder="3201xxxxxxxxxxxx"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Tempat, Tanggal Lahir"
|
||||
hint="Contoh: Denpasar, 31-12-1990"
|
||||
/>
|
||||
}
|
||||
placeholder="Tempat, tanggal lahir"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Select
|
||||
label={<FieldLabel label="Jenis Kelamin" />}
|
||||
placeholder="Pilih jenis kelamin"
|
||||
data={["Laki-laki", "Perempuan"]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Select
|
||||
label={<FieldLabel label="Agama" />}
|
||||
placeholder="Pilih agama"
|
||||
data={[
|
||||
"Islam",
|
||||
"Kristen",
|
||||
"Katolik",
|
||||
"Hindu",
|
||||
"Buddha",
|
||||
"Konghucu",
|
||||
"Lainnya",
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<Select
|
||||
label={<FieldLabel label="Status Perkawinan" />}
|
||||
placeholder="Pilih status"
|
||||
data={[
|
||||
"Belum Kawin",
|
||||
"Kawin",
|
||||
"Cerai Hidup",
|
||||
"Cerai Mati",
|
||||
]}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Pekerjaan" />}
|
||||
placeholder="Pekerjaan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<Textarea
|
||||
label={<FieldLabel label="Alamat Lengkap" />}
|
||||
placeholder="Alamat domisili"
|
||||
minRows={2}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={2}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="RT" />}
|
||||
placeholder="001"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={2}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="RW" />}
|
||||
placeholder="002"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Desa / Kelurahan" />}
|
||||
placeholder="Desa"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Kecamatan" />}
|
||||
placeholder="Kecamatan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={4}>
|
||||
<TextInput
|
||||
label={<FieldLabel label="Kabupaten / Kota" />}
|
||||
placeholder="Kabupaten / Kota"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{/* Keterangan Section */}
|
||||
<FormSection
|
||||
title="Keterangan"
|
||||
icon={<IconMapPin size={18} />}
|
||||
description="Isi pernyataan SKTM"
|
||||
>
|
||||
<Textarea
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Isi Surat"
|
||||
hint="Jelaskan kondisi ekonomi secara singkat"
|
||||
/>
|
||||
}
|
||||
placeholder="Pernyataan resmi bahwa yang bersangkutan benar-benar tergolong keluarga tidak mampu..."
|
||||
minRows={4}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Keperluan"
|
||||
hint="Contoh: Beasiswa pendidikan / Perawatan kesehatan"
|
||||
/>
|
||||
}
|
||||
placeholder="Keperluan surat"
|
||||
/>
|
||||
</FormSection>
|
||||
|
||||
{/* Actions */}
|
||||
<Group justify="right" mt="md">
|
||||
<Button variant="default" onClick={() => { }}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit">Kirim / Simpan</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
18
src/server/lib/slug_converter.ts
Normal file
18
src/server/lib/slug_converter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function toSlug(text: string): string {
|
||||
return encodeURIComponent(
|
||||
text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
export function fromSlug(slug: string): string {
|
||||
return decodeURIComponent(slug)
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function capitalizeWords(text: string): string {
|
||||
return text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
@@ -105,12 +105,36 @@ const PelayananRoute = new Elysia({
|
||||
.get("/category/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const data = await prisma.categoryPelayanan.findUnique({
|
||||
where:{
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataText: string[] = Array.isArray(data.dataText)
|
||||
? data.dataText.filter((v): v is string => typeof v === "string")
|
||||
: [];
|
||||
|
||||
const syaratDokumen: { name: string }[] = Array.isArray(data.syaratDokumen)
|
||||
? data.syaratDokumen.filter(
|
||||
(v): v is { name: string } =>
|
||||
typeof v === "object" &&
|
||||
v !== null &&
|
||||
"name" in v &&
|
||||
typeof (v as any).name === "string"
|
||||
)
|
||||
: [];
|
||||
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
dataText,
|
||||
syaratDokumen,
|
||||
};
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
|
||||
Reference in New Issue
Block a user