amalia/02-jan-26 #99

Merged
amaliadwiy merged 2 commits from amalia/02-jan-26 into main 2026-01-02 17:32:32 +08:00
18 changed files with 985 additions and 766 deletions

View File

@@ -13,7 +13,7 @@ import {
Table, Table,
Text, Text,
Title, Title,
Tooltip Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react"; import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
@@ -190,7 +190,10 @@ export default function KategoriPelayananSurat({
function handleAddSyarat() { function handleAddSyarat() {
setDataChoose({ setDataChoose({
...dataChoose, ...dataChoose,
syaratDokumen: [...dataChoose.syaratDokumen, { key: "", name: "", desc: "" }], syaratDokumen: [
...dataChoose.syaratDokumen,
{ key: "", name: "", desc: "" },
],
}); });
} }

View File

@@ -1,19 +1,12 @@
import { import { Button, Center, Group, Stack, Text, Title } from "@mantine/core";
Button, import { IconSearch } from "@tabler/icons-react";
Center,
Group,
Stack,
Text,
Title
} from "@mantine/core"
import { IconSearch } from "@tabler/icons-react"
export function DataNotFound({ export function DataNotFound({
onRetry, onRetry,
backTo backTo,
}: { }: {
onRetry?: () => void onRetry?: () => void;
backTo?: () => void backTo?: () => void;
}) { }) {
return ( return (
<Center mih={320}> <Center mih={320}>
@@ -23,7 +16,8 @@ export function DataNotFound({
<Title order={4}>Data Pengajuan Tidak Ditemukan</Title> <Title order={4}>Data Pengajuan Tidak Ditemukan</Title>
<Text size="sm" c="dimmed" ta="center" maw={380}> <Text size="sm" c="dimmed" ta="center" maw={380}>
Kami tidak dapat menemukan data pengajuan dengan nomor pengajuan yg diinputkan. Silakan periksa kembali data Anda. Kami tidak dapat menemukan data pengajuan dengan nomor pengajuan yg
diinputkan. Silakan periksa kembali data Anda.
</Text> </Text>
<Group mt="md"> <Group mt="md">
@@ -47,5 +41,5 @@ export function DataNotFound({
</Group> </Group>
</Stack> </Stack>
</Center> </Center>
) );
} }

View File

@@ -4,13 +4,13 @@ import { IconCheck } from "@tabler/icons-react";
type SuccessPengajuanProps = { type SuccessPengajuanProps = {
noPengajuan: string; noPengajuan: string;
onClose?: () => void; onClose?: () => void;
category?: 'create' | 'update'; category?: "create" | "update";
}; };
export default function SuccessPengajuan({ export default function SuccessPengajuan({
noPengajuan, noPengajuan,
onClose, onClose,
category category,
}: SuccessPengajuanProps) { }: SuccessPengajuanProps) {
return ( return (
<Center h="100vh"> <Center h="100vh">
@@ -19,11 +19,15 @@ export default function SuccessPengajuan({
<IconCheck size={56} color="green" /> <IconCheck size={56} color="green" />
<Title order={3} ta="center"> <Title order={3} ta="center">
{category == 'create' ? 'Pengajuan Berhasil Dibuat' : 'Pengajuan Berhasil Diupdate'} {category == "create"
? "Pengajuan Berhasil Dibuat"
: "Pengajuan Berhasil Diupdate"}
</Title> </Title>
<Text ta="center" size="sm" c="dimmed"> <Text ta="center" size="sm" c="dimmed">
{category == 'create' ? 'Pengajuan layanan surat sudah dibuat dengan nomor:' : 'Pengajuan layanan surat sudah diupdate dengan nomor:'} {category == "create"
? "Pengajuan layanan surat sudah dibuat dengan nomor:"
: "Pengajuan layanan surat sudah diupdate dengan nomor:"}
</Text> </Text>
<Badge size="xl" variant="light" color="green"> <Badge size="xl" variant="light" color="green">

View File

@@ -146,9 +146,7 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
<tr> <tr>
<td style={{ width: "160px" }}>1. {getValue("data_dokumen")}</td> <td style={{ width: "160px" }}>1. {getValue("data_dokumen")}</td>
<td style={{ width: "10px" }}></td> <td style={{ width: "10px" }}></td>
<td> <td>{/* {getValue("nama")} */}</td>
{/* {getValue("nama")} */}
</td>
</tr> </tr>
<tr> <tr>
<td>Tertulis pada dokumen A</td> <td>Tertulis pada dokumen A</td>

View File

@@ -135,9 +135,7 @@ export default function SKPenghasilan({ data }: { data: any }) {
<tr> <tr>
<td style={{ width: "160px" }}>Penghasilan</td> <td style={{ width: "160px" }}>Penghasilan</td>
<td style={{ width: "10px" }}>:</td> <td style={{ width: "10px" }}>:</td>
<td> <td>Rp. {getValue("penghasilan")} per bulan</td>
Rp. {getValue("penghasilan")} per bulan
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -145,8 +143,8 @@ export default function SKPenghasilan({ data }: { data: any }) {
{/* KEPERLUAN */} {/* KEPERLUAN */}
<div style={{ marginTop: "20px" }}> <div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat untuk keperluan:{" "} Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan")}</b>
<b>{getValue("alasan")}</b>. .
</div> </div>
<div style={{ marginTop: "20px" }}> <div style={{ marginTop: "20px" }}>

View File

@@ -71,7 +71,10 @@ export default function SKTempatUsaha({ data }: { data: any }) {
label="Tempat/Tanggal Lahir" label="Tempat/Tanggal Lahir"
value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`} value={`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
/> />
<Row label="Alamat Pemilik Usaha" value={getValue("alamat_pemilik")} /> <Row
label="Alamat Pemilik Usaha"
value={getValue("alamat_pemilik")}
/>
<Row label="Nomor KTP" value={getValue("nik")} /> <Row label="Nomor KTP" value={getValue("nik")} />
</div> </div>
@@ -86,14 +89,8 @@ export default function SKTempatUsaha({ data }: { data: any }) {
<Row label="Nama Usaha" value={getValue("nama_usaha")} /> <Row label="Nama Usaha" value={getValue("nama_usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang_usaha")} /> <Row label="Bidang Usaha" value={getValue("bidang_usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat_usaha")} /> <Row label="Alamat Usaha" value={getValue("alamat_usaha")} />
<Row <Row label="Status Tempat Usaha" value={getValue("status_tempat")} />
label="Status Tempat Usaha" <Row label="Luas Tempat Usaha" value={getValue("luas_usaha")} />
value={getValue("status_tempat")}
/>
<Row
label="Luas Tempat Usaha"
value={getValue("luas_usaha")}
/>
<Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} /> <Row label="Jumlah Karyawan" value={getValue("jumlah_karyawan")} />
</div> </div>

View File

@@ -98,7 +98,9 @@ export default function SKYatim({ data }: { data: any }) {
</tr> </tr>
<tr> <tr>
<td>Tempat/Tanggal Lahir</td> <td>Tempat/Tanggal Lahir</td>
<td>: {`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}</td> <td>
: {`${getValue("tempat_lahir")}, ${getValue("tanggal_lahir")}`}
</td>
</tr> </tr>
<tr> <tr>
<td>Jenis Kelamin</td> <td>Jenis Kelamin</td>

View File

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

View File

@@ -31,7 +31,7 @@ import {
IconInfoCircle, IconInfoCircle,
IconNotes, IconNotes,
IconPhone, IconPhone,
IconUpload IconUpload,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/id"; import "dayjs/locale/id";
@@ -268,9 +268,7 @@ export default function FormSurat() {
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Text> <Text>Apakah anda yakin ingin mengirim pengajuan surat ini?</Text>
Apakah anda yakin ingin mengirim pengajuan surat ini?
</Text>
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Tidak Tidak
@@ -298,8 +296,7 @@ export default function FormSurat() {
}} }}
category="create" category="create"
/> />
) ) : (
:
<Box> <Box>
<Stack gap="lg"> <Stack gap="lg">
<Group justify="space-between" align="center"> <Group justify="space-between" align="center">
@@ -348,7 +345,6 @@ export default function FormSurat() {
</Grid> </Grid>
</FormSection> </FormSection>
{/* Kontak Section */} {/* Kontak Section */}
<FormSection <FormSection
title="Kontak" title="Kontak"
@@ -385,7 +381,9 @@ export default function FormSurat() {
</Grid> </Grid>
</FormSection> </FormSection>
{jenisSuratFix.id != "" && dataSurat && dataSurat.dataPelengkap && ( {jenisSuratFix.id != "" &&
dataSurat &&
dataSurat.dataPelengkap && (
<> <>
<FormSection <FormSection
title="Data Yang Diperlukan" title="Data Yang Diperlukan"
@@ -393,55 +391,74 @@ export default function FormSurat() {
icon={<IconNotes size={16} />} icon={<IconNotes size={16} />}
> >
<Grid> <Grid>
{dataSurat.dataPelengkap.map((item: any, index: number) => ( {dataSurat.dataPelengkap.map(
(item: any, index: number) => (
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
{ {item.type == "enum" ? (
item.type == "enum"
?
<Select <Select
label={ label={
<FieldLabel label={item.name} hint={item.desc} /> <FieldLabel
label={item.name}
hint={item.desc}
/>
} }
data={item.options ?? []} data={item.options ?? []}
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
validationForm({ validationForm({
key: "dataPelengkap", key: "dataPelengkap",
value: { key: item.key, value: e } value: { key: item.key, value: e },
}) });
}} }}
value={formSurat.dataPelengkap.find((n: any) => n.key == item.key)?.value} value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value
}
/> />
: item.type == "date" ) : item.type == "date" ? (
?
<DateInput <DateInput
locale="id" locale="id"
valueFormat="DD MMMM YYYY" valueFormat="DD MMMM YYYY"
label={ label={
<FieldLabel label={item.name} hint={item.desc} /> <FieldLabel
label={item.name}
hint={item.desc}
/>
} }
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
const formatted = e const formatted = e
? dayjs(e).locale("id").format("DD MMMM YYYY") ? dayjs(e)
.locale("id")
.format("DD MMMM YYYY")
: ""; : "";
validationForm({ validationForm({
key: "dataPelengkap", key: "dataPelengkap",
value: { key: item.key, value: formatted }, value: {
}) key: item.key,
value: formatted,
},
});
}} }}
/> />
: ) : (
<TextInput <TextInput
type={item.type} type={item.type}
label={ label={
<FieldLabel label={item.name} hint={item.desc} /> <FieldLabel
label={item.name}
hint={item.desc}
/>
} }
placeholder={item.name} placeholder={item.name}
onChange={(e) => onChange={(e) =>
validationForm({ validationForm({
key: "dataPelengkap", key: "dataPelengkap",
value: { key: item.key, value: e.target.value }, value: {
key: item.key,
value: e.target.value,
},
}) })
} }
value={ value={
@@ -450,10 +467,10 @@ export default function FormSurat() {
)?.value )?.value
} }
/> />
} )}
</Grid.Col> </Grid.Col>
))} ),
)}
</Grid> </Grid>
</FormSection> </FormSection>
@@ -463,7 +480,8 @@ export default function FormSurat() {
icon={<IconFiles size={16} />} icon={<IconFiles size={16} />}
> >
<Grid> <Grid>
{dataSurat.syaratDokumen.map((item: any, index: number) => ( {dataSurat.syaratDokumen.map(
(item: any, index: number) => (
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
<FileInputWrapper <FileInputWrapper
label={item.desc} label={item.desc}
@@ -478,7 +496,8 @@ export default function FormSurat() {
name={item.name} name={item.name}
/> />
</Grid.Col> </Grid.Col>
))} ),
)}
</Grid> </Grid>
</FormSection> </FormSection>
@@ -494,8 +513,7 @@ export default function FormSurat() {
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
} )}
</Container> </Container>
); );
} }

View File

@@ -24,7 +24,7 @@ import {
Stack, Stack,
Text, Text,
TextInput, TextInput,
Tooltip Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DateInput } from "@mantine/dates";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
@@ -33,7 +33,7 @@ import {
IconFiles, IconFiles,
IconInfoCircle, IconInfoCircle,
IconNotes, IconNotes,
IconUpload IconUpload,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/id"; import "dayjs/locale/id";
@@ -73,7 +73,8 @@ type DataPengajuan = {
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
idSurat: string | undefined; idSurat: string | undefined;
} alasan: string | undefined | null;
};
export default function UpdateDataSurat() { export default function UpdateDataSurat() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -82,13 +83,11 @@ export default function UpdateDataSurat() {
const noPengajuan = query.get("pengajuan"); const noPengajuan = query.get("pengajuan");
const [found, setFound] = useState(true); const [found, setFound] = useState(true);
return ( return (
<Container size="md" w={"100%"}> <Container size="md" w={"100%"}>
<Box> <Box>
<Stack gap="lg"> <Stack gap="lg">
{ {found && (
found &&
<Group justify="space-between" align="center" mt={"lg"}> <Group justify="space-between" align="center" mt={"lg"}>
<Group align="center"> <Group align="center">
<IconBuildingCommunity size={28} /> <IconBuildingCommunity size={28} />
@@ -97,24 +96,28 @@ export default function UpdateDataSurat() {
Update Data Pengajuan Surat Administrasi Update Data Pengajuan Surat Administrasi
</Text> </Text>
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
Formulir ini digunakan untuk memperbarui data pengajuan surat administrasi yang telah diajukan sebelumnya. Formulir ini digunakan untuk memperbarui data pengajuan
surat administrasi yang telah diajukan sebelumnya.
</Text> </Text>
</div> </div>
</Group> </Group>
</Group> </Group>
} )}
<Stack gap="lg" mb="lg"> <Stack gap="lg" mb="lg">
{ {!noPengajuan ? (
!noPengajuan ? (
<SearchData /> <SearchData />
) ) : found ? (
: <DataUpdate
found ? ( noPengajuan={noPengajuan}
<DataUpdate noPengajuan={noPengajuan} onValidate={(e) => { setFound(e) }} /> onValidate={(e) => {
setFound(e);
}}
/>
) : ( ) : (
<DataNotFound backTo={() => navigate("/darmasaba/update-data-surat")} /> <DataNotFound
) backTo={() => navigate("/darmasaba/update-data-surat")}
} />
)}
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
@@ -160,11 +163,13 @@ function FormSection({
</Group> </Group>
{description && <Badge variant="light">{description}</Badge>} {description && <Badge variant="light">{description}</Badge>}
</Group> </Group>
{info && <Text size="sm" c="dimmed">{info}</Text>} {info && (
<Text size="sm" c="dimmed">
{info}
</Text>
)}
</Box> </Box>
{ {title && <Divider mb="sm" />}
title && <Divider mb="sm" />
}
<Stack gap="sm">{children}</Stack> <Stack gap="sm">{children}</Stack>
</Card> </Card>
); );
@@ -179,7 +184,7 @@ function FileInputWrapper({
name, name,
description, description,
linkView, linkView,
disabled disabled,
}: { }: {
label: string; label: string;
placeholder?: string; placeholder?: string;
@@ -194,7 +199,6 @@ function FileInputWrapper({
const [viewImg, setViewImg] = useState(""); const [viewImg, setViewImg] = useState("");
const [openedPreviewFile, setOpenedPreviewFile] = useState(false); const [openedPreviewFile, setOpenedPreviewFile] = useState(false);
useShallowEffect(() => { useShallowEffect(() => {
if (viewImg) { if (viewImg) {
setOpenedPreviewFile(true); setOpenedPreviewFile(true);
@@ -222,13 +226,11 @@ function FileInputWrapper({
{description} {description}
</Text> </Text>
)} )}
{ {linkView && (
linkView && (
<Anchor onClick={() => setViewImg(linkView)} size="sm"> <Anchor onClick={() => setViewImg(linkView)} size="sm">
Lihat dokumen sebelumnya Lihat dokumen sebelumnya
</Anchor> </Anchor>
) )}
}
</Flex> </Flex>
<FileInput <FileInput
@@ -257,7 +259,6 @@ function FileInputWrapper({
) : null} ) : null}
</Stack> </Stack>
</> </>
); );
} }
@@ -274,40 +275,41 @@ function SearchData() {
notification({ notification({
title: "Peringatan", title: "Peringatan",
message: "Silakan isi nomor pengajuan atau nomor telephone", message: "Silakan isi nomor pengajuan atau nomor telephone",
type: "warning" type: "warning",
}); });
return; return;
} }
const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({ const response = await apiFetch.api.pelayanan["get-no-pengajuan"].post({
phone: searchPengajuanPhone, phone: searchPengajuanPhone,
noPengajuan: searchPengajuan noPengajuan: searchPengajuan,
}); });
if (response.status === 200) { if (response.status === 200) {
if (response.data?.success) { if (response.data?.success) {
navigate(`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`); navigate(
`/darmasaba/update-data-surat?pengajuan=${response.data.nomer}`,
);
} else { } else {
notification({ notification({
title: "Peringatan", title: "Peringatan",
message: response.data?.message || "Data pengajuan tidak valid", message: response.data?.message || "Data pengajuan tidak valid",
type: "warning" type: "warning",
}); });
} }
} else { } else {
notification({ notification({
title: "Error", title: "Error",
message: "Pengajuan tidak ditemukan atau gagal memuat data", message: "Pengajuan tidak ditemukan atau gagal memuat data",
type: "error" type: "error",
}); });
} }
} catch (error) { } catch (error) {
console.error("Error searching:", error); console.error("Error searching:", error);
notification({ notification({
title: "Error", title: "Error",
message: "Gagal mencari data pengajuan", message: "Gagal mencari data pengajuan",
type: "error" type: "error",
}); });
} finally { } finally {
setSubmitLoading(false); setSubmitLoading(false);
@@ -322,9 +324,16 @@ function SearchData() {
<Grid> <Grid>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextInput <TextInput
label={<FieldLabel label="Nomor Pengajuan" hint="Nomor pengajuan surat" />} label={
<FieldLabel
label="Nomor Pengajuan"
hint="Nomor pengajuan surat"
/>
}
placeholder="PS-2025-000123" placeholder="PS-2025-000123"
onChange={(e) => { setSearchPengajuan(e.target.value) }} onChange={(e) => {
setSearchPengajuan(e.target.value);
}}
/> />
</Grid.Col> </Grid.Col>
@@ -338,30 +347,45 @@ function SearchData() {
} }
placeholder="08123456789" placeholder="08123456789"
type="number" type="number"
onChange={(e) => { setSearchPengajuanPhone(e.target.value) }} onChange={(e) => {
setSearchPengajuanPhone(e.target.value);
}}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
<Button fullWidth variant="light" color="blue" onClick={() => { handleSearch() }} loading={submitLoading}> <Button
fullWidth
variant="light"
color="blue"
onClick={() => {
handleSearch();
}}
loading={submitLoading}
>
Cari Pengajuan Cari Pengajuan
</Button> </Button>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</FormSection> </FormSection>
) );
} }
function DataUpdate({
function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValidate: (e: boolean) => void }) { noPengajuan,
const [opened, { open, close }] = useDisclosure(false) onValidate,
const navigate = useNavigate() }: {
const [sukses, setSukses] = useState(false) noPengajuan: string;
const [submitLoading, setSubmitLoading] = useState(false) onValidate: (e: boolean) => void;
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]) }) {
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]) const [opened, { open, close }] = useDisclosure(false);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({}) const navigate = useNavigate();
const [status, setStatus] = useState("") const [sukses, setSukses] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [dataPelengkap, setDataPelengkap] = useState<DataItem[]>([]);
const [dataSyaratDokumen, setDataSyaratDokumen] = useState<DataItem[]>([]);
const [dataPengajuan, setDataPengajuan] = useState<DataPengajuan | {}>({});
const [status, setStatus] = useState("");
const [formSurat, setFormSurat] = useState<FormUpdateSurat>({ const [formSurat, setFormSurat] = useState<FormUpdateSurat>({
dataPelengkap: [], dataPelengkap: [],
syaratDokumen: [], syaratDokumen: [],
@@ -369,28 +393,33 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
async function fetchData() { async function fetchData() {
try { try {
const res = await apiFetch.api.pelayanan["detail-data"].post({ nomerPengajuan: noPengajuan }); const res = await apiFetch.api.pelayanan["detail-data"].post({
nomerPengajuan: noPengajuan,
});
if (res.data && res.data.success === true) { if (res.data && res.data.success === true) {
onValidate(true) onValidate(true);
setDataPelengkap(res.data.dataPelengkap || []); setDataPelengkap(res.data.dataPelengkap || []);
setDataSyaratDokumen(res.data.syaratDokumen || []); setDataSyaratDokumen(res.data.syaratDokumen || []);
setDataPengajuan(res.data.pengajuan || {}); setDataPengajuan(res.data.pengajuan || {});
setStatus(res.data.pengajuan && 'status' in res.data.pengajuan ? res.data.pengajuan.status : ""); setStatus(
res.data.pengajuan && "status" in res.data.pengajuan
? res.data.pengajuan.status
: "",
);
} else { } else {
// notification({ // notification({
// title: "Error", // title: "Error",
// message: res.data?.message || "Gagal memuat data", // message: res.data?.message || "Gagal memuat data",
// type: "error", // type: "error",
// }); // });
onValidate(false) onValidate(false);
setDataPelengkap([]); setDataPelengkap([]);
setDataSyaratDokumen([]); setDataSyaratDokumen([]);
setDataPengajuan({}); setDataPengajuan({});
} }
} catch (error) { } catch (error) {
console.error('Error fetching data:', error); console.error("Error fetching data:", error);
} }
} }
@@ -398,22 +427,18 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
fetchData(); fetchData();
}, []); }, []);
function upsertById<T extends { id: string }>( function upsertById<T extends { id: string }>(array: T[], item: T): T[] {
array: T[], const index = array.findIndex((v) => v.id === item.id);
item: T
): T[] {
const index = array.findIndex((v) => v.id === item.id)
// insert // insert
if (index === -1) { if (index === -1) {
return [...array, item] return [...array, item];
} }
// ✏️ update // ✏️ update
return array.map((v, i) => (i === index ? { ...v, ...item } : v)) return array.map((v, i) => (i === index ? { ...v, ...item } : v));
} }
function validationForm({ function validationForm({
kategori, kategori,
value, value,
@@ -426,8 +451,8 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
[kategori]: upsertById(prev[kategori], { [kategori]: upsertById(prev[kategori], {
id: value.id, id: value.id,
key: value.key, key: value.key,
value: value.value value: value.value,
}) }),
})); }));
} }
@@ -436,25 +461,25 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
id: string, id: string,
value: any, value: any,
): UpdateDataItem[] { ): UpdateDataItem[] {
return list.map((item) => return list.map((item) => (item.id === id ? { ...item, value } : item));
item.id === id ? { ...item, value } : item,
);
} }
function onChecking() { function onChecking() {
if (formSurat.dataPelengkap.length == 0 && formSurat.syaratDokumen.length == 0) if (
formSurat.dataPelengkap.length == 0 &&
formSurat.syaratDokumen.length == 0
)
return notification({ return notification({
title: "Peringatan", title: "Peringatan",
message: "Tidak ada data yang diupdate", message: "Tidak ada data yang diupdate",
type: "warning", type: "warning",
}); });
const isFormKosong = Object.values(formSurat).some((value: UpdateDataItem[] | string) => { const isFormKosong = Object.values(formSurat).some(
(value: UpdateDataItem[] | string) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return ( return value.some(
value.some(
(item) => (item) =>
typeof item.value === "string" && item.value.trim() === "", typeof item.value === "string" && item.value.trim() === "",
)
); );
} }
@@ -463,7 +488,8 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
} }
return false; return false;
}); },
);
if (isFormKosong) { if (isFormKosong) {
return notification({ return notification({
@@ -472,7 +498,7 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
type: "error", type: "error",
}); });
} else { } else {
open() open();
} }
} }
@@ -504,7 +530,7 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
// 4⃣ SUBMIT KE API // 4⃣ SUBMIT KE API
const res = await apiFetch.api.pelayanan.update.post({ const res = await apiFetch.api.pelayanan.update.post({
id: dataPengajuan && ('id' in dataPengajuan) ? dataPengajuan.id : "", id: dataPengajuan && "id" in dataPengajuan ? dataPengajuan.id : "",
dataPelengkap: finalFormSurat.dataPelengkap, dataPelengkap: finalFormSurat.dataPelengkap,
syaratDokumen: finalFormSurat.syaratDokumen, syaratDokumen: finalFormSurat.syaratDokumen,
}); });
@@ -530,7 +556,6 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
} }
} }
return ( return (
<> <>
<FullScreenLoading visible={submitLoading} /> <FullScreenLoading visible={submitLoading} />
@@ -541,9 +566,7 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Text> <Text>Apakah anda yakin ingin mengupdate pengajuan surat ini?</Text>
Apakah anda yakin ingin mengupdate pengajuan surat ini?
</Text>
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Tidak Tidak
@@ -561,8 +584,7 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
</Group> </Group>
</Stack> </Stack>
</Modal> </Modal>
{ {sukses ? (
sukses ?
<SuccessPengajuan <SuccessPengajuan
noPengajuan={noPengajuan} noPengajuan={noPengajuan}
onClose={() => { onClose={() => {
@@ -570,12 +592,29 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
}} }}
category="update" category="update"
/> />
: ) : (
<> <>
{ {status != "ditolak" && status != "antrian" && (
(status != "ditolak" && status != "antrian") <Alert
&& <Alert variant="light" color="yellow" radius="lg" title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`} icon={<span style={{ fontSize: '1.2rem' }}></span>} /> variant="light"
} color="yellow"
radius="lg"
title={`Data pengajuan surat ini tidak dapat diupdate karena berstatus ${status}.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
{status == "ditolak" && (
<Alert
variant="light"
color="yellow"
radius="lg"
title={`Data pengajuan surat ini ditolak, karena ${dataPengajuan && 'alasan' in dataPengajuan && dataPengajuan.alasan
? dataPengajuan.alasan
: "alasan tidak tersedia"
}. Silahkan perbaiki data pengajuan surat ini.`}
icon={<span style={{ fontSize: "1.2rem" }}></span>}
/>
)}
<FormSection <FormSection
title="Data Yang Diperlukan" title="Data Yang Diperlukan"
description="Data yang diperlukan" description="Data yang diperlukan"
@@ -584,31 +623,29 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
<Grid> <Grid>
{dataPelengkap.map((item: any, index: number) => ( {dataPelengkap.map((item: any, index: number) => (
<Grid.Col span={6} key={index}> <Grid.Col span={6} key={index}>
{ {item.type == "enum" ? (
item.type == "enum"
?
<Select <Select
label={ label={<FieldLabel label={item.name} hint={item.desc} />}
<FieldLabel label={item.name} hint={item.desc} />
}
data={item.options ?? []} data={item.options ?? []}
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
validationForm({ validationForm({
kategori: "dataPelengkap", kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: e } value: { id: item.id, key: item.key, value: e },
}) });
}} }}
value={formSurat.dataPelengkap.find((n: any) => n.key == item.key)?.value || dataPelengkap.find((n: any) => n.key == item.key)?.value} value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value ||
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
/> />
: item.type == "date" ) : item.type == "date" ? (
?
<DateInput <DateInput
locale="id" locale="id"
valueFormat="DD MMMM YYYY" valueFormat="DD MMMM YYYY"
label={ label={<FieldLabel label={item.name} hint={item.desc} />}
<FieldLabel label={item.name} hint={item.desc} />
}
placeholder={item.name} placeholder={item.name}
onChange={(e) => { onChange={(e) => {
const formatted = e const formatted = e
@@ -616,34 +653,47 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
: ""; : "";
validationForm({ validationForm({
kategori: "dataPelengkap", kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: formatted }, value: {
}) id: item.id,
key: item.key,
value: formatted,
},
});
}} }}
value={ value={
formSurat.dataPelengkap.find((n: any) => n.key === item.key)?.value ? formSurat.dataPelengkap.find(
parseTanggalID( (n: any) => n.key === item.key,
formSurat.dataPelengkap.find((n: any) => n.key === item.key)?.value )?.value
) : ? parseTanggalID(
parseTanggalID( formSurat.dataPelengkap.find(
item.value (n: any) => n.key === item.key,
)?.value,
) )
: parseTanggalID(item.value)
} }
/> />
: <TextInput ) : (
label={ <TextInput
<FieldLabel label={item.name} hint={item.desc} /> label={<FieldLabel label={item.name} hint={item.desc} />}
}
placeholder={item.name} placeholder={item.name}
onChange={(e) => onChange={(e) =>
validationForm({ validationForm({
kategori: "dataPelengkap", kategori: "dataPelengkap",
value: { id: item.id, key: item.key, value: e.target.value }, value: {
id: item.id,
key: item.key,
value: e.target.value,
},
}) })
} }
value={formSurat.dataPelengkap.find((n) => n.id === item.id)?.value ?? dataPelengkap.find((n: any) => n.key == item.key,)?.value} value={
formSurat.dataPelengkap.find((n) => n.id === item.id)
?.value ??
dataPelengkap.find((n: any) => n.key == item.key)?.value
}
disabled={status != "ditolak" && status != "antrian"} disabled={status != "ditolak" && status != "antrian"}
/> />
} )}
</Grid.Col> </Grid.Col>
))} ))}
</Grid> </Grid>
@@ -677,10 +727,17 @@ function DataUpdate({ noPengajuan, onValidate }: { noPengajuan: string, onValida
</FormSection> </FormSection>
<Group justify="right" mt="md"> <Group justify="right" mt="md">
<Button onClick={() => { onChecking() }} disabled={status != "ditolak" && status != "antrian"}>Kirim</Button> <Button
onClick={() => {
onChecking();
}}
disabled={status != "ditolak" && status != "antrian"}
>
Kirim
</Button>
</Group> </Group>
</> </>
} )}
</> </>
) );
} }

View File

@@ -437,7 +437,12 @@ function DetailDataHistori({ data }: { data: any }) {
</Title> </Title>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Spoiler maxHeight={200} showLabel="Show more" hideLabel="Hide" transitionDuration={1000}> <Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table> <Table>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>

View File

@@ -38,6 +38,7 @@ import { useEffect, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import useSwr from "swr"; import useSwr from "swr";
export default function DetailPengaduanPage() { export default function DetailPengaduanPage() {
const { search } = useLocation(); const { search } = useLocation();
const query = new URLSearchParams(search); const query = new URLSearchParams(search);
@@ -61,6 +62,7 @@ export default function DetailPengaduanPage() {
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengaduan <DetailDataPengaduan
data={data?.data?.pengaduan} data={data?.data?.pengaduan}
phone={data && data.data && data.data.warga ? data.data.warga.phone : null}
onAction={() => { onAction={() => {
mutate(); mutate();
}} }}
@@ -78,9 +80,11 @@ export default function DetailPengaduanPage() {
function DetailDataPengaduan({ function DetailDataPengaduan({
data, data,
phone,
onAction, onAction,
}: { }: {
data: any | null; data: any | null;
phone?: string | null;
onAction: () => void; onAction: () => void;
}) { }) {
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
@@ -122,6 +126,21 @@ function DetailDataPengaduan({
}); });
if (res?.status === 200) { if (res?.status === 200) {
const resWA = await apiFetch.api["send-wa"].pengaduan.post({
noPengaduan: data?.noPengaduan,
judulPengaduan: data?.title,
status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
alasan: keterangan,
tlp: String(phone),
})
onAction(); onAction();
close(); close();
notification({ notification({
@@ -129,6 +148,28 @@ function DetailDataPengaduan({
message: "Success update pengaduan", message: "Success update pengaduan",
type: "success", type: "success",
}); });
if (resWA?.status === 200) {
if (resWA.data?.success) {
notification({
title: "Success",
message: "Success send message to warga",
type: "success",
});
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else {
notification({
title: "Failed",
message: "Failed send message to warga",
type: "error",
});
}
} else { } else {
notification({ notification({
title: "Error", title: "Error",
@@ -425,7 +466,12 @@ function DetailDataHistori({ data }: { data: any }) {
</Title> </Title>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Spoiler maxHeight={200} showLabel="Show more" hideLabel="Hide" transitionDuration={1000}> <Spoiler
maxHeight={200}
showLabel="Show more"
hideLabel="Hide"
transitionDuration={1000}
>
<Table> <Table>
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>

View File

@@ -745,6 +745,19 @@ const PelayananRoute = new Elysia({
} }
}) })
const alasanDitolak = await prisma.historyPelayanan.findFirst({
where: {
idPengajuanLayanan: data?.id,
status: "ditolak"
},
select: {
keteranganAlasan: true,
},
orderBy: {
createdAt: "desc"
}
})
const dataHistoryFix = dataHistory.map((item) => { const dataHistoryFix = dataHistory.map((item) => {
return { return {
id: item.id, id: item.id,
@@ -771,6 +784,7 @@ const PelayananRoute = new Elysia({
createdAt: data?.createdAt, createdAt: data?.createdAt,
updatedAt: data?.updatedAt, updatedAt: data?.updatedAt,
idSurat: dataSurat?.id, idSurat: dataSurat?.id,
alasan: alasanDitolak?.keteranganAlasan,
} }
const datafix = { const datafix = {

View File

@@ -415,7 +415,7 @@ const PengaduanRoute = new Elysia({
const datafix = { const datafix = {
pengaduan: {}, pengaduan: {},
history: [], history: [],
warga: {}, warga: null,
} }
return datafix return datafix

View File

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