Merge pull request 'upd:' (#30) from amalia/18-nov-25 into main
Reviewed-on: http://wibugit.wibudev.com/wibu/jenna-mcp/pulls/30
This commit is contained in:
@@ -184,8 +184,8 @@ model SuratPelayanan {
|
|||||||
Warga Warga @relation(fields: [idWarga], references: [id])
|
Warga Warga @relation(fields: [idWarga], references: [id])
|
||||||
idWarga String
|
idWarga String
|
||||||
noSurat String
|
noSurat String
|
||||||
dateExpired DateTime @db.Date
|
dateExpired DateTime? @db.Date
|
||||||
status Int
|
status Int @default(0)
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import notification from "@/components/notificationGlobal";
|
||||||
import apiFetch from "@/lib/apiFetch";
|
import apiFetch from "@/lib/apiFetch";
|
||||||
import {
|
import {
|
||||||
Anchor,
|
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -9,89 +9,160 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
List,
|
||||||
Modal,
|
Modal,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
Title,
|
ThemeIcon,
|
||||||
|
Title
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconAlignJustified,
|
IconAlignJustified,
|
||||||
IconCategory,
|
IconCheck,
|
||||||
IconFileCertificate,
|
IconFileCertificate,
|
||||||
IconInfoTriangle,
|
IconFileCheck,
|
||||||
IconMapPin,
|
|
||||||
IconMessageReport,
|
IconMessageReport,
|
||||||
IconPhotoScan,
|
IconPhone,
|
||||||
IconUser,
|
IconUser
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
import type { User } from "generated/prisma";
|
||||||
|
import _ from "lodash";
|
||||||
|
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 DetailPelayananPage() {
|
export default function DetailPengajuanPage() {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const id = query.get("id");
|
const id = query.get("id");
|
||||||
|
const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
|
apiFetch.api.pelayanan.detail.get({
|
||||||
|
query: {
|
||||||
|
id: id!,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col span={8}>
|
<Grid.Col span={8}>
|
||||||
<Stack gap={"xl"}>
|
<Stack gap={"xl"}>
|
||||||
<DetailDataPelayanan />
|
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} />
|
||||||
<DetailDataHistori />
|
<DetailDataHistori data={data?.data?.history} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={4}>
|
<Grid.Col span={4}>
|
||||||
<DetailUserPelayanan />
|
<DetailUserPengajuan data={data?.data?.warga} />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailDataPelayanan() {
|
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) {
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||||
|
const [keterangan, setKeterangan] = useState("");
|
||||||
|
const [host, setHost] = useState<User | null>(null);
|
||||||
|
const [noSurat, setNoSurat] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchHost() {
|
||||||
|
const { data } = await apiFetch.api.user.find.get();
|
||||||
|
setHost(data?.user ?? null);
|
||||||
|
}
|
||||||
|
fetchHost();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch.api.pelayanan["update-status"].post({
|
||||||
|
id: data?.id,
|
||||||
|
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
|
||||||
|
keterangan: keterangan,
|
||||||
|
idUser: host?.id ?? "",
|
||||||
|
noSurat: noSurat
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res?.status === 200) {
|
||||||
|
onAction();
|
||||||
|
close();
|
||||||
|
notification({
|
||||||
|
title: "Success",
|
||||||
|
message: "Success update pengajuan surat",
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notification({
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to update pengajuan surat",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notification({
|
||||||
|
title: "Error",
|
||||||
|
message: "Failed to update pengajuan surat",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
|
{/* MODAL KONFIRMASI */}
|
||||||
<Modal
|
<Modal
|
||||||
opened={opened}
|
opened={opened}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
title={"Konfirmasi"}
|
title={"Konfirmasi"}
|
||||||
centered
|
|
||||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||||
>
|
>
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
{catModal === "tolak" ? (
|
{catModal === "tolak" ? (
|
||||||
<>
|
<>
|
||||||
<Text>
|
<Text>
|
||||||
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
|
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
|
||||||
</Text>
|
</Text>
|
||||||
|
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} />
|
||||||
<Textarea size="md" minRows={5} />
|
|
||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button variant="light" onClick={close}>
|
<Button variant="light" onClick={close}>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="filled" color="red" onClick={close}>
|
<Button variant="filled" color="red" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}>
|
||||||
Tolak
|
Tolak
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text>Anda yakin ingin menerima pengaduan ini?</Text>
|
<Text>
|
||||||
|
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
|
||||||
|
{
|
||||||
|
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
{
|
||||||
|
data.status == 'diterima' && (
|
||||||
|
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" />
|
||||||
|
)
|
||||||
|
}
|
||||||
<Group justify="center" grow>
|
<Group justify="center" grow>
|
||||||
<Button variant="light" onClick={close}>
|
<Button variant="light" onClick={close}>
|
||||||
Batal
|
Tidak
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="filled" color="green" onClick={close}>
|
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}>
|
||||||
Terima
|
Ya
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</>
|
</>
|
||||||
@@ -114,117 +185,141 @@ function DetailDataPelayanan() {
|
|||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Pelayanan Surat
|
Pengajuan {data?.category}
|
||||||
</Title>
|
</Title>
|
||||||
<Title order={4} c="dimmed">
|
<Title order={4} c="dimmed">
|
||||||
#PGf-2345-33
|
#{data?.noPengajuan}
|
||||||
</Title>
|
</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Badge
|
<Badge
|
||||||
size="xl"
|
size="xl"
|
||||||
variant="light"
|
variant="light"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
color={"yellow"}
|
color={
|
||||||
|
data?.status === "diterima"
|
||||||
|
? "green"
|
||||||
|
: data?.status === "ditolak"
|
||||||
|
? "red"
|
||||||
|
: data?.status === "selesai"
|
||||||
|
? "blue"
|
||||||
|
: data?.status === "dikerjakan"
|
||||||
|
? "gray"
|
||||||
|
: "yellow"
|
||||||
|
}
|
||||||
style={{ textTransform: "none" }}
|
style={{ textTransform: "none" }}
|
||||||
>
|
>
|
||||||
antrian
|
{data?.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.Col span={6}>
|
<Grid.Col span={12}>
|
||||||
<Stack gap="md">
|
<Stack gap="lg">
|
||||||
|
<Flex direction={"column"} justify="flex-start">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconFileCheck size={20} />
|
||||||
|
<Text size="md">Syarat Dokumen</Text>
|
||||||
|
</Group>
|
||||||
|
<List
|
||||||
|
spacing="sm"
|
||||||
|
pt={10}
|
||||||
|
icon={
|
||||||
|
<ThemeIcon color="green" size={20} radius="xl">
|
||||||
|
<IconCheck size={13} />
|
||||||
|
</ThemeIcon>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{syaratDokumen?.map((v: any) => (
|
||||||
|
<List.Item key={v.id}>{v.jenis}</List.Item>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Flex direction={"column"} justify="flex-start">
|
<Flex direction={"column"} justify="flex-start">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconAlignJustified size={20} />
|
<IconAlignJustified size={20} />
|
||||||
<Text size="md">Judul</Text>
|
<Text size="md">Data Pelengkap</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c={"white"}>
|
|
||||||
Judul Pelayanan Surat
|
<Table withRowBorders={false}>
|
||||||
</Text>
|
<Table.Tbody>
|
||||||
</Flex>
|
{
|
||||||
<Flex direction={"column"} justify="flex-start">
|
dataText?.map((item: any) => (
|
||||||
<Group gap="xs">
|
<Table.Tr key={item.id}>
|
||||||
<IconMapPin size={20} />
|
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td>
|
||||||
<Text size="md">Lokasi</Text>
|
<Table.Td>:</Table.Td>
|
||||||
</Group>
|
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td>
|
||||||
<Text size="md" c="white">
|
</Table.Tr>
|
||||||
fwef
|
))
|
||||||
</Text>
|
}
|
||||||
</Flex>
|
</Table.Tbody>
|
||||||
</Stack>
|
</Table>
|
||||||
</Grid.Col>
|
|
||||||
<Grid.Col span={6}>
|
|
||||||
<Stack gap="md">
|
|
||||||
<Flex direction={"column"} justify="flex-start">
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCategory size={20} />
|
|
||||||
<Text size="md">Kategori</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="md" c="white">
|
|
||||||
fwef
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
<Flex direction={"column"} justify="flex-start">
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconPhotoScan size={20} />
|
|
||||||
<Text size="md">Gambar</Text>
|
|
||||||
</Group>
|
|
||||||
<Anchor href="https://mantine.dev/" target="_blank">
|
|
||||||
Lihat Gambar
|
|
||||||
</Anchor>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={12}>
|
<Grid.Col span={12}>
|
||||||
<Stack gap="md">
|
{
|
||||||
<Flex direction={"column"} justify="flex-start">
|
data?.status === "antrian" ? (
|
||||||
<Group gap="xs">
|
<Group justify="center" grow>
|
||||||
<IconAlignJustified size={20} />
|
<Button
|
||||||
<Text size="md">Detail</Text>
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("tolak");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("terima");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Terima
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c="white">
|
) : data?.status === "diterima" ? (
|
||||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
<Group justify="center" grow>
|
||||||
Illum, corporis iusto. Suscipit veritatis quas, non nobis
|
<Button
|
||||||
fuga, laudantium accusantium tempora sint aliquid architecto
|
variant="light"
|
||||||
totam esse eum excepturi nostrum fugiat ut.
|
onClick={() => {
|
||||||
</Text>
|
setCatModal("tolak");
|
||||||
</Flex>
|
open();
|
||||||
<Flex direction={"column"} justify="flex-start">
|
}}
|
||||||
<Group gap="xs">
|
>
|
||||||
<IconInfoTriangle size={20} />
|
Tolak
|
||||||
<Text size="md">Keterangan</Text>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
onClick={() => {
|
||||||
|
setCatModal("terima");
|
||||||
|
open();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Setujui
|
||||||
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c={"white"}>
|
) : (
|
||||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. At
|
<Group justify="center" grow>
|
||||||
fugiat eligendi nesciunt dolore? Maiores a cumque vitae
|
<Button
|
||||||
suscipit incidunt quos beatae modi, vel, id ullam quae
|
variant="light"
|
||||||
voluptas, deserunt quas placeat.
|
onClick={() => { }}
|
||||||
</Text>
|
>
|
||||||
</Flex>
|
Lihat Surat
|
||||||
</Stack>
|
</Button>
|
||||||
</Grid.Col>
|
<Button
|
||||||
<Grid.Col span={12}>
|
variant="light"
|
||||||
<Group justify="center" grow>
|
onClick={() => { }}
|
||||||
<Button
|
>
|
||||||
variant="light"
|
Download
|
||||||
onClick={() => {
|
</Button>
|
||||||
setCatModal("tolak");
|
</Group>
|
||||||
open();
|
)
|
||||||
}}
|
}
|
||||||
>
|
|
||||||
Tolak
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
onClick={() => {
|
|
||||||
setCatModal("terima");
|
|
||||||
open();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Terima
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -233,23 +328,7 @@ function DetailDataPelayanan() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailDataHistori() {
|
function DetailDataHistori({ data }: { data: any }) {
|
||||||
const elements = [
|
|
||||||
{ position: 6, mass: 12.011, symbol: "C", name: "Carbon" },
|
|
||||||
{ position: 7, mass: 14.007, symbol: "N", name: "Nitrogen" },
|
|
||||||
{ position: 39, mass: 88.906, symbol: "Y", name: "Yttrium" },
|
|
||||||
{ position: 56, mass: 137.33, symbol: "Ba", name: "Barium" },
|
|
||||||
{ position: 58, mass: 140.12, symbol: "Ce", name: "Cerium" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const rows = elements.map((element) => (
|
|
||||||
<Table.Tr key={element.name}>
|
|
||||||
<Table.Td>{element.position}</Table.Td>
|
|
||||||
<Table.Td>{element.name}</Table.Td>
|
|
||||||
<Table.Td>{element.symbol}</Table.Td>
|
|
||||||
<Table.Td>{element.mass}</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
));
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -265,7 +344,7 @@ function DetailDataHistori() {
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Histori Pengaduan
|
Histori Pengajuan Surat
|
||||||
</Title>
|
</Title>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
@@ -278,33 +357,25 @@ function DetailDataHistori() {
|
|||||||
<Table.Th>User</Table.Th>
|
<Table.Th>User</Table.Th>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>{rows}</Table.Tbody>
|
<Table.Tbody>
|
||||||
|
{
|
||||||
|
data?.map((item: any) => (
|
||||||
|
<Table.Tr key={item.id}>
|
||||||
|
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||||
|
<Table.Td>{item.deskripsi}</Table.Td>
|
||||||
|
<Table.Td>{item.status}</Table.Td>
|
||||||
|
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Table.Tbody>
|
||||||
</Table>
|
</Table>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailUserPelayanan() {
|
function DetailUserPengajuan({ data }: { data: any }) {
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
const [value, setValue] = useState("");
|
|
||||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
|
||||||
apiFetch.api.pengaduan.list.get({
|
|
||||||
query: {
|
|
||||||
status,
|
|
||||||
search: value,
|
|
||||||
take: "",
|
|
||||||
page: "",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
|
||||||
mutate();
|
|
||||||
}, [status, value]);
|
|
||||||
|
|
||||||
const list = data?.data || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -333,16 +404,16 @@ function DetailUserPelayanan() {
|
|||||||
<Text size="md">Nama</Text>
|
<Text size="md">Nama</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c={"white"}>
|
<Text size="md" c={"white"}>
|
||||||
Amalia Dwi Yustiani
|
{data?.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconMapPin size={20} />
|
<IconPhone size={20} />
|
||||||
<Text size="md">Telepon</Text>
|
<Text size="md">Telepon</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c="white">
|
<Text size="md" c="white">
|
||||||
08123456789
|
{data?.phone}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -351,7 +422,7 @@ function DetailUserPelayanan() {
|
|||||||
<Text size="md">Jumlah Pengaduan</Text>
|
<Text size="md">Jumlah Pengaduan</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c="white">
|
<Text size="md" c="white">
|
||||||
10
|
{data?.pengaduan}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -360,7 +431,7 @@ function DetailUserPelayanan() {
|
|||||||
<Text size="md">Jumlah Pelayanan Surat</Text>
|
<Text size="md">Jumlah Pelayanan Surat</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md" c="white">
|
<Text size="md" c="white">
|
||||||
10
|
{data?.pelayanan}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import {
|
import {
|
||||||
IconAlignJustified,
|
|
||||||
IconClockHour3,
|
IconClockHour3,
|
||||||
IconFileSad,
|
IconFileSad,
|
||||||
IconMapPin,
|
|
||||||
IconSearch,
|
IconSearch,
|
||||||
|
IconUser
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import useSwr from "swr";
|
import useSwr from "swr";
|
||||||
import { proxy } from "valtio";
|
import { proxy, subscribe } from "valtio";
|
||||||
|
|
||||||
const state = proxy({ reload: "" });
|
const state = proxy({ reload: "" });
|
||||||
function reloadState() {
|
function reloadState() {
|
||||||
@@ -49,14 +48,14 @@ export default function PelayananSuratListPage() {
|
|||||||
function TabListPelayananSurat({ status }: { status: string }) {
|
function TabListPelayananSurat({ status }: { status: string }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dataCount = useSwr("/pelayanan-surat/count", () =>
|
const dataCount = useSwr("/pelayanan-surat/count", () =>
|
||||||
apiFetch.api.pengaduan.count.get().then((res) => res.data),
|
apiFetch.api.pelayanan.count.get().then((res) => res.data),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue={status || "semua"} color="teal">
|
<Tabs defaultValue={status || "semua"} color="teal">
|
||||||
<Tabs.List grow>
|
<Tabs.List grow>
|
||||||
<Tabs.Tab
|
<Tabs.Tab
|
||||||
value="all"
|
value="semua"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("?status=semua");
|
navigate("?status=semua");
|
||||||
}}
|
}}
|
||||||
@@ -118,21 +117,29 @@ type StatusKey =
|
|||||||
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
const { data, mutate, isLoading } = useSwr("/", async () => {
|
||||||
apiFetch.api.pengaduan.list.get({
|
const res = await apiFetch.api.pelayanan.list.get({
|
||||||
query: {
|
query: {
|
||||||
status,
|
status,
|
||||||
search: value,
|
search: value,
|
||||||
take: "",
|
take: "",
|
||||||
page: "",
|
page: "",
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||||
|
});
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
mutate();
|
mutate();
|
||||||
}, [status, value]);
|
}, [status, value]);
|
||||||
|
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const unsubscribe = subscribe(state, () => mutate());
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
@@ -147,19 +154,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Loading pengaduan...
|
Loading pelayanan surat...
|
||||||
</Text>
|
</Text>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = data?.data || [];
|
const list = data || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Cari pengaduan..."
|
placeholder="Cari pengajuan..."
|
||||||
onChange={(event) => setValue(event.currentTarget.value)}
|
onChange={(event) => setValue(event.currentTarget.value)}
|
||||||
leftSection={<IconSearch size={16} />}
|
leftSection={<IconSearch size={16} />}
|
||||||
rightSectionPointerEvents="all"
|
rightSectionPointerEvents="all"
|
||||||
@@ -204,11 +211,11 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
|||||||
<Flex align="center" justify="space-between">
|
<Flex align="center" justify="space-between">
|
||||||
<Flex direction={"column"}>
|
<Flex direction={"column"}>
|
||||||
<Title order={3} c="gray.2">
|
<Title order={3} c="gray.2">
|
||||||
{v.title}
|
{v.category}
|
||||||
</Title>
|
</Title>
|
||||||
<Group>
|
<Group>
|
||||||
<Title order={6} c="gray.5">
|
<Title order={6} c="gray.5">
|
||||||
#{v.noPengaduan}
|
#{v.noPengajuan}
|
||||||
</Title>
|
</Title>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{v.updatedAt}
|
{v.updatedAt}
|
||||||
@@ -227,7 +234,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
|||||||
: v.status === "selesai"
|
: v.status === "selesai"
|
||||||
? "blue"
|
? "blue"
|
||||||
: v.status === "dikerjakan"
|
: v.status === "dikerjakan"
|
||||||
? "purple"
|
? "gray"
|
||||||
: "yellow"
|
: "yellow"
|
||||||
}
|
}
|
||||||
style={{ textTransform: "none" }}
|
style={{ textTransform: "none" }}
|
||||||
@@ -241,28 +248,19 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
|||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconClockHour3 size={20} color="white" />
|
<IconClockHour3 size={20} color="white" />
|
||||||
<Text size="md" c="white">
|
<Text size="md" c="white">
|
||||||
Tanggal Aduan
|
Tanggal Ajuan
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md">{v.createdAt}</Text>
|
<Text size="md">{v.createdAt}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex direction={"column"} justify="flex-start">
|
<Flex direction={"column"} justify="flex-start">
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<IconMapPin size={20} color="white" />
|
<IconUser size={20} color="white" />
|
||||||
<Text size="md" c="white">
|
<Text size="md" c="white">
|
||||||
Lokasi
|
Warga
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Text size="md">{v.location}</Text>
|
<Text size="md">{v.warga}</Text>
|
||||||
</Flex>
|
|
||||||
<Flex direction={"column"} justify="flex-start">
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconAlignJustified size={20} color="white" />
|
|
||||||
<Text size="md" c="white">
|
|
||||||
Detail
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Text size="md">{v.detail}</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
24
src/server/lib/create-surat.ts
Normal file
24
src/server/lib/create-surat.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { prisma } from "./prisma"
|
||||||
|
|
||||||
|
export async function createSurat({ idPengajuan, idCategory, idWarga, noSurat }: { idPengajuan: string, idCategory: string, idWarga: string, noSurat: string }) {
|
||||||
|
try {
|
||||||
|
const surat = await prisma.suratPelayanan.create({
|
||||||
|
data: {
|
||||||
|
idPengajuanLayanan: idPengajuan,
|
||||||
|
idCategory,
|
||||||
|
idWarga,
|
||||||
|
noSurat,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!surat.id) {
|
||||||
|
return { success: false, message: 'gagal membuat surat' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: 'surat sudah dibuat' }
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return { success: false, message: 'gagal membuat surat' }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import Elysia, { t } from "elysia"
|
import Elysia, { t } from "elysia"
|
||||||
import type { StatusPengaduan } from "generated/prisma"
|
import type { StatusPengaduan } from "generated/prisma"
|
||||||
|
import { createSurat } from "../lib/create-surat"
|
||||||
|
import { getLastUpdated } from "../lib/get-last-updated"
|
||||||
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
|
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
|
||||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||||
import { prisma } from "../lib/prisma"
|
import { prisma } from "../lib/prisma"
|
||||||
@@ -102,28 +104,179 @@ const PelayananRoute = new Elysia({
|
|||||||
|
|
||||||
|
|
||||||
// --- PELAYANAN SURAT ---
|
// --- PELAYANAN SURAT ---
|
||||||
.get("/", async () => {
|
.get("/", async ({ query }) => {
|
||||||
|
const { phone } = query
|
||||||
const data = await prisma.pelayananAjuan.findMany({
|
const data = await prisma.pelayananAjuan.findMany({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc"
|
||||||
|
},
|
||||||
where: {
|
where: {
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
Warga: {
|
||||||
|
phone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
}, {
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||||
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "List Ajuan Pelayanan Surat",
|
summary: "List Ajuan Pelayanan Surat by Phone",
|
||||||
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
|
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
|
||||||
tags: ["mcp"]
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get("/detail", async ({ query }) => {
|
.get("/detail", async ({ query }) => {
|
||||||
const { id } = query
|
const { id } = query
|
||||||
const data = await prisma.pelayananAjuan.findUnique({
|
|
||||||
|
const data = await prisma.pelayananAjuan.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id,
|
OR: [
|
||||||
|
{
|
||||||
|
noPengajuan: id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengajuan: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
CategoryPelayanan: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
dataText: true,
|
||||||
|
syaratDokumen: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Warga: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
phone: true,
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
Pengaduan: true,
|
||||||
|
PelayananAjuan: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return data
|
|
||||||
|
const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({
|
||||||
|
where: {
|
||||||
|
idPengajuanLayanan: data?.id,
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
jenis: true,
|
||||||
|
value: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataText = await prisma.dataTextPelayanan.findMany({
|
||||||
|
where: {
|
||||||
|
idPengajuanLayanan: data?.id,
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
value: true,
|
||||||
|
jenis: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
|
||||||
|
name: string;
|
||||||
|
desc: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const dataSyaratFix = dataSyarat.map((item) => {
|
||||||
|
const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
jenis: desc,
|
||||||
|
value: item.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataTextFix = dataText.map((item) => {
|
||||||
|
const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
jenis: item.jenis,
|
||||||
|
value: item.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataHistory = await prisma.historyPelayanan.findMany({
|
||||||
|
where: {
|
||||||
|
idPengajuanLayanan: data?.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
deskripsi: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
idUser: true,
|
||||||
|
User: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataHistoryFix = dataHistory.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
deskripsi: item.deskripsi,
|
||||||
|
status: item.status,
|
||||||
|
createdAt: item.createdAt.toLocaleString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false
|
||||||
|
}),
|
||||||
|
idUser: item.idUser,
|
||||||
|
nameUser: item.User?.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const warga = {
|
||||||
|
name: data?.Warga?.name,
|
||||||
|
phone: data?.Warga?.phone,
|
||||||
|
pengaduan: data?.Warga?._count.Pengaduan,
|
||||||
|
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataPengajuan = {
|
||||||
|
id: data?.id,
|
||||||
|
noPengajuan: data?.noPengajuan,
|
||||||
|
category: data?.CategoryPelayanan.name,
|
||||||
|
status: data?.status,
|
||||||
|
createdAt: data?.createdAt,
|
||||||
|
updatedAt: data?.updatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
const datafix = {
|
||||||
|
pengajuan: dataPengajuan,
|
||||||
|
history: dataHistoryFix,
|
||||||
|
warga: warga,
|
||||||
|
syaratDokumen: dataSyaratFix,
|
||||||
|
dataText: dataTextFix,
|
||||||
|
}
|
||||||
|
|
||||||
|
return datafix
|
||||||
}, {
|
}, {
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
@@ -135,20 +288,20 @@ const PelayananRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/create", async ({ body }) => {
|
.post("/create", async ({ body }) => {
|
||||||
const { idCategory, idWarga, phone, dataText, syaratDokumen } = body
|
const { kategoriId, wargaId, noTelepon, dataText, syaratDokumen } = body
|
||||||
const noPengajuan = await generateNoPengajuanSurat()
|
const noPengajuan = await generateNoPengajuanSurat()
|
||||||
let idCategoryFix = idCategory
|
let idCategoryFix = kategoriId
|
||||||
let idWargaFix = idWarga
|
let idWargaFix = wargaId
|
||||||
const category = await prisma.categoryPelayanan.findUnique({
|
const category = await prisma.categoryPelayanan.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: idCategory,
|
id: kategoriId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
const cariCategory = await prisma.categoryPelayanan.findFirst({
|
const cariCategory = await prisma.categoryPelayanan.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: idCategory,
|
name: kategoriId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -162,12 +315,12 @@ const PelayananRoute = new Elysia({
|
|||||||
|
|
||||||
const warga = await prisma.warga.findUnique({
|
const warga = await prisma.warga.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: idWarga,
|
id: wargaId,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!warga) {
|
if (!warga) {
|
||||||
const nomorHP = normalizePhoneNumber({ phone })
|
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||||
const cariWarga = await prisma.warga.findFirst({
|
const cariWarga = await prisma.warga.findFirst({
|
||||||
where: {
|
where: {
|
||||||
phone: nomorHP,
|
phone: nomorHP,
|
||||||
@@ -177,7 +330,7 @@ const PelayananRoute = new Elysia({
|
|||||||
if (!cariWarga) {
|
if (!cariWarga) {
|
||||||
const wargaCreate = await prisma.warga.create({
|
const wargaCreate = await prisma.warga.create({
|
||||||
data: {
|
data: {
|
||||||
name: idWarga,
|
name: wargaId,
|
||||||
phone: nomorHP,
|
phone: nomorHP,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -203,7 +356,7 @@ const PelayananRoute = new Elysia({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!pengaduan.id) {
|
if (!pengaduan.id) {
|
||||||
throw new Error("gagal membuat pengajuan surat")
|
return { success: false, message: 'gagal membuat pengajuan surat' }
|
||||||
}
|
}
|
||||||
|
|
||||||
let dataInsertSyaratDokumen = []
|
let dataInsertSyaratDokumen = []
|
||||||
@@ -246,17 +399,81 @@ const PelayananRoute = new Elysia({
|
|||||||
return { success: true, message: 'pengajuan surat sudah dibuat' }
|
return { success: true, message: 'pengajuan surat sudah dibuat' }
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
idCategory: t.String({ minLength: 1, error: "idCategory harus diisi" }),
|
kategoriId: t.String({
|
||||||
idWarga: t.String({ minLength: 1, error: "idWarga harus diisi" }),
|
minLength: 1,
|
||||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
description: "ID atau nama kategori pelayanan surat yang dipilih. Jika berupa nama, sistem akan mencocokkan secara otomatis.",
|
||||||
dataText: t.Array(t.Object({
|
examples: ["skusaha"],
|
||||||
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
|
error: "ID kategori harus diisi"
|
||||||
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
}),
|
||||||
})),
|
|
||||||
syaratDokumen: t.Array(t.Object({
|
wargaId: t.String({
|
||||||
jenis: t.String({ minLength: 1, error: "jenis harus diisi" }),
|
minLength: 1,
|
||||||
value: t.String({ minLength: 1, error: "value harus diisi" }),
|
description: "ID warga atau nama warga. Jika ID tidak ditemukan, sistem akan mencari berdasarkan nama.",
|
||||||
})),
|
examples: ["Budi Santoso"],
|
||||||
|
error: "ID warga harus diisi"
|
||||||
|
}),
|
||||||
|
|
||||||
|
noTelepon: t.String({
|
||||||
|
minLength: 8,
|
||||||
|
description: "Nomor HP warga yang akan dinormalisasi. Jika data warga tidak ditemukan berdasarkan idWarga, pencarian dilakukan via nomor ini.",
|
||||||
|
examples: ["081234567890"],
|
||||||
|
error: "Nomor telepon harus diisi"
|
||||||
|
}),
|
||||||
|
|
||||||
|
dataText: t.Array(
|
||||||
|
t.Object({
|
||||||
|
jenis: t.String({
|
||||||
|
minLength: 1,
|
||||||
|
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
||||||
|
examples: ["nama", "alamat", "pekerjaan", "keperluan"],
|
||||||
|
error: "jenis harus diisi"
|
||||||
|
}),
|
||||||
|
value: t.String({
|
||||||
|
minLength: 1,
|
||||||
|
description: "Isi atau nilai dari jenis field terkait.",
|
||||||
|
examples: ["Budi Santoso", "Jl. Mawar No. 10", "Karyawan Swasta"],
|
||||||
|
error: "value harus diisi"
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
description: "Kumpulan data text dinamis sesuai kategori layanan.",
|
||||||
|
examples: [
|
||||||
|
[
|
||||||
|
{ jenis: "jenis usaha", value: "usaha makanan" },
|
||||||
|
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
|
||||||
|
]
|
||||||
|
],
|
||||||
|
error: "dataText harus berupa array"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
syaratDokumen: t.Array(
|
||||||
|
t.Object({
|
||||||
|
jenis: t.String({
|
||||||
|
minLength: 1,
|
||||||
|
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
|
||||||
|
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
||||||
|
error: "jenis harus diisi"
|
||||||
|
}),
|
||||||
|
value: t.String({
|
||||||
|
minLength: 1,
|
||||||
|
description: "Nama file atau identifier file dokumen yang diupload.",
|
||||||
|
examples: ["ktp_budi.png", "kk_budi.png"],
|
||||||
|
error: "value harus diisi"
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
|
||||||
|
examples: [
|
||||||
|
[
|
||||||
|
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
|
||||||
|
{ jenis: "ktp/kk", value: "kk_budi.png" },
|
||||||
|
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
|
||||||
|
]
|
||||||
|
],
|
||||||
|
error: "syaratDokumen harus berupa array"
|
||||||
|
}
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Create Pengajuan Pelayanan Surat",
|
summary: "Create Pengajuan Pelayanan Surat",
|
||||||
@@ -265,7 +482,9 @@ const PelayananRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post("/update-status", async ({ body }) => {
|
.post("/update-status", async ({ body }) => {
|
||||||
const { id, status, keterangan, idUser } = body
|
const { id, status, keterangan, idUser, noSurat } = body
|
||||||
|
let deskripsi = ""
|
||||||
|
|
||||||
|
|
||||||
const pengajuan = await prisma.pelayananAjuan.update({
|
const pengajuan = await prisma.pelayananAjuan.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -273,28 +492,48 @@ const PelayananRoute = new Elysia({
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
status: status as StatusPengaduan,
|
status: status as StatusPengaduan,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
idCategory: true,
|
||||||
|
idWarga: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!pengajuan) {
|
if (!pengajuan) {
|
||||||
throw new Error("gagal membuat pengajuan")
|
return { success: false, message: 'gagal update status pengajuan surat' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "diterima") {
|
||||||
|
deskripsi = "Pengajuan surat diterima"
|
||||||
|
} else if (status === "ditolak") {
|
||||||
|
deskripsi = "Pengajuan surat ditolak dengan keterangan " + keterangan
|
||||||
|
} else if (status === "selesai") {
|
||||||
|
deskripsi = "Pengajuan surat disetujui"
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.historyPelayanan.create({
|
await prisma.historyPelayanan.create({
|
||||||
data: {
|
data: {
|
||||||
idPengajuanLayanan: pengajuan.id,
|
idPengajuanLayanan: pengajuan.id,
|
||||||
deskripsi: "Pengajuan surat diperbarui",
|
deskripsi: deskripsi,
|
||||||
|
status: status as StatusPengaduan,
|
||||||
|
idUser,
|
||||||
keteranganAlasan: keterangan,
|
keteranganAlasan: keterangan,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (status === "selesai") {
|
||||||
|
await createSurat({ idPengajuan: pengajuan.id, idCategory: pengajuan.idCategory, idWarga: pengajuan.idWarga, noSurat })
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, message: 'pengajuan surat sudah diperbarui' }
|
return { success: true, message: 'pengajuan surat sudah diperbarui' }
|
||||||
}, {
|
}, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
status: t.String({ minLength: 1, error: "status harus diisi" }),
|
||||||
keterangan: t.String({ minLength: 1, error: "keterangan harus diisi" }),
|
keterangan: t.String({ optional: true }),
|
||||||
idUser: t.String({ minLength: 1, error: "idUser harus diisi" }),
|
idUser: t.String({ optional: true }),
|
||||||
|
noSurat: t.String({ optional: true }),
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Update Status Pengajuan Pelayanan Surat",
|
summary: "Update Status Pengajuan Pelayanan Surat",
|
||||||
@@ -302,5 +541,128 @@ const PelayananRoute = new Elysia({
|
|||||||
tags: ["mcp"]
|
tags: ["mcp"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.get("/list", async ({ query }) => {
|
||||||
|
const { take, page, search, status } = query
|
||||||
|
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||||
|
|
||||||
|
let where: any = {
|
||||||
|
isActive: true,
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
CategoryPelayanan: {
|
||||||
|
name: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noPengajuan: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Warga: {
|
||||||
|
phone: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status !== "semua") {
|
||||||
|
where = {
|
||||||
|
...where,
|
||||||
|
status: status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await prisma.pelayananAjuan.findMany({
|
||||||
|
skip,
|
||||||
|
take: !take ? 10 : Number(take),
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc"
|
||||||
|
},
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengajuan: true,
|
||||||
|
status: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
CategoryPelayanan: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Warga: {
|
||||||
|
select: {
|
||||||
|
name: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataFix = data.map((item) => {
|
||||||
|
return {
|
||||||
|
noPengajuan: item.noPengajuan,
|
||||||
|
id: item.id,
|
||||||
|
category: item.CategoryPelayanan.name,
|
||||||
|
warga: item.Warga.name,
|
||||||
|
status: item.status,
|
||||||
|
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||||
|
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return dataFix
|
||||||
|
}, {
|
||||||
|
query: t.Object({
|
||||||
|
take: t.String({ optional: true }),
|
||||||
|
page: t.String({ optional: true }),
|
||||||
|
search: t.String({ optional: true }),
|
||||||
|
status: t.String({ optional: true }),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "List Pengajuan Pelayanan Surat Warga",
|
||||||
|
description: `tool untuk mendapatkan list pengajuan pelayanan surat warga`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/count", async ({ query }) => {
|
||||||
|
const counts = await prisma.pelayananAjuan.groupBy({
|
||||||
|
by: ['status'],
|
||||||
|
where: {
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
status: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped = Object.fromEntries(
|
||||||
|
counts.map(c => [c.status, c._count.status])
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = await prisma.pelayananAjuan.count({
|
||||||
|
where: { isActive: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
antrian: grouped?.antrian || 0,
|
||||||
|
diterima: grouped?.diterima || 0,
|
||||||
|
dikerjakan: grouped?.dikerjakan || 0,
|
||||||
|
ditolak: grouped?.ditolak || 0,
|
||||||
|
selesai: grouped?.selesai || 0,
|
||||||
|
semua: total,
|
||||||
|
};
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Jumlah Pengajuan Pelayanan Surat Warga",
|
||||||
|
description: `tool untuk mendapatkan jumlah pengajuan pelayanan surat warga`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default PelayananRoute
|
export default PelayananRoute
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ Respon:
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!pengaduan) {
|
if (!pengaduan) {
|
||||||
throw new Error("gagal membuat pengaduan")
|
return { success: false, message: 'gagal update status pengaduan' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === "diterima") {
|
if (status === "diterima") {
|
||||||
|
|||||||
Reference in New Issue
Block a user