upd: notif warga

Deskripsi:
- tolak pengaduan
- terima pengaduan
- kerjakan pengaduan
- pengaduan selesai

NO Issues
This commit is contained in:
2026-01-02 14:52:00 +08:00
parent 3944e1ee82
commit 487395bdb3
17 changed files with 958 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,33 +1,27 @@
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}>
<Stack align="center" gap="sm"> <Stack align="center" gap="sm">
<IconSearch size={64} opacity={0.5} /> <IconSearch size={64} opacity={0.5} />
<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
</Text> diinputkan. Silakan periksa kembali data Anda.
</Text>
<Group mt="md"> <Group mt="md">
{/* {onRetry && ( {/* {onRetry && (
<Button <Button
variant="light" variant="light"
leftSection={<IconSearch size={16} />} leftSection={<IconSearch size={16} />}
@@ -37,15 +31,15 @@ export function DataNotFound({
</Button> </Button>
)} */} )} */}
<Button <Button
variant="outline" variant="outline"
// leftSection={<IconArrowLeft size={16} />} // leftSection={<IconArrowLeft size={16} />}
onClick={backTo} onClick={backTo}
> >
Cari Kembali Cari Kembali
</Button> </Button>
</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

@@ -7,7 +7,7 @@ export default function SKBedaBiodataDiri({ data }: { data: any }) {
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"", "",
); );
const loadImage = async () => { const loadImage = async () => {
@@ -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

@@ -7,7 +7,7 @@ export default function SKBelumKawin({ data }: { data: any }) {
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"", "",
); );
const loadImage = async () => { const loadImage = async () => {

View File

@@ -7,7 +7,7 @@ export default function SKKematian({ data }: { data: any }) {
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"", "",
); );
const loadImage = async () => { const loadImage = async () => {

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

@@ -7,7 +7,7 @@ export default function SKUsaha({ data }: { data: any }) {
const getValue = (jenis: string) => const getValue = (jenis: string) =>
_.upperFirst( _.upperFirst(
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
"", "",
); );
const loadImage = async () => { const loadImage = async () => {

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,63 +381,84 @@ export default function FormSurat() {
</Grid> </Grid>
</FormSection> </FormSection>
{jenisSuratFix.id != "" && dataSurat && dataSurat.dataPelengkap && ( {jenisSuratFix.id != "" &&
<> dataSurat &&
<FormSection dataSurat.dataPelengkap && (
title="Data Yang Diperlukan" <>
description="Data yang diperlukan untuk mengajukan surat" <FormSection
icon={<IconNotes size={16} />} title="Data Yang Diperlukan"
> description="Data yang diperlukan untuk mengajukan surat"
<Grid> icon={<IconNotes size={16} />}
{dataSurat.dataPelengkap.map((item: any, index: number) => ( >
<Grid.Col span={6} key={index}> <Grid>
{ {dataSurat.dataPelengkap.map(
item.type == "enum" (item: any, index: number) => (
? <Grid.Col span={6} key={index}>
<Select {item.type == "enum" ? (
label={ <Select
<FieldLabel label={item.name} hint={item.desc} /> label={
} <FieldLabel
data={item.options ?? []} label={item.name}
placeholder={item.name} hint={item.desc}
onChange={(e) => { />
validationForm({ }
key: "dataPelengkap", data={item.options ?? []}
value: { key: item.key, value: e } placeholder={item.name}
}) onChange={(e) => {
}} validationForm({
value={formSurat.dataPelengkap.find((n: any) => n.key == item.key)?.value} key: "dataPelengkap",
/> value: { key: item.key, value: e },
: item.type == "date" });
? }}
value={
formSurat.dataPelengkap.find(
(n: any) => n.key == item.key,
)?.value
}
/>
) : 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,52 +467,53 @@ export default function FormSurat() {
)?.value )?.value
} }
/> />
} )}
</Grid.Col>
),
)}
</Grid>
</FormSection>
</Grid.Col> <FormSection
))} title="Syarat Dokumen"
</Grid> description="Syarat dokumen yang diperlukan"
</FormSection> icon={<IconFiles size={16} />}
>
<Grid>
{dataSurat.syaratDokumen.map(
(item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FileInputWrapper
label={item.desc}
placeholder={"Upload file "}
accept="image/*,application/pdf"
onChange={(file) =>
validationForm({
key: "syaratDokumen",
value: { key: item.key, value: file },
})
}
name={item.name}
/>
</Grid.Col>
),
)}
</Grid>
</FormSection>
<FormSection {/* Actions */}
title="Syarat Dokumen" <Group justify="right" mt="md">
description="Syarat dokumen yang diperlukan" {/* <Button variant="default" onClick={() => { }}>
icon={<IconFiles size={16} />}
>
<Grid>
{dataSurat.syaratDokumen.map((item: any, index: number) => (
<Grid.Col span={6} key={index}>
<FileInputWrapper
label={item.desc}
placeholder={"Upload file "}
accept="image/*,application/pdf"
onChange={(file) =>
validationForm({
key: "syaratDokumen",
value: { key: item.key, value: file },
})
}
name={item.name}
/>
</Grid.Col>
))}
</Grid>
</FormSection>
{/* Actions */}
<Group justify="right" mt="md">
{/* <Button variant="default" onClick={() => { }}>
Reset Reset
</Button> */} </Button> */}
<Button onClick={onChecking}>Kirim</Button> <Button onClick={onChecking}>Kirim</Button>
</Group> </Group>
</> </>
)} )}
</Stack> </Stack>
</Stack> </Stack>
</Box> </Box>
} )}
</Container> </Container>
); );
} }

File diff suppressed because it is too large Load Diff

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

@@ -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