upd: form surat

Deskripsi:
- api detail categori list
- form awal buat surat

No Issues
This commit is contained in:
2025-12-16 17:38:13 +08:00
parent a13e51a724
commit 7c6e4ac9eb
38 changed files with 4236 additions and 2941 deletions

View File

@@ -14,6 +14,7 @@ import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterang
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran"; import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha"; import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik"; import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
import Surat from "./pages/darmasaba/surat";
import Home from "./pages/Home"; import Home from "./pages/Home";
import CredentialPage from "./pages/scr/dashboard/credential/credential_page"; import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
import DashboardHome from "./pages/scr/dashboard/dashboard_home"; import DashboardHome from "./pages/scr/dashboard/dashboard_home";
@@ -84,6 +85,7 @@ export default function AppRoutes() {
path="/darmasaba/surat-keterangan-kelakuan-baik" path="/darmasaba/surat-keterangan-kelakuan-baik"
element={<FormSuratKeteranganKelakuanBaik />} element={<FormSuratKeteranganKelakuanBaik />}
/> />
<Route path="/darmasaba/surat" element={<Surat />} />
</Route> </Route>
<Route path="/" element={<Home />} /> <Route path="/" element={<Home />} />

View File

@@ -14,6 +14,7 @@ const clientRoutes = {
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran", "/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha", "/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
"/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik", "/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik",
"/darmasaba/surat": "/darmasaba/surat",
"/": "/", "/": "/",
"/scr": "/scr", "/scr": "/scr",
"/scr/dashboard": "/scr/dashboard", "/scr/dashboard": "/scr/dashboard",

View File

@@ -1,98 +1,101 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core"; import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { IconFileCertificate, IconMessageReport, IconUsers } from "@tabler/icons-react"; import {
IconFileCertificate,
IconMessageReport,
IconUsers,
} from "@tabler/icons-react";
import useSWR from "swr"; import useSWR from "swr";
export default function DashboardCountData() { export default function DashboardCountData() {
const { data, mutate, isLoading } = useSWR("/", () => const { data, mutate, isLoading } = useSWR("/", () =>
apiFetch.api.dashboard.count.get() apiFetch.api.dashboard.count.get(),
); );
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
return ( return (
<Grid> <Grid>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard <MetricCard
icon={<IconMessageReport size={28} />} icon={<IconMessageReport size={28} />}
label="Pengaduan Hari Ini" label="Pengaduan Hari Ini"
value={String(data?.data?.pengaduan?.today)} value={String(data?.data?.pengaduan?.today)}
change={String(data?.data?.pengaduan?.kenaikan) + "%"} change={String(data?.data?.pengaduan?.kenaikan) + "%"}
color={"gray"} color={"gray"}
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard <MetricCard
icon={<IconFileCertificate size={28} />} icon={<IconFileCertificate size={28} />}
label="Pengajuan Surat Hari Ini" label="Pengajuan Surat Hari Ini"
value={String(data?.data?.pelayanan?.today)} value={String(data?.data?.pelayanan?.today)}
change={String(data?.data?.pelayanan?.kenaikan) + "%"} change={String(data?.data?.pelayanan?.kenaikan) + "%"}
color="gray" color="gray"
/> />
</Grid.Col> </Grid.Col>
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}> <Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
<MetricCard <MetricCard
icon={<IconUsers size={28} />} icon={<IconUsers size={28} />}
label="Warga" label="Warga"
value={String(data?.data?.warga)} value={String(data?.data?.warga)}
color="blue" color="blue"
/> />
</Grid.Col> </Grid.Col>
</Grid> </Grid>
); );
} }
function MetricCard({ function MetricCard({
icon, icon,
label, label,
value, value,
change, change,
color, color,
}: { }: {
icon: React.ReactNode; icon: React.ReactNode;
label: string; label: string;
value: string; value: string;
change?: string; change?: string;
color: string; color: string;
}) { }) {
return ( return (
<Card <Card
radius="lg" radius="lg"
p="md" p="md"
withBorder withBorder
style={{ style={{
background: background:
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))", "linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
borderColor: "rgba(100,100,100,0.2)", borderColor: "rgba(100,100,100,0.2)",
transition: "transform 0.15s ease, box-shadow 0.15s ease", transition: "transform 0.15s ease, box-shadow 0.15s ease",
}} }}
onMouseEnter={(e) => onMouseEnter={(e) =>
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)") (e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
} }
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")} onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
> >
<Stack gap={6}> <Stack gap={6}>
<Group gap={6}> <Group gap={6}>
{icon} {icon}
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
{label} {label}
</Text> </Text>
</Group> </Group>
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Text fw={600} size="xl" c="gray.0"> <Text fw={600} size="xl" c="gray.0">
{value} {value}
</Text> </Text>
{change && ( {change && (
<Text size="sm" c={color}> <Text size="sm" c={color}>
{change} {change}
</Text> </Text>
)} )}
</Flex> </Flex>
</Stack> </Stack>
</Card> </Card>
); );
} }

View File

@@ -7,77 +7,71 @@ import { useEffect, useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
export default function DashboardGrafik() { export default function DashboardGrafik() {
const [options, setOptions] = useState<EChartsOption>({}); const [options, setOptions] = useState<EChartsOption>({});
const { data, mutate, isLoading } = useSWR( const { data, mutate, isLoading } = useSWR("grafik-dashboard", async () => {
"grafik-dashboard", return apiFetch.api.dashboard.grafik.get().then((res) => res.data);
async () => { });
return apiFetch.api.dashboard.grafik.get().then(res => res.data);
}
);
const loadData = () => { const loadData = () => {
if (!data) return; if (!data) return;
const option: EChartsOption = { const option: EChartsOption = {
darkMode: true, darkMode: true,
animation: true, animation: true,
legend: { legend: {
textStyle: { color: "#fff" } textStyle: { color: "#fff" },
}, },
tooltip: {}, tooltip: {},
dataset: { dataset: {
dimensions: data.dimensions, dimensions: data.dimensions,
source: data.source source: data.source,
}, },
xAxis: { xAxis: {
type: "category", type: "category",
axisLabel: { color: "#fff" } axisLabel: { color: "#fff" },
}, },
yAxis: { yAxis: {
type: "value", type: "value",
minInterval: 1, minInterval: 1,
axisLabel: { color: "#fff" } axisLabel: { color: "#fff" },
}, },
color: ["#1abc9c", "#10816aff"], color: ["#1abc9c", "#10816aff"],
series: [ series: [{ type: "bar" }, { type: "bar" }],
{ type: "bar" }, };
{ type: "bar" }
]
};
setOptions(option); setOptions(option);
}; };
useEffect(() => { useEffect(() => {
if (data) loadData(); if (data) loadData();
}, [data]); }, [data]);
return ( return (
<Card <Card
radius="lg" radius="lg"
p="xl" p="xl"
withBorder withBorder
style={{ style={{
background: background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))", "linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)", borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)", boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}} }}
> >
<Stack gap="sm"> <Stack gap="sm">
<Flex align="center" justify="space-between"> <Flex align="center" justify="space-between">
<Flex direction={"column"}> <Flex direction={"column"}>
<Title order={4} c="gray.0"> <Title order={4} c="gray.0">
Grafik Pengaduan dan Pelayanan Surat Grafik Pengaduan dan Pelayanan Surat
</Title> </Title>
<Text size="sm">7 Hari Terakhir</Text> <Text size="sm">7 Hari Terakhir</Text>
</Flex> </Flex>
<IconChartBar size={20} color="gray" /> <IconChartBar size={20} color="gray" />
</Flex> </Flex>
<Divider my="xs" /> <Divider my="xs" />
<Stack gap="sm"> <Stack gap="sm">
<EChartsReact style={{ height: 400 }} option={options} /> <EChartsReact style={{ height: 400 }} option={options} />
</Stack> </Stack>
</Stack> </Stack>
</Card> </Card>
) );
} }

View File

@@ -1,133 +1,210 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { Badge, Button, Card, Flex, Group, Stack, Text, Title, Tooltip } from "@mantine/core"; import {
Badge,
Button,
Card,
Flex,
Group,
Stack,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks"; import { useShallowEffect } from "@mantine/hooks";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import useSWR from "swr"; import useSWR from "swr";
export default function DashboardLastData() { export default function DashboardLastData() {
const navigate = useNavigate(); const navigate = useNavigate();
const { data, mutate, isLoading } = useSWR("last-update", async () => { const { data, mutate, isLoading } = useSWR("last-update", async () => {
const res = await apiFetch.api.dashboard["last-update"].get(); const res = await apiFetch.api.dashboard["last-update"].get();
return res.data return res.data;
}); });
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
return (
<Flex justify="flex-start" gap="md">
<Card
radius="lg"
p="xl"
withBorder
w={"50%"}
style={{
background:
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
borderColor: "rgba(100,100,100,0.2)",
boxShadow: "0 0 25px rgba(0,255,200,0.08)",
}}
>
<Stack gap="sm">
<Flex
align="center"
pb={"sm"}
justify="space-between"
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
>
<Title order={4} c="gray.0">
Last update pengaduan
</Title>
<Button
variant="subtle"
size="xs"
radius="md"
onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}
>
View All
</Button>
</Flex>
<Stack gap="sm" mt="md" align="stretch" justify="center">
{data &&
Array.isArray(data.pengaduan) &&
data.pengaduan.length > 0 ? (
data.pengaduan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengaduan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pengaduan"
/>
))
) : (
<Text c="dimmed" ta={"center"}>
Tidak ada data
</Text>
)}
</Stack>
</Stack>
</Card>
return ( <Card
<Flex justify="flex-start" gap="md"> radius="lg"
<Card p="xl"
radius="lg" withBorder
p="xl" w={"50%"}
withBorder style={{
w={"50%"} background:
style={{ "linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
background: borderColor: "rgba(100,100,100,0.2)",
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))", boxShadow: "0 0 25px rgba(0,255,200,0.08)",
borderColor: "rgba(100,100,100,0.2)", }}
boxShadow: "0 0 25px rgba(0,255,200,0.08)", >
}} <Stack gap="sm">
> <Flex
<Stack gap="sm"> align="center"
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}> pb={"sm"}
<Title order={4} c="gray.0"> justify="space-between"
Last update pengaduan style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
</Title> >
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pengaduan/list`)}>View All</Button> <Title order={4} c="gray.0">
</Flex> Last update pelayanan surat
<Stack gap="sm" mt="md" align="stretch" justify="center"> </Title>
{ <Button
data && Array.isArray(data.pengaduan) && data.pengaduan.length > 0 ? data.pengaduan.map((item: any, index: number) => ( variant="subtle"
<PengaduanSection size="xs"
key={index} radius="md"
id={item.id} onClick={() =>
nomer={item.noPengaduan} navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)
judul={item.title} }
status={item.status} >
updated={item.updatedAt} View All
kategori="pengaduan" </Button>
/> </Flex>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text> <Stack gap="sm" mt="md" align="stretch" justify="center">
} {data &&
</Stack> Array.isArray(data.pelayanan) &&
</Stack> data.pelayanan.length > 0 ? (
</Card> data.pelayanan.map((item: any, index: number) => (
<PengaduanSection
<Card key={index}
radius="lg" id={item.id}
p="xl" nomer={item.noPengajuan}
withBorder judul={item.title}
w={"50%"} status={item.status}
style={{ updated={item.updatedAt}
background: kategori="pelayanan"
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))", />
borderColor: "rgba(100,100,100,0.2)", ))
boxShadow: "0 0 25px rgba(0,255,200,0.08)", ) : (
}} <Text c="dimmed" ta={"center"}>
> Tidak ada data
<Stack gap="sm"> </Text>
<Flex align="center" pb={"sm"} justify="space-between" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}> )}
<Title order={4} c="gray.0"> </Stack>
Last update pelayanan surat </Stack>
</Title> </Card>
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)}>View All</Button> </Flex>
</Flex> );
<Stack gap="sm" mt="md" align="stretch" justify="center">
{
data && Array.isArray(data.pelayanan) && data.pelayanan.length > 0 ? data.pelayanan.map((item: any, index: number) => (
<PengaduanSection
key={index}
id={item.id}
nomer={item.noPengajuan}
judul={item.title}
status={item.status}
updated={item.updatedAt}
kategori="pelayanan"
/>
)) : <Text c="dimmed" ta={"center"} >Tidak ada data</Text>
}
</Stack>
</Stack>
</Card>
</Flex>
);
} }
function PengaduanSection({ id, nomer, judul, status, updated, kategori }: { id: string, nomer: string, judul: string, status: string, updated: string, kategori: 'pengaduan' | 'pelayanan' }) { function PengaduanSection({
const navigate = useNavigate(); id,
nomer,
judul,
status,
updated,
kategori,
}: {
id: string;
nomer: string;
judul: string;
status: string;
updated: string;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate();
return ( return (
<Stack <Stack
gap="xs" gap="xs"
onClick={() => navigate(kategori == "pelayanan" ? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}` : `/scr/dashboard/pengaduan/detail?id=${id}`)} onClick={() =>
navigate(
kategori == "pelayanan"
? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}`
: `/scr/dashboard/pengaduan/detail?id=${id}`,
)
}
>
<Flex
align="center"
pb={"sm"}
justify="space-between"
gap="md"
style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}
> >
<Flex align="center" pb={"sm"} justify="space-between" gap="md" style={{ borderBottom: "1px solid rgba(255,255,255,0.1)" }}> <Flex direction={"column"}>
<Flex direction={"column"}> <Text size="md" c="gray.2" lineClamp={1}>
<Text size="md" c="gray.2" lineClamp={1}> {judul}
{judul} </Text>
</Text> <Group>
<Group> <Text size="sm" c="dimmed">
<Text size="sm" c="dimmed"> #{nomer} {updated}
#{nomer} {updated} </Text>
</Text> </Group>
</Group> </Flex>
</Flex> <Tooltip label={status}>
<Tooltip label={status}> <Badge
<Badge size="xs" circle color={ size="xs"
status === "diterima" circle
? "green" color={
: status === "ditolak" status === "diterima"
? "red" ? "green"
: status === "selesai" : status === "ditolak"
? "blue" ? "red"
: status === "dikerjakan" : status === "selesai"
? "gray" ? "blue"
: "yellow" : status === "dikerjakan"
} /> ? "gray"
</Tooltip> : "yellow"
</Flex> }
</Stack> />
) </Tooltip>
} </Flex>
</Stack>
);
}

View File

@@ -12,7 +12,7 @@ import {
Stack, Stack,
Table, Table,
Title, Title,
Tooltip Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { IconEdit } from "@tabler/icons-react"; import { IconEdit } from "@tabler/icons-react";
@@ -23,11 +23,15 @@ import useSWR from "swr";
import ModalFile from "./ModalFile"; import ModalFile from "./ModalFile";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function DesaSetting({ permissions }: { permissions: JsonValue[] }) { export default function DesaSetting({
permissions,
}: {
permissions: JsonValue[];
}) {
const [btnDisable, setBtnDisable] = useState(false); const [btnDisable, setBtnDisable] = useState(false);
const [btnLoading, setBtnLoading] = useState(false); const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
const [img, setImg] = useState<any>() const [img, setImg] = useState<any>();
const [openedPreview, setOpenedPreview] = useState(false); const [openedPreview, setOpenedPreview] = useState(false);
const [viewImg, setViewImg] = useState(""); const [viewImg, setViewImg] = useState("");
const { data, mutate, isLoading } = useSWR("/", () => const { data, mutate, isLoading } = useSWR("/", () =>
@@ -51,13 +55,19 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
let finalData = { ...dataEdit }; // ← buffer data terbaru let finalData = { ...dataEdit }; // ← buffer data terbaru
if (dataEdit.name === "TTD") { if (dataEdit.name === "TTD") {
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({ file: dataEdit.value, folder: "lainnya" }); const oldImg = await apiFetch.api.pengaduan["delete-image"].post({
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img, folder: "lainnya" }); file: dataEdit.value,
folder: "lainnya",
});
const resImg = await apiFetch.api.pengaduan.upload.post({
file: img,
folder: "lainnya",
});
if (resImg.status === 200) { if (resImg.status === 200) {
finalData = { finalData = {
...finalData, ...finalData,
value: resImg.data?.filename || "" value: resImg.data?.filename || "",
}; };
setDataEdit(finalData); // update state setDataEdit(finalData); // update state
@@ -70,7 +80,6 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
} }
} }
const res = await apiFetch.api["configuration-desa"].edit.post(finalData); const res = await apiFetch.api["configuration-desa"].edit.post(finalData);
if (res.status === 200) { if (res.status === 200) {
@@ -100,8 +109,11 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
} }
} }
function chooseEdit({
function chooseEdit({ data }: { data: { id: string; value: string; name: string }; }) { data,
}: {
data: { id: string; value: string; name: string };
}) {
setDataEdit(data); setDataEdit(data);
open(); open();
} }
@@ -133,31 +145,27 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
> >
<Stack gap="ld"> <Stack gap="ld">
{ {dataEdit.name == "TTD" ? (
dataEdit.name == "TTD" <Input.Wrapper label={dataEdit.name}>
? <FileInput
( clearable
<Input.Wrapper label={dataEdit.name}> placeholder="Upload TTD"
<FileInput accept="image/*"
clearable onChange={(e) => {
placeholder="Upload TTD" setImg(e);
accept="image/*" }}
onChange={(e) => { setImg(e) }} />
/> </Input.Wrapper>
</Input.Wrapper> ) : (
) <Input.Wrapper label={dataEdit.name}>
: <Input
( value={dataEdit.value}
<Input.Wrapper label={dataEdit.name}> onChange={(e) =>
<Input onValidation({ kat: "value", value: e.target.value })
value={dataEdit.value} }
onChange={(e) => />
onValidation({ kat: "value", value: e.target.value }) </Input.Wrapper>
} )}
/>
</Input.Wrapper>
)
}
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
@@ -203,21 +211,33 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
<Table.Tr key={v.id}> <Table.Tr key={v.id}>
<Table.Td>{v.name}</Table.Td> <Table.Td>{v.name}</Table.Td>
<Table.Td> <Table.Td>
{ {v.name == "TTD" ? (
v.name == "TTD" v.value ? (
? <Anchor
v.value ? href="#"
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always"> onClick={() => {
Lihat setViewImg(v.value);
</Anchor> setOpenedPreview(true);
: }}
"-" underline="always"
: >
v.value Lihat
} </Anchor>
) : (
"-"
)
) : (
v.value
)}
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Tooltip label={permissions.includes("setting.desa.edit") ? "Edit Setting" : "Edit Setting - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes("setting.desa.edit")
? "Edit Setting"
: "Edit Setting - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"

View File

@@ -23,7 +23,11 @@ import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function KategoriPelayananSurat({ permissions }: { permissions: JsonValue[] }) { export default function KategoriPelayananSurat({
permissions,
}: {
permissions: JsonValue[];
}) {
const [openedDelete, { open: openDelete, close: closeDelete }] = const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false); useDisclosure(false);
const [openedDetail, { open: openDetail, close: closeDetail }] = const [openedDetail, { open: openDetail, close: closeDetail }] =
@@ -53,7 +57,6 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
mutate(); mutate();
}, []); }, []);
async function handleCreate() { async function handleCreate() {
try { try {
setBtnLoading(true); setBtnLoading(true);
@@ -535,19 +538,17 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
Kategori Pelayanan Surat Kategori Pelayanan Surat
</Title> </Title>
{ {permissions.includes("setting.kategori_pelayanan.tambah") && (
permissions.includes("setting.kategori_pelayanan.tambah") && ( <Tooltip label="Tambah Kategori Pelayanan Surat">
<Tooltip label="Tambah Kategori Pelayanan Surat"> <Button
<Button variant="light"
variant="light" leftSection={<IconPlus size={20} />}
leftSection={<IconPlus size={20} />} onClick={openTambah}
onClick={openTambah} >
> Tambah
Tambah </Button>
</Button> </Tooltip>
</Tooltip> )}
)
}
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Stack gap={"md"}> <Stack gap={"md"}>
@@ -578,7 +579,15 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
<IconEye size={20} /> <IconEye size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pelayanan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes(
"setting.kategori_pelayanan.edit",
)
? "Edit Kategori"
: "Edit Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
@@ -587,12 +596,24 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
setDataChoose(v); setDataChoose(v);
open(); open();
}} }}
disabled={!permissions.includes("setting.kategori_pelayanan.edit")} disabled={
!permissions.includes(
"setting.kategori_pelayanan.edit",
)
}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pelayanan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes(
"setting.kategori_pelayanan.delete",
)
? "Hapus Kategori"
: "Hapus Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
@@ -602,7 +623,11 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes("setting.kategori_pelayanan.delete")} disabled={
!permissions.includes(
"setting.kategori_pelayanan.delete",
)
}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

@@ -20,7 +20,11 @@ import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function KategoriPengaduan({ permissions }: { permissions: JsonValue[] }) { export default function KategoriPengaduan({
permissions,
}: {
permissions: JsonValue[];
}) {
const [openedDelete, { open: openDelete, close: closeDelete }] = const [openedDelete, { open: openDelete, close: closeDelete }] =
useDisclosure(false); useDisclosure(false);
const [btnDisable, setBtnDisable] = useState(true); const [btnDisable, setBtnDisable] = useState(true);
@@ -294,19 +298,17 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
Kategori Pengaduan Kategori Pengaduan
</Title> </Title>
{ {permissions.includes("setting.kategori_pengaduan.tambah") && (
permissions.includes("setting.kategori_pengaduan.tambah") && ( <Tooltip label="Tambah Kategori Pengaduan">
<Tooltip label="Tambah Kategori Pengaduan"> <Button
<Button variant="light"
variant="light" leftSection={<IconPlus size={20} />}
leftSection={<IconPlus size={20} />} onClick={openTambah}
onClick={openTambah} >
> Tambah
Tambah </Button>
</Button> </Tooltip>
</Tooltip> )}
)
}
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Stack gap={"md"}> <Stack gap={"md"}>
@@ -323,18 +325,38 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
<Table.Td>{v.name}</Table.Td> <Table.Td>{v.name}</Table.Td>
<Table.Td> <Table.Td>
<Group> <Group>
<Tooltip label={permissions.includes("setting.kategori_pengaduan.edit") ? "Edit Kategori" : "Edit Kategori - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes(
"setting.kategori_pengaduan.edit",
)
? "Edit Kategori"
: "Edit Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }} style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })} onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes("setting.kategori_pengaduan.edit") || v.id == "lainnya"} disabled={
!permissions.includes(
"setting.kategori_pengaduan.edit",
) || v.id == "lainnya"
}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={permissions.includes("setting.kategori_pengaduan.delete") ? "Hapus Kategori" : "Hapus Kategori - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes(
"setting.kategori_pengaduan.delete",
)
? "Hapus Kategori"
: "Hapus Kategori - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
@@ -344,7 +366,11 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes("setting.kategori_pengaduan.delete") || v.id == "lainnya"} disabled={
!permissions.includes(
"setting.kategori_pengaduan.delete",
) || v.id == "lainnya"
}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

@@ -3,92 +3,100 @@ import { Flex, Image, Loader, Modal } from "@mantine/core";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function ModalFile({ open, onClose, folder, fileName }: { open: boolean, onClose: () => void, folder: string, fileName: string }) { export default function ModalFile({
const [viewFile, setViewFile] = useState<string>(""); open,
const [loading, setLoading] = useState<boolean>(false); onClose,
const [typeFile, setTypeFile] = useState<string>(""); folder,
const [error, setError] = useState<boolean>(false); fileName,
}: {
open: boolean;
onClose: () => void;
folder: string;
fileName: string;
}) {
const [viewFile, setViewFile] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [typeFile, setTypeFile] = useState<string>("");
const [error, setError] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
if (open && fileName) { if (open && fileName) {
loadImage(); loadImage();
}
}, [open, fileName]);
const loadImage = async () => {
try {
setViewFile("");
setLoading(true);
// detect type of file
const { ext, type } = detectFileType(fileName);
setTypeFile(type || "");
// load file
const urlApi =
"/api/pengaduan/image?folder=" + folder + "&fileName=" + fileName;
const res = await fetch(urlApi);
if (!res.ok) {
setError(true);
return notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
} }
}, [open, fileName]); const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewFile(url);
} catch (err) {
setError(true);
notification({
title: "Error",
message: "Failed to load image",
type: "error",
});
} finally {
setLoading(false);
}
};
const loadImage = async () => { useEffect(() => {
try { if (error) {
setViewFile(""); onClose();
setLoading(true); }
}, [error]);
// detect type of file return (
const { ext, type } = detectFileType(fileName); <Modal
setTypeFile(type || ""); opened={open}
onClose={onClose}
// load file overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
const urlApi = '/api/pengaduan/image?folder=' + folder + '&fileName=' + fileName; size="xl"
const res = await fetch(urlApi); withCloseButton
if (!res.ok) { removeScrollProps={{ allowPinchZoom: true }}
setError(true); title="File"
return notification({ >
title: "Error", {loading && (
message: "Failed to load image", <Flex justify="center" align="center" h={200}>
type: "error", <Loader />
}); </Flex>
} )}
const blob = await res.blob(); {viewFile && (
const url = URL.createObjectURL(blob); <>
{typeFile == "pdf" ? (
setViewFile(url); <embed
} catch (err) { src={viewFile}
setError(true); type="application/pdf"
notification({ width="100%"
title: "Error", height="950"
message: "Failed to load image", />
type: "error", ) : (
}); <Image radius="md" h={300} fit="contain" src={viewFile} />
} finally { )}
setLoading(false); </>
} )}
}; </Modal>
);
useEffect(() => {
if (error) {
onClose();
}
}, [error]);
return (
<Modal
opened={open}
onClose={onClose}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="xl"
withCloseButton
removeScrollProps={{ allowPinchZoom: true }}
title="File"
>
{loading && (
<Flex justify="center" align="center" h={200}>
<Loader />
</Flex>
)}
{viewFile && (
<>
{typeFile == "pdf" ? (
<embed src={viewFile} type="application/pdf" width="100%" height="950" />
) : (
<Image
radius="md"
h={300}
fit="contain"
src={viewFile}
/>
)}
</>
)}
</Modal>
);
} }

View File

@@ -18,122 +18,129 @@ import SKTidakMampu from "./surat/SKTidakMampu";
import SKUsaha from "./surat/SKUsaha"; import SKUsaha from "./surat/SKUsaha";
import SKYatim from "./surat/SKYatimPiatu"; import SKYatim from "./surat/SKYatimPiatu";
export default function ModalSurat({ open, onClose, surat }: { open: boolean, onClose: () => void, surat: string }) { export default function ModalSurat({
const A4Style = { open,
width: "210mm", onClose,
height: "297mm", surat,
padding: "20mm", }: {
background: "#fff", open: boolean;
color: "#000", onClose: () => void;
fontSize: "14px", surat: string;
fontFamily: "Times New Roman", }) {
}; const A4Style = {
const hiddenRef = useRef<any>(null); width: "210mm",
const { data, mutate, isLoading } = useSWR("surat", () => height: "297mm",
apiFetch.api.surat.detail.get({ padding: "20mm",
query: { background: "#fff",
id: surat, color: "#000",
}, fontSize: "14px",
}), fontFamily: "Times New Roman",
); };
const hiddenRef = useRef<any>(null);
const { data, mutate, isLoading } = useSWR("surat", () =>
apiFetch.api.surat.detail.get({
query: {
id: surat,
},
}),
);
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
const downloadPDF = async () => {
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const downloadPDF = async () => { const imgData = canvas.toDataURL("image/jpeg", 1.0);
const element = hiddenRef.current;
const canvas = await html2canvas(element, {
scale: 2,
useCORS: true,
allowTaint: true,
width: element.offsetWidth,
height: element.offsetHeight,
});
const imgData = canvas.toDataURL("image/jpeg", 1.0); const pdf = new jsPDF("p", "mm", "a4");
const pageWidth = 210; // A4 width mm
const pageHeight = 297; // A4 height mm
const pdf = new jsPDF("p", "mm", "a4"); const imgWidth = pageWidth;
const pageWidth = 210; // A4 width mm const imgHeight = (canvas.height * pageWidth) / canvas.width;
const pageHeight = 297; // A4 height mm
const imgWidth = pageWidth; pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
const imgHeight = (canvas.height * pageWidth) / canvas.width;
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight); pdf.save(`${data?.data?.surat?.nameCategory}.pdf`);
};
pdf.save(`${data?.data?.surat?.nameCategory}.pdf`); return (
}; <>
<Modal
opened={open}
onClose={() => onClose()}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
},
title: {
width: "100%",
},
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>Preview Surat</div>
return ( <Flex gap={8}>
<> <ActionIcon size={32} variant="default">
<Modal <IconDownload size={20} onClick={downloadPDF} />
opened={open} </ActionIcon>
onClose={() => onClose()}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size="auto"
withCloseButton={false}
removeScrollProps={{ allowPinchZoom: true }}
styles={{
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px 16px",
},
title: {
width: "100%",
}
}}
title={
<Flex justify="space-between" align="center" w="100%">
<div style={{ fontSize: 18, fontWeight: 600 }}>
Preview Surat
</div>
<Flex gap={8}> <ActionIcon size={32} variant="default" onClick={onClose}>
<ActionIcon size={32} variant="default"> <IconX size={20} />
<IconDownload size={20} onClick={downloadPDF} /> </ActionIcon>
</ActionIcon> </Flex>
</Flex>
<ActionIcon size={32} variant="default" onClick={onClose}> }
<IconX size={20} /> >
</ActionIcon> <div ref={hiddenRef} style={A4Style}>
</Flex> {data && data.data ? (
</Flex> data.data.surat.idCategory == "skusaha" ? (
} <SKUsaha data={data.data} />
> ) : data.data.surat.idCategory == "skkelahiran" ? (
<div ref={hiddenRef} style={A4Style}> <SKKelahiran data={data.data} />
{ ) : data.data.surat.idCategory == "skkelakuanbaik" ? (
data && data.data <SKKelakuanBaik data={data.data} />
? data.data.surat.idCategory == "skusaha" ) : data.data.surat.idCategory == "skpenghasilan" ? (
? <SKUsaha data={data.data} /> <SKPenghasilan data={data.data} />
: data.data.surat.idCategory == "skkelahiran" ) : data.data.surat.idCategory == "sktidakmampu" ? (
? <SKKelahiran data={data.data} /> <SKTidakMampu data={data.data} />
: data.data.surat.idCategory == "skkelakuanbaik" ) : data.data.surat.idCategory == "skyatimpiatu" ? (
? <SKKelakuanBaik data={data.data} /> <SKYatim data={data.data} />
: data.data.surat.idCategory == "skpenghasilan" ) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
? <SKPenghasilan data={data.data} /> <SKDomisiliOrganisasi data={data.data} />
: data.data.surat.idCategory == "sktidakmampu" ) : data.data.surat.idCategory == "skbedabiodata" ? (
? <SKTidakMampu data={data.data} /> <SKBedaBiodataDiri data={data.data} />
: data.data.surat.idCategory == "skyatimpiatu" ) : data.data.surat.idCategory == "sktempatusaha" ? (
? <SKYatim data={data.data} /> <SKTempatUsaha data={data.data} />
: data.data.surat.idCategory == "skdomisiliorganisasi" ) : data.data.surat.idCategory == "skbelumkawin" ? (
? <SKDomisiliOrganisasi data={data.data} /> <SKBelumKawin data={data.data} />
: data.data.surat.idCategory == "skbedabiodata" ) : data.data.surat.idCategory == "skkematian" ? (
? <SKBedaBiodataDiri data={data.data} /> <SKKematian data={data.data} />
: data.data.surat.idCategory == "sktempatusaha" ) : (
? <SKTempatUsaha data={data.data} /> <></>
: data.data.surat.idCategory == "skbelumkawin" )
? <SKBelumKawin data={data.data} /> ) : (
: data.data.surat.idCategory == "skkematian" <></>
? <SKKematian data={data.data} /> )}
: <></> </div>
: <></> </Modal>
} </>
</div> );
</Modal> }
</>
)
}

View File

@@ -3,65 +3,66 @@ import { Anchor, Flex, Stack, Text } from "@mantine/core";
import { useState } from "react"; import { useState } from "react";
interface Node { interface Node {
label: string; label: string;
children: any; children: any;
actions: string[]; actions: string[];
} }
function RenderNode({ node }: { node: Node }) { function RenderNode({ node }: { node: Node }) {
const sub = Object.values(node.children || {}); const sub = Object.values(node.children || {});
return ( return (
<Stack pl="md" gap={6}> <Stack pl="md" gap={6}>
{/* Title */} {/* Title */}
<Text size="sm">- {node.label}</Text> <Text size="sm">- {node.label}</Text>
{/* Children */} {/* Children */}
{sub.map((child: any, i) => ( {sub.map((child: any, i) => (
<RenderNode key={i} node={child} /> <RenderNode key={i} node={child} />
))} ))}
</Stack> </Stack>
); );
} }
function RenderNode2({ node }: { node: Node }) { function RenderNode2({ node }: { node: Node }) {
const sub = Object.values(node.children || {}); const sub = Object.values(node.children || {});
return ( return (
<Flex direction={"row"} wrap={'wrap'} gap={6}> <Flex direction={"row"} wrap={"wrap"} gap={6}>
{/* Title */} {/* Title */}
<Text size="sm">{node.label},</Text> <Text size="sm">{node.label},</Text>
{/* Children */} {/* Children */}
{sub.map((child: any, i) => ( {sub.map((child: any, i) => (
<RenderNode2 key={i} node={child} /> <RenderNode2 key={i} node={child} />
))} ))}
</Flex> </Flex>
); );
} }
export default function PermissionRole({ permissions }: { permissions: string[] }) { export default function PermissionRole({
const [showAll, setShowAll] = useState(false); permissions,
if (!permissions?.length) return <Text c="dimmed">-</Text>; }: {
permissions: string[];
}) {
const [showAll, setShowAll] = useState(false);
if (!permissions?.length) return <Text c="dimmed">-</Text>;
const groups = groupPermissions(permissions); const groups = groupPermissions(permissions);
const rootNodes = Object.values(groups); const rootNodes = Object.values(groups);
return ( return (
<Stack gap="sm"> <Stack gap="sm">
{ {showAll
showAll ? ? rootNodes.map((node: any, idx) => (
rootNodes.map((node: any, idx) => ( <RenderNode key={idx} node={node} />
<RenderNode key={idx} node={node} /> ))
)) : rootNodes
: .slice(0, 2)
rootNodes.slice(0, 2).map((node: any, idx) => ( .map((node: any, idx) => <RenderNode2 key={idx} node={node} />)}
<RenderNode2 key={idx} node={node} /> <Anchor size="xs" onClick={() => setShowAll(!showAll)}>
)) {showAll ? "View less" : "View more"}
} </Anchor>
<Anchor size="xs" onClick={() => setShowAll(!showAll)} > </Stack>
{showAll ? "View less" : "View more"} );
</Anchor>
</Stack>
);
} }

View File

@@ -1,177 +1,183 @@
import permissionConfig from "@/lib/listPermission.json"; import permissionConfig from "@/lib/listPermission.json";
import { ActionIcon, Checkbox, Collapse, Group, Stack, Text } from "@mantine/core"; import {
ActionIcon,
Checkbox,
Collapse,
Group,
Stack,
Text,
} from "@mantine/core";
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react"; import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
import { useState } from "react"; import { useState } from "react";
interface Node { interface Node {
label: string; label: string;
key: string; key: string;
children?: Node[]; children?: Node[];
} }
export default function PermissionTree({ export default function PermissionTree({
selected, selected,
onChange, onChange,
}: { }: {
selected: string[]; selected: string[];
onChange: (val: string[]) => void; onChange: (val: string[]) => void;
}) { }) {
// Ambil semua child dari node // Ambil semua child dari node
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({}); const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
function toggleNode(label: string) { function toggleNode(label: string) {
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] })); setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
} }
function getAllChildKeys(node: Node): string[] { function getAllChildKeys(node: Node): string[] {
let result: string[] = []; let result: string[] = [];
if (node.children) { if (node.children) {
node.children.forEach((c) => { node.children.forEach((c) => {
result.push(c.key); result.push(c.key);
result = [...result, ...getAllChildKeys(c)]; result = [...result, ...getAllChildKeys(c)];
}); });
}
return result;
}
// Dapatkan parentKey, jika ada
function getParentKey(key: string) {
const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
// Update parent ke atas secara rekursif
function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const allChildKeys = findAllChildKeysFromKey(parentKey);
const selectedChild = allChildKeys.filter((c) => next.includes(c));
if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
next = next.filter((x) => x !== parentKey);
} else if (selectedChild.length === allChildKeys.length) {
// Semua child check → parent check
if (!next.includes(parentKey)) {
next.push(parentKey);
} }
return result; } else {
} // Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
// Dapatkan parentKey, jika ada next.push(parentKey);
function getParentKey(key: string) {
const split = key.split(".");
if (split.length <= 1) return null;
split.pop();
return split.join(".");
}
// Update parent ke atas secara rekursif
function updateParent(next: string[], parentKey: string | null): string[] {
if (!parentKey) return next;
const allChildKeys = findAllChildKeysFromKey(parentKey);
const selectedChild = allChildKeys.filter((c) => next.includes(c));
if (selectedChild.length === 0) {
// Semua child uncheck → parent uncheck
next = next.filter((x) => x !== parentKey);
} else if (selectedChild.length === allChildKeys.length) {
// Semua child check → parent check
if (!next.includes(parentKey)) {
next.push(parentKey);
}
} else {
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
if (!next.includes(parentKey)) {
next.push(parentKey);
}
} }
}
// Rekursif naik ke atas // Rekursif naik ke atas
return updateParent(next, getParentKey(parentKey)); return updateParent(next, getParentKey(parentKey));
} }
// dapatkan child dari string key // dapatkan child dari string key
function findAllChildKeysFromKey(parentKey: string) { function findAllChildKeysFromKey(parentKey: string) {
const list: string[] = []; const list: string[] = [];
function traverse(nodes: Node[]) { function traverse(nodes: Node[]) {
nodes.forEach((n) => { nodes.forEach((n) => {
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) { if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
list.push(n.key); list.push(n.key);
} }
if (n.children) traverse(n.children); if (n.children) traverse(n.children);
}); });
} }
traverse(permissionConfig.menus); traverse(permissionConfig.menus);
return list; return list;
} }
const RenderMenu = ({ menu }: { menu: Node }) => { const RenderMenu = ({ menu }: { menu: Node }) => {
const hasChild = menu.children && menu.children.length > 0; const hasChild = menu.children && menu.children.length > 0;
const open = openNodes[menu.label] ?? false; const open = openNodes[menu.label] ?? false;
const childKeys = getAllChildKeys(menu); const childKeys = getAllChildKeys(menu);
const isChecked = selected.includes(menu.key); const isChecked = selected.includes(menu.key);
const isIndeterminate = const isIndeterminate =
!isChecked && !isChecked &&
selected.some( selected.some(
(x) => (x) => typeof x === "string" && x.startsWith(menu.key + "."),
typeof x === "string" &&
x.startsWith(menu.key + ".")
);
function handleCheck() {
let next = [...selected];
if (childKeys.length > 0) {
// klik parent
if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<Stack gap={4}>
<Group gap="xs">
{menu.children && menu.children.length > 0 ? (
<ActionIcon
variant="subtle"
onClick={() => toggleNode(menu.label)}
>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon>
) : (
<div style={{ width: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack>
); );
};
return ( function handleCheck() {
<Stack> let next = [...selected];
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus.filter((menu: Node) => !menu.key.startsWith("api") && !menu.key.startsWith("credential")).map((menu: Node) => ( if (childKeys.length > 0) {
<RenderMenu key={menu.key} menu={menu} /> // klik parent
))} if (!isChecked) {
next = [...new Set([...next, menu.key, ...childKeys])];
} else {
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
return;
}
// klik child
if (isChecked) {
next = next.filter((x) => x !== menu.key);
} else {
next.push(menu.key);
}
next = updateParent(next, getParentKey(menu.key));
onChange(next);
}
return (
<Stack gap={4}>
<Group gap="xs">
{menu.children && menu.children.length > 0 ? (
<ActionIcon variant="subtle" onClick={() => toggleNode(menu.label)}>
{openNodes[menu.label] ? (
<IconChevronDown size={16} />
) : (
<IconChevronRight size={16} />
)}
</ActionIcon>
) : (
<div style={{ width: 28 }} />
)}
<Checkbox
label={menu.label}
checked={isChecked}
indeterminate={isIndeterminate}
onChange={handleCheck}
/>
</Group>
{menu.children && (
<Collapse in={open}>
<Stack gap={4} pl="md">
{menu.children.map((child) => (
<RenderMenu key={child.key} menu={child} />
))}
</Stack>
</Collapse>
)}
</Stack> </Stack>
); );
};
return (
<Stack>
<Text size="sm">Hak Akses</Text>
{permissionConfig.menus
.filter(
(menu: Node) =>
!menu.key.startsWith("api") && !menu.key.startsWith("credential"),
)
.map((menu: Node) => (
<RenderMenu key={menu.key} menu={menu} />
))}
</Stack>
);
} }

View File

@@ -13,7 +13,11 @@ import type { JsonValue } from "generated/prisma/runtime/library";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function ProfileUser({ permissions }: { permissions: JsonValue[] }) { export default function ProfileUser({
permissions,
}: {
permissions: JsonValue[];
}) {
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [openedPassword, setOpenedPassword] = useState(false); const [openedPassword, setOpenedPassword] = useState(false);
const [pwdBaru, setPwdBaru] = useState(""); const [pwdBaru, setPwdBaru] = useState("");
@@ -127,21 +131,17 @@ export default function ProfileUser({ permissions }: { permissions: JsonValue[]
Profile Pengguna Profile Pengguna
</Title> </Title>
<Group gap="md"> <Group gap="md">
{ {permissions.includes("setting.profile.edit") && (
permissions.includes("setting.profile.edit") && ( <Button variant="light" onClick={() => setOpened(true)}>
<Button variant="light" onClick={() => setOpened(true)}> Edit
Edit </Button>
</Button> )}
)
}
{ {permissions.includes("setting.profile.password") && (
permissions.includes("setting.profile.password") && ( <Button variant="light" onClick={() => setOpenedPassword(true)}>
<Button variant="light" onClick={() => setOpenedPassword(true)}> Ubah Password
Ubah Password </Button>
</Button> )}
)
}
</Group> </Group>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />

View File

@@ -1,17 +1,17 @@
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import {
ActionIcon, ActionIcon,
Button, Button,
Divider, Divider,
Flex, Flex,
Group, Group,
Input, Input,
Modal, Modal,
Stack, Stack,
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, IconPlus, IconTrash } from "@tabler/icons-react"; import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
@@ -24,404 +24,449 @@ import PermissionRole from "./PermissionRole";
import PermissionTree from "./PermissionTree"; import PermissionTree from "./PermissionTree";
interface MenuNode { interface MenuNode {
key: string; key: string;
label: string; label: string;
default: boolean; default: boolean;
children?: MenuNode[]; children?: MenuNode[];
} }
export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) { export default function UserRoleSetting({
const [btnDisable, setBtnDisable] = useState(true); permissions,
const [btnLoading, setBtnLoading] = useState(false); }: {
const [opened, { open, close }] = useDisclosure(false); permissions: JsonValue[];
const [openedDelete, { open: openDelete, close: closeDelete }] = }) {
useDisclosure(false); const [btnDisable, setBtnDisable] = useState(true);
const [dataDelete, setDataDelete] = useState(""); const [btnLoading, setBtnLoading] = useState(false);
const { const [opened, { open, close }] = useDisclosure(false);
data: dataRole, const [openedDelete, { open: openDelete, close: closeDelete }] =
mutate: mutateRole, useDisclosure(false);
isLoading: isLoadingRole, const [dataDelete, setDataDelete] = useState("");
} = useSWR("user-role", () => apiFetch.api.user.role.get()); const {
const [openedTambah, { open: openTambah, close: closeTambah }] = data: dataRole,
useDisclosure(false); mutate: mutateRole,
const { data, mutate, isLoading } = useSWR("role-list", () => isLoading: isLoadingRole,
apiFetch.api.user.role.get(), } = useSWR("user-role", () => apiFetch.api.user.role.get());
); const [openedTambah, { open: openTambah, close: closeTambah }] =
const list = data?.data || []; useDisclosure(false);
const listRole = dataRole?.data || []; const { data, mutate, isLoading } = useSWR("role-list", () =>
const [dataEdit, setDataEdit] = useState({ apiFetch.api.user.role.get(),
id: "", );
name: "", const list = data?.data || [];
permissions: [], const listRole = dataRole?.data || [];
}); const [dataEdit, setDataEdit] = useState({
const [dataTambah, setDataTambah] = useState({ id: "",
name: "", name: "",
permissions: [], permissions: [],
}); });
const [error, setError] = useState({ const [dataTambah, setDataTambah] = useState({
name: false, name: "",
permissions: false, permissions: [],
}); });
const [error, setError] = useState({
name: false,
permissions: false,
});
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, []); }, []);
async function handleCreate() { async function handleCreate() {
try { try {
setBtnLoading(true); setBtnLoading(true);
const res = await apiFetch.api.user["role-create"].post(dataTambah as any); const res = await apiFetch.api.user["role-create"].post(
if (res.status === 200) { dataTambah as any,
mutate(); );
closeTambah(); if (res.status === 200) {
setDataTambah({ mutate();
name: "", closeTambah();
permissions: [], setDataTambah({
}); name: "",
notification({ permissions: [],
title: "Success", });
message: "Your role have been saved", notification({
type: "success", title: "Success",
}); message: "Your role have been saved",
} else { type: "success",
notification({ });
title: "Error",
message: "Failed to create role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to create role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
async function handleEdit() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
async function handleDelete() {
try {
setBtnLoading(true);
const res = await apiFetch.api.user["role-delete"].post({ id: dataDelete });
if (res.status === 200) {
mutate();
closeDelete();
notification({
title: "Success",
message: "Your role have been deleted",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
}
} catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to delete role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
function chooseEdit({ data }: { data: { id: string; name: string; permissions: []; }; }) {
setDataEdit({
id: data.id, name: data.name, permissions: data.permissions ? data.permissions : []
});
open();
}
function onValidation({ kat, value, aksi, }: { kat: "name" | "permission"; value: string | null; aksi: "edit" | "tambah"; }) {
if (value == null || value.length < 1) {
setBtnDisable(true);
setError({ ...error, [kat]: true });
} else { } else {
setBtnDisable(false); notification({
setError({ ...error, [kat]: false }); title: "Error",
message: "Failed to create role",
type: "error",
});
} }
} catch (error) {
if (aksi === "edit") { console.error(error);
setDataEdit({ ...dataEdit, [kat]: value }); notification({
} else { title: "Error",
setDataTambah({ ...dataTambah, [kat]: value }); message: "Failed to create role",
} type: "error",
}
function buildOrderList(menus: MenuNode[]): string[] {
const list: string[] = [];
const traverse = (nodes: MenuNode[]) => {
nodes.forEach((node) => {
list.push(node.key);
if (node.children) traverse(node.children);
});
};
traverse(menus);
return list;
}
function sortByJsonOrder(arrayData: string[]): string[] {
const orderList = buildOrderList(listMenu.menus);
return arrayData.sort((a, b) => {
return orderList.indexOf(a) - orderList.indexOf(b);
}); });
} } finally {
setBtnLoading(false);
}
}
useShallowEffect(() => { async function handleEdit() {
if (dataEdit.name.length > 0) { try {
setBtnDisable(false); setBtnLoading(true);
const res = await apiFetch.api.user["role-update"].post(dataEdit as any);
if (res.status === 200) {
mutate();
close();
notification({
title: "Success",
message: "Your role have been saved",
type: "success",
});
} else {
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
} }
}, [dataEdit.id]); } catch (error) {
console.error(error);
notification({
title: "Error",
message: "Failed to edit role",
type: "error",
});
} finally {
setBtnLoading(false);
}
}
return ( async function handleDelete() {
<> try {
{/* Modal Edit */} setBtnLoading(true);
<Modal const res = await apiFetch.api.user["role-delete"].post({
opened={opened} id: dataDelete,
onClose={close} });
title={"Edit"} if (res.status === 200) {
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} mutate();
size={"lg"} closeDelete();
> notification({
<Stack gap="ld"> title: "Success",
<Input.Wrapper label="Nama Role"> message: "Your role have been deleted",
<Input type: "success",
value={dataEdit.name} });
onChange={(e) => } else {
onValidation({ notification({
kat: "name", title: "Error",
value: e.target.value, message: "Failed to delete role",
aksi: "edit", type: "error",
}) });
} }
/> } catch (error) {
</Input.Wrapper> console.error(error);
<PermissionTree notification({
selected={dataEdit.permissions} title: "Error",
onChange={(permissions) => { message: "Failed to delete role",
setDataEdit({ ...dataEdit, permissions: sortByJsonOrder(permissions) as never[] }); type: "error",
}} });
/> } finally {
<Group justify="center" grow> setBtnLoading(false);
<Button variant="light" onClick={close}> }
Batal }
</Button>
<Button
variant="filled"
onClick={handleEdit}
disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */} function chooseEdit({
<Modal data,
opened={openedTambah} }: {
onClose={closeTambah} data: { id: string; name: string; permissions: [] };
title={"Tambah"} }) {
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} setDataEdit({
size={"lg"} id: data.id,
> name: data.name,
<Stack gap="ld"> permissions: data.permissions ? data.permissions : [],
<Input.Wrapper });
label="Nama Role" open();
description="" }
error={error.name ? "Field is required" : ""}
>
<Input
value={dataTambah.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({ ...dataTambah, permissions: sortByJsonOrder(permissions) as never[] });
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button
variant="filled"
onClick={handleCreate}
disabled={
btnDisable ||
dataTambah.name.length < 1 ||
dataTambah.permissions.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Delete */} function onValidation({
<Modal kat,
opened={openedDelete} value,
onClose={closeDelete} aksi,
title={"Delete"} }: {
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }} kat: "name" | "permission";
> value: string | null;
<Stack gap="md"> aksi: "edit" | "tambah";
<Text size="md" color="gray.6"> }) {
Apakah anda yakin ingin menghapus role ini? if (value == null || value.length < 1) {
</Text> setBtnDisable(true);
<Group justify="center" grow> setError({ ...error, [kat]: true });
<Button variant="light" onClick={closeDelete}> } else {
Batal setBtnDisable(false);
</Button> setError({ ...error, [kat]: false });
<Button }
variant="filled"
color="red"
onClick={handleDelete}
loading={btnLoading}
>
Hapus
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}> if (aksi === "edit") {
<Flex align="center" justify="space-between"> setDataEdit({ ...dataEdit, [kat]: value });
<Title order={4} c="gray.2"> } else {
Daftar Role setDataTambah({ ...dataTambah, [kat]: value });
</Title> }
{ }
permissions.includes('setting.user_role.tambah') && (
<Tooltip label="Tambah Role"> function buildOrderList(menus: MenuNode[]): string[] {
<Button const list: string[] = [];
variant="light"
leftSection={<IconPlus size={20} />} const traverse = (nodes: MenuNode[]) => {
onClick={openTambah} nodes.forEach((node) => {
list.push(node.key);
if (node.children) traverse(node.children);
});
};
traverse(menus);
return list;
}
function sortByJsonOrder(arrayData: string[]): string[] {
const orderList = buildOrderList(listMenu.menus);
return arrayData.sort((a, b) => {
return orderList.indexOf(a) - orderList.indexOf(b);
});
}
useShallowEffect(() => {
if (dataEdit.name.length > 0) {
setBtnDisable(false);
}
}, [dataEdit.id]);
return (
<>
{/* Modal Edit */}
<Modal
opened={opened}
onClose={close}
title={"Edit"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper label="Nama Role">
<Input
value={dataEdit.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "edit",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataEdit.permissions}
onChange={(permissions) => {
setDataEdit({
...dataEdit,
permissions: sortByJsonOrder(permissions) as never[],
});
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={close}>
Batal
</Button>
<Button
variant="filled"
onClick={handleEdit}
disabled={
btnDisable ||
dataEdit.name.length < 1 ||
dataEdit.permissions?.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Tambah */}
<Modal
opened={openedTambah}
onClose={closeTambah}
title={"Tambah"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
size={"lg"}
>
<Stack gap="ld">
<Input.Wrapper
label="Nama Role"
description=""
error={error.name ? "Field is required" : ""}
>
<Input
value={dataTambah.name}
onChange={(e) =>
onValidation({
kat: "name",
value: e.target.value,
aksi: "tambah",
})
}
/>
</Input.Wrapper>
<PermissionTree
selected={dataTambah.permissions}
onChange={(permissions) => {
setDataTambah({
...dataTambah,
permissions: sortByJsonOrder(permissions) as never[],
});
}}
/>
<Group justify="center" grow>
<Button variant="light" onClick={closeTambah}>
Batal
</Button>
<Button
variant="filled"
onClick={handleCreate}
disabled={
btnDisable ||
dataTambah.name.length < 1 ||
dataTambah.permissions.length < 1
}
loading={btnLoading}
>
Simpan
</Button>
</Group>
</Stack>
</Modal>
{/* Modal Delete */}
<Modal
opened={openedDelete}
onClose={closeDelete}
title={"Delete"}
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
>
<Stack gap="md">
<Text size="md" color="gray.6">
Apakah anda yakin ingin menghapus role ini?
</Text>
<Group justify="center" grow>
<Button variant="light" onClick={closeDelete}>
Batal
</Button>
<Button
variant="filled"
color="red"
onClick={handleDelete}
loading={btnLoading}
>
Hapus
</Button>
</Group>
</Stack>
</Modal>
<Stack gap={"md"}>
<Flex align="center" justify="space-between">
<Title order={4} c="gray.2">
Daftar Role
</Title>
{permissions.includes("setting.user_role.tambah") && (
<Tooltip label="Tambah Role">
<Button
variant="light"
leftSection={<IconPlus size={20} />}
onClick={openTambah}
>
Tambah
</Button>
</Tooltip>
)}
</Flex>
<Divider my={0} />
<Stack gap={"md"}>
<Table highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Role</Table.Th>
<Table.Th>Permission</Table.Th>
<Table.Th>Aksi</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{list.length > 0 ? (
list?.map((v: any) => (
<Table.Tr key={v.id}>
<Table.Td w={"150"}>{v.name}</Table.Td>
<Table.Td>
<PermissionRole permissions={v.permissions} />
</Table.Td>
<Table.Td w={"100"}>
<Group>
<Tooltip
label={
permissions.includes("setting.user_role.edit")
? "Edit Role"
: "Edit Role - Anda tidak memiliki akses"
}
> >
Tambah <ActionIcon
</Button> variant="light"
</Tooltip> size="sm"
) style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
} onClick={() => chooseEdit({ data: v })}
</Flex> disabled={
<Divider my={0} /> !permissions.includes("setting.user_role.edit") ||
<Stack gap={"md"}> v.id == "developer"
<Table highlightOnHover> }
<Table.Thead> >
<Table.Tr> <IconEdit size={20} />
<Table.Th>Role</Table.Th> </ActionIcon>
<Table.Th>Permission</Table.Th> </Tooltip>
<Table.Th>Aksi</Table.Th> <Tooltip
</Table.Tr> label={
</Table.Thead> permissions.includes("setting.user_role.delete")
<Table.Tbody> ? "Delete Role"
{list.length > 0 ? ( : "Delete Role - Anda tidak memiliki akses"
list?.map((v: any) => ( }
<Table.Tr key={v.id}> >
<Table.Td w={"150"}>{v.name}</Table.Td> <ActionIcon
<Table.Td> variant="light"
<PermissionRole permissions={v.permissions} /> size="sm"
</Table.Td> color="red"
<Table.Td w={"100"}> style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
<Group> onClick={() => {
<Tooltip label={permissions.includes('setting.user_role.edit') ? "Edit Role" : "Edit Role - Anda tidak memiliki akses"}> setDataDelete(v.id);
<ActionIcon openDelete();
variant="light" }}
size="sm" disabled={
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }} !permissions.includes(
onClick={() => chooseEdit({ data: v })} "setting.user_role.delete",
disabled={!permissions.includes('setting.user_role.edit') || v.id == "developer"} ) || v.id == "developer"
> }
<IconEdit size={20} /> >
</ActionIcon> <IconTrash size={20} />
</Tooltip> </ActionIcon>
<Tooltip label={permissions.includes('setting.user_role.delete') ? "Delete Role" : "Delete Role - Anda tidak memiliki akses"}> </Tooltip>
<ActionIcon </Group>
variant="light" </Table.Td>
size="sm" </Table.Tr>
color="red" ))
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }} ) : (
onClick={() => { <Table.Tr>
setDataDelete(v.id); <Table.Td colSpan={5} align="center">
openDelete(); Data Role Tidak Ditemukan
}} </Table.Td>
disabled={!permissions.includes('setting.user_role.delete') || v.id == "developer"} </Table.Tr>
> )}
<IconTrash size={20} /> </Table.Tbody>
</ActionIcon> </Table>
</Tooltip> </Stack>
</Group> </Stack>
</Table.Td> </>
</Table.Tr> );
))
) : (
<Table.Tr>
<Table.Td colSpan={5} align="center">
Data Role Tidak Ditemukan
</Table.Td>
</Table.Tr>
)}
</Table.Tbody>
</Table>
</Stack>
</Stack>
</>
);
} }

View File

@@ -21,7 +21,11 @@ import { useState } from "react";
import useSWR from "swr"; import useSWR from "swr";
import notification from "./notificationGlobal"; import notification from "./notificationGlobal";
export default function UserSetting({ permissions }: { permissions: JsonValue[] }) { export default function UserSetting({
permissions,
}: {
permissions: JsonValue[];
}) {
const [btnDisable, setBtnDisable] = useState(true); const [btnDisable, setBtnDisable] = useState(true);
const [btnLoading, setBtnLoading] = useState(false); const [btnLoading, setBtnLoading] = useState(false);
const [opened, { open, close }] = useDisclosure(false); const [opened, { open, close }] = useDisclosure(false);
@@ -437,20 +441,17 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
<Title order={4} c="gray.2"> <Title order={4} c="gray.2">
Daftar User Daftar User
</Title> </Title>
{ {permissions.includes("setting.user.tambah") && (
permissions.includes('setting.user.tambah') && ( <Tooltip label="Tambah User">
<Tooltip label="Tambah User"> <Button
<Button variant="light"
variant="light" leftSection={<IconPlus size={20} />}
leftSection={<IconPlus size={20} />} onClick={openTambah}
onClick={openTambah} >
> Tambah
Tambah </Button>
</Button> </Tooltip>
</Tooltip> )}
)
}
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
<Stack gap={"md"}> <Stack gap={"md"}>
@@ -474,18 +475,33 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
<Table.Td>{v.nameRole}</Table.Td> <Table.Td>{v.nameRole}</Table.Td>
<Table.Td> <Table.Td>
<Group> <Group>
<Tooltip label={permissions.includes('setting.user.edit') ? "Edit User" : "Edit User - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes("setting.user.edit")
? "Edit User"
: "Edit User - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }} style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
onClick={() => chooseEdit({ data: v })} onClick={() => chooseEdit({ data: v })}
disabled={!permissions.includes('setting.user.edit') || v.roleId == "developer"} disabled={
!permissions.includes("setting.user.edit") ||
v.roleId == "developer"
}
> >
<IconEdit size={20} /> <IconEdit size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label={permissions.includes('setting.user.delete') ? "Delete User" : "Delete User - Anda tidak memiliki akses"}> <Tooltip
label={
permissions.includes("setting.user.delete")
? "Delete User"
: "Delete User - Anda tidak memiliki akses"
}
>
<ActionIcon <ActionIcon
variant="light" variant="light"
size="sm" size="sm"
@@ -495,7 +511,10 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
setDataDelete(v.id); setDataDelete(v.id);
openDelete(); openDelete();
}} }}
disabled={!permissions.includes('setting.user.delete') || v.roleId == "developer"} disabled={
!permissions.includes("setting.user.delete") ||
v.roleId == "developer"
}
> >
<IconTrash size={20} /> <IconTrash size={20} />
</ActionIcon> </ActionIcon>

View File

@@ -3,157 +3,242 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKBedaBiodataDiri({ data }: { data: any }) { export default function SKBedaBiodataDiri({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(); const [viewImg, setViewImg] = useState<string>();
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.25" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "15px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center" }}>
<b><u>SURAT KETERANGAN BEDA BIODATA DIRI</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan + " " + data.setting.desaNama}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "15px" }}>
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang bersangkutan:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>, meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa dokumen, sebagai berikut:
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>1. Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td><td style={{ width: "10px" }}>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>3. Nama Orang Tua</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama orang tua")}</td></tr>
<tr><td>Tertulis pada dokumen A</td><td>:</td><td>{getValue("tertulis pada dokumen a")}</td></tr>
<tr><td>Tertulis pada dokumen B</td><td>:</td><td>{getValue("tertulis pada dokumen b")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena <b>kesalahan penulisan/pencatatan administratif</b>, namun yang bersangkutan adalah <b>orang yang sama</b>.
<br />
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "15px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "0px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.25" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "15px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
<br />
Kode Pos: {data.setting.desaPos}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center" }}>
<b>
<u>SURAT KETERANGAN BEDA BIODATA DIRI</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "15px" }}>
Dengan ini menerangkan bahwa berdasarkan keterangan dari yang
bersangkutan:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Bahwa orang tersebut di atas <b>benar merupakan orang yang sama</b>,
meskipun terdapat <b>perbedaan data pribadi (biodata)</b> pada beberapa
dokumen, sebagai berikut:
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>1. Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>2. Tempat/Tanggal Lahir</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>3. Nama Orang Tua</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama orang tua")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen A</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen a")}</td>
</tr>
<tr>
<td>Tertulis pada dokumen B</td>
<td>:</td>
<td>{getValue("tertulis pada dokumen b")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "15px" }}>
Perbedaan tersebut terjadi karena{" "}
<b>kesalahan penulisan/pencatatan administratif</b>, namun yang
bersangkutan adalah <b>orang yang sama</b>.
<br />
Dengan surat keterangan ini dibuat dengan sebenar-benarnya untuk
dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "15px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "0px",
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,108 +3,166 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKBelumKawin({ data }: { data: any }) { export default function SKBelumKawin({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(); const [viewImg, setViewImg] = useState<string>();
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN BELUM KAWIN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan} {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan dari yang bersangkutan dan data administrasi kependudukan yang ada di Desa {data.setting.desaNama},
yang bersangkutan benar sampai saat ini belum pernah menikah, baik secara adat, agama, maupun hukum negara.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br /><br /><br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br /><br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
<br />
Kode Pos: {data.setting.desaPos}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b>
<u>SURAT KETERANGAN BELUM KAWIN</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini {data.setting.perbekelJabatan}{" "}
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
Kabupaten {data.setting.desaKabupaten}, dengan ini menerangkan bahwa:
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan dari yang bersangkutan dan data administrasi
kependudukan yang ada di Desa {data.setting.desaNama}, yang bersangkutan
benar sampai saat ini belum pernah menikah, baik secara adat, agama,
maupun hukum negara.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "40px",
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
<br />
Pemohon
<br />
<br />
<br />
<br />
<br />
<br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,121 +3,163 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKDomisiliOrganisasi({ data }: { data: any }) { export default function SKDomisiliOrganisasi({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN DOMISILI ORGANISASI</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Alamat Kantor</td>
<td>:</td>
<td>{data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama Organisasi</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Jenis Organisasi</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Nomor Telepon</td><td>:</td><td>{getValue("negara")}</td></tr>
<tr><td>Nama Pimpinan</td><td>:</td><td>{getValue("agama")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan {data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan}, Kabupaten {data.setting.desaKabupaten}.
Dan sampai saat ini masih aktif melakukan kegiatan sesuai dengan bidangnya.<br />
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
<br />
Kode Pos: {data.setting.desaPos}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b>
<u>SURAT KETERANGAN DOMISILI ORGANISASI</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Alamat Kantor</td>
<td>:</td>
<td>{data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama Organisasi</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>Jenis Organisasi</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Nomor Telepon</td>
<td>:</td>
<td>{getValue("negara")}</td>
</tr>
<tr>
<td>Nama Pimpinan</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Benar bahwa organisasi tersebut berdomisili di wilayah Desa / Kelurahan{" "}
{data.setting.desaNama}, Kecamatan {data.setting.desaKecamatan},
Kabupaten {data.setting.desaKabupaten}. Dan sampai saat ini masih aktif
melakukan kegiatan sesuai dengan bidangnya.
<br />
Surat keterangan ini dibuat untuk keperluan {getValue("keperluan")}.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "40px",
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,142 +3,244 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKKelahiran({ data }: { data: any }) { export default function SKKelahiran({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.2" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b>PEMERINTAH KABUPATEN/KOTA {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN KELAHIRAN</u></b><br />
Nomor : {data.surat.noSurat}
</div>
{/* PEMBUKA */}
<div>
Yang bertanda tangan di bawah ini, {data.setting.perbekelJabatan}
{` ${data.setting.desaNama}, Kecamatan ${data.setting.desaKecamatan}, Kabupaten/Kota ${data.setting.desaKabupaten}`}
, dengan ini menerangkan bahwa:
</div>
{/* DATA KELAHIRAN ANAK */}
<div style={{ marginTop: "20px" }}>
Telah lahir seorang anak pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "200px" }}>Tanggal Lahir</td><td>:</td><td>{getValue("tanggal lahir anak")}</td></tr>
<tr><td>Pukul</td><td>:</td><td>{getValue("pukul lahir anak")}</td></tr>
<tr><td>Tempat Kelahiran</td><td>:</td><td>{getValue("tempat lahir anak")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin anak")}</td></tr>
<tr><td>Anak ke</td><td>:</td><td>{getValue("anak ke")}</td></tr>
<tr><td>Nama Anak</td><td>:</td><td>{getValue("nama anak")}</td></tr>
</tbody>
</table>
</div>
{/* DATA IBU */}
<div style={{ marginTop: "20px" }}>
Dari seorang ibu bernama:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "200px" }}>Nama Lengkap Ibu</td><td>:</td><td>{getValue("nama ibu")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik ibu")}</td></tr>
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ibu")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ibu")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ibu")}</td></tr>
</tbody>
</table>
</div>
{/* DATA AYAH */}
<div style={{ marginTop: "20px" }}>
Dan seorang ayah bernama:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "200px" }}>Nama Lengkap Ayah</td><td>:</td><td>{getValue("nama ayah")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik ayah")}</td></tr>
<tr><td>Tempat & Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir ayah")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan ayah")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat ayah")}</td></tr>
</tbody>
</table>
</div>
{/* DATA PELAPOR */}
<div style={{ marginTop: "20px" }}>
Berdasarkan laporan dari:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "200px" }}>Nama Pelapor</td><td>:</td><td>{getValue("nama pelapor")}</td></tr>
<tr><td>Hubungan dengan Anak</td><td>:</td><td>{getValue("hubungan pelapor")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat pelapor")}</td></tr>
</tbody>
</table>
</div>
{/* PENUTUP */}
<div style={{ marginTop: "20px", textAlign: "justify" }}>
Demikian Surat Keterangan Kelahiran ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
{/* TEMPAT TANGGAL */}
<table style={{ width: "100%", marginTop: "20px" }}>
<tbody>
<tr><td style={{ width: "200px" }}>Dikeluarkan di</td><td>:</td><td>{data.setting.desaNama}</td></tr>
<tr><td>Pada tanggal</td><td>:</td><td>{data.surat.createdAt}</td></tr>
</tbody>
</table>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.2" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b>
PEMERINTAH KABUPATEN/KOTA {_.upperCase(data.setting.desaKabupaten)}
</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b>
<u>SURAT KETERANGAN KELAHIRAN</u>
</b>
<br />
Nomor : {data.surat.noSurat}
</div>
{/* PEMBUKA */}
<div>
Yang bertanda tangan di bawah ini, {data.setting.perbekelJabatan}
{` ${data.setting.desaNama}, Kecamatan ${data.setting.desaKecamatan}, Kabupaten/Kota ${data.setting.desaKabupaten}`}
, dengan ini menerangkan bahwa:
</div>
{/* DATA KELAHIRAN ANAK */}
<div style={{ marginTop: "20px" }}>
Telah lahir seorang anak pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "200px" }}>Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tanggal lahir anak")}</td>
</tr>
<tr>
<td>Pukul</td>
<td>:</td>
<td>{getValue("pukul lahir anak")}</td>
</tr>
<tr>
<td>Tempat Kelahiran</td>
<td>:</td>
<td>{getValue("tempat lahir anak")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin anak")}</td>
</tr>
<tr>
<td>Anak ke</td>
<td>:</td>
<td>{getValue("anak ke")}</td>
</tr>
<tr>
<td>Nama Anak</td>
<td>:</td>
<td>{getValue("nama anak")}</td>
</tr>
</tbody>
</table>
</div>
{/* DATA IBU */}
<div style={{ marginTop: "20px" }}>
Dari seorang ibu bernama:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "200px" }}>Nama Lengkap Ibu</td>
<td>:</td>
<td>{getValue("nama ibu")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik ibu")}</td>
</tr>
<tr>
<td>Tempat & Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir ibu")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan ibu")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat ibu")}</td>
</tr>
</tbody>
</table>
</div>
{/* DATA AYAH */}
<div style={{ marginTop: "20px" }}>
Dan seorang ayah bernama:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "200px" }}>Nama Lengkap Ayah</td>
<td>:</td>
<td>{getValue("nama ayah")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik ayah")}</td>
</tr>
<tr>
<td>Tempat & Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir ayah")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan ayah")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat ayah")}</td>
</tr>
</tbody>
</table>
</div>
{/* DATA PELAPOR */}
<div style={{ marginTop: "20px" }}>
Berdasarkan laporan dari:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "200px" }}>Nama Pelapor</td>
<td>:</td>
<td>{getValue("nama pelapor")}</td>
</tr>
<tr>
<td>Hubungan dengan Anak</td>
<td>:</td>
<td>{getValue("hubungan pelapor")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat pelapor")}</td>
</tr>
</tbody>
</table>
</div>
{/* PENUTUP */}
<div style={{ marginTop: "20px", textAlign: "justify" }}>
Demikian Surat Keterangan Kelahiran ini dibuat dengan sebenarnya agar
dapat digunakan sebagaimana mestinya.
</div>
{/* TEMPAT TANGGAL */}
<table style={{ width: "100%", marginTop: "20px" }}>
<tbody>
<tr>
<td style={{ width: "200px" }}>Dikeluarkan di</td>
<td>:</td>
<td>{data.setting.desaNama}</td>
</tr>
<tr>
<td>Pada tanggal</td>
<td>:</td>
<td>{data.surat.createdAt}</td>
</tr>
</tbody>
</table>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "40px",
width: "100%",
display: "flex",
justifyContent: "flex-end",
}}
>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,143 +3,153 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKKelakuanBaik({ data }: { data: any }) { export default function SKKelakuanBaik({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "30px" }}>
<b style={{ fontSize: "18px" }}>SURAT KETERANGAN KELAKUAN BAIK</b><br />
(PENGANTAR SKCK)<br />
Nomor: {data.surat.noSurat}
</div>
{/* PEMBUKA */}
<div style={{ marginBottom: "15px" }}>
Yang bertanda tangan di bawah ini menerangkan dengan sebenarnya bahwa:
</div>
{/* IDENTITAS PENDUDUK */}
<table style={{ width: "100%", marginBottom: "15px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama lengkap</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Tempat/Tgl Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
{/* ISI */}
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Adalah benar penduduk yang berdomisili di wilayah kami dan selama tinggal di lingkungan
Desa {data.setting.desaNama}, berkelakuan baik, tidak pernah terlibat perbuatan melanggar hukum,
serta dikenal sopan dan aktif dalam kegiatan kemasyarakatan.
</div>
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Surat keterangan ini diberikan sebagai pengantar permohonan penerbitan Surat Keterangan
Catatan Kepolisian (SKCK) ke Polsek/Polres {getValue("polsek")}.
</div>
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Surat ini berlaku selama 6 (enam) bulan sejak tanggal diterbitkan, kecuali terdapat perubahan
data yang mendasar.
</div>
<div style={{ textAlign: "justify", marginBottom: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
</div>
{/* TANGGAL */}
<table style={{ width: "100%", marginBottom: "40px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Dikeluarkan di</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.desaNama}</td>
</tr>
<tr>
<td>Pada tanggal</td>
<td>:</td>
<td>{data.surat.createdAt}</td>
</tr>
</tbody>
</table>
{/* TANDA TANGAN */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u><br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "30px" }}>
<b style={{ fontSize: "18px" }}>SURAT KETERANGAN KELAKUAN BAIK</b>
<br />
(PENGANTAR SKCK)
<br />
Nomor: {data.surat.noSurat}
</div> </div>
);
{/* PEMBUKA */}
<div style={{ marginBottom: "15px" }}>
Yang bertanda tangan di bawah ini menerangkan dengan sebenarnya bahwa:
</div>
{/* IDENTITAS PENDUDUK */}
<table style={{ width: "100%", marginBottom: "15px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama lengkap</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Tempat/Tgl Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
{/* ISI */}
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Adalah benar penduduk yang berdomisili di wilayah kami dan selama
tinggal di lingkungan Desa {data.setting.desaNama}, berkelakuan baik,
tidak pernah terlibat perbuatan melanggar hukum, serta dikenal sopan dan
aktif dalam kegiatan kemasyarakatan.
</div>
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Surat keterangan ini diberikan sebagai pengantar permohonan penerbitan
Surat Keterangan Catatan Kepolisian (SKCK) ke Polsek/Polres{" "}
{getValue("polsek")}.
</div>
<div style={{ textAlign: "justify", marginBottom: "15px" }}>
Surat ini berlaku selama 6 (enam) bulan sejak tanggal diterbitkan,
kecuali terdapat perubahan data yang mendasar.
</div>
<div style={{ textAlign: "justify", marginBottom: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk
dipergunakan sebagaimana mestinya.
</div>
{/* TANGGAL */}
<table style={{ width: "100%", marginBottom: "40px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Dikeluarkan di</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.desaNama}</td>
</tr>
<tr>
<td>Pada tanggal</td>
<td>:</td>
<td>{data.surat.createdAt}</td>
</tr>
</tbody>
</table>
{/* TANDA TANGAN */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u>
<br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,118 +3,201 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKKematian({ data }: { data: any }) { export default function SKKematian({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b><u>SURAT KETERANGAN KEMATIAN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
<tr><td>Hubungan dengan almarhum/almarhumah</td><td>:</td><td>{getValue("hubungan dengan almarhum")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Melaporkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>NIK</td><td>:</td><td>{getValue("nik")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Tempat/Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Telah meninggal dunia pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Tanggal Kematian</td><td style={{ width: "10px" }}>:</td><td>{getValue("tanggal kematian")}</td></tr>
<tr><td>Waktu Kematian</td><td>:</td><td>{getValue("waktu kematian")}</td></tr>
<tr><td>Tempat Kematian</td><td>:</td><td>{getValue("tempat kematian")}</td></tr>
<tr><td>Penyebab Kematian</td><td>:</td><td>{getValue("penyebab kematian")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat digunakan sebagaimana mestinya.
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "40px", display: "flex", justifyContent: "space-between", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br /><br />
Pemohon
<br /><br /><br /><br /> <br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
<br />
Kode Pos: {data.setting.desaPos}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b>
<u>SURAT KETERANGAN KEMATIAN</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
<tr>
<td>Hubungan dengan almarhum/almarhumah</td>
<td>:</td>
<td>{getValue("hubungan dengan almarhum")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Melaporkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>NIK</td>
<td>:</td>
<td>{getValue("nik")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Telah meninggal dunia pada:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Tanggal Kematian</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("tanggal kematian")}</td>
</tr>
<tr>
<td>Waktu Kematian</td>
<td>:</td>
<td>{getValue("waktu kematian")}</td>
</tr>
<tr>
<td>Tempat Kematian</td>
<td>:</td>
<td>{getValue("tempat kematian")}</td>
</tr>
<tr>
<td>Penyebab Kematian</td>
<td>:</td>
<td>{getValue("penyebab kematian")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya agar dapat
digunakan sebagaimana mestinya.
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "40px",
display: "flex",
justifyContent: "space-between",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
<br />
Pemohon
<br />
<br />
<br />
<br /> <br />
<u>{getValue("nama")}</u> <br />
</div>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,141 +3,180 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKPenghasilan({ data }: { data: any }) { export default function SKPenghasilan({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return ( return (
<div style={{ lineHeight: "1.3" }}> <div style={{ lineHeight: "1.3" }}>
{/* HEADER */} {/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}> <div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br /> <b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br /> <br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br /> <b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
Alamat: {data.setting.desaAlamat}<br /> <br />
Kode Pos: {data.setting.desaPos} <b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
</div> <br />
Alamat: {data.setting.desaAlamat}
{/* JUDUL */} <br />
<div style={{ textAlign: "center", margin: "20px 0" }}> Kode Pos: {data.setting.desaPos}
<b><u>SURAT KETERANGAN PENGHASILAN</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
{/* PENGHASILAN */}
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan yang bersangkutan, orang tersebut memiliki penghasilan rata-rata:
<table style={{ width: "100%", marginTop: "10px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Penghasilan</td>
<td style={{ width: "10px" }}>:</td>
<td>
Rp {getValue("penghasilan")}
{" "}
({getValue("penghasilan terbilang")}) per bulan
</td>
</tr>
</tbody>
</table>
</div>
{/* KEPERLUAN */}
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat untuk keperluan: <b>{getValue("alasan permohonan")}</b>.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
</div>
{/* TANGGAL & TANDA TANGAN */}
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
<div style={{ marginTop: "40px", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "20px 0" }}>
<b>
<u>SURAT KETERANGAN PENGHASILAN</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Tempat / Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
</div>
{/* PENGHASILAN */}
<div style={{ marginTop: "20px" }}>
Berdasarkan keterangan yang bersangkutan, orang tersebut memiliki
penghasilan rata-rata:
<table style={{ width: "100%", marginTop: "10px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Penghasilan</td>
<td style={{ width: "10px" }}>:</td>
<td>
Rp {getValue("penghasilan")} (
{getValue("penghasilan terbilang")}) per bulan
</td>
</tr>
</tbody>
</table>
</div>
{/* KEPERLUAN */}
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat untuk keperluan:{" "}
<b>{getValue("alasan permohonan")}</b>.
</div>
<div style={{ marginTop: "20px" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat
dipergunakan sebagaimana mestinya.
</div>
{/* TANGGAL & TANDA TANGAN */}
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
<div
style={{
marginTop: "40px",
display: "flex",
justifyContent: "flex-end",
}}
>
<div style={{ textAlign: "center" }}>
Kepala Desa / Lurah {data.setting.desaNama}
<br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -3,119 +3,142 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKTempatUsaha({ data }: { data: any }) { export default function SKTempatUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) => const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || ""); _.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => { const loadImage = async () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
return ( <div style={{ lineHeight: "1.5" }}>
<div style={{ lineHeight: "1.5" }}> {/* TITLE */}
{/* TITLE */} <div style={{ textAlign: "center", marginBottom: "20px" }}>
<div style={{ textAlign: "center", marginBottom: "20px" }}> <b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b>
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</b><br /> <br />
Nomor: {data.surat.noSurat} Nomor: {data.surat.noSurat}
</div>
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya:
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
<Row label="Alamat" value={data.setting.desaAlamat} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama")} />
<Row label="Tempat/Tanggal Lahir" value={getValue("tempat tanggal lahir")} />
<Row label="Alamat Pemilik Usaha" value={getValue("alamat")} />
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
<br />
<div>Benar yang bersangkutan memiliki tempat usaha dengan keterangan seperti berikut:</div>
<div>
<Row label="Nama Usaha" value={getValue("nama usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat usaha")} />
<Row label="Status Tempat Usaha" value={getValue("status tempat usaha")} />
<Row label="Luas Tempat Usaha" value={getValue("luas tempat usaha")} />
<Row label="Jumlah Karyawan" value={getValue("jumlah karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan <b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat dipergunakan sebagaimana mestinya.
</p>
<div>
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
<Row label="Pada tanggal" value={data.surat.createdAt} />
</div>
<br /><br />
{/* TANDA TANGAN */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
</div> </div>
);
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya:
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
<Row label="Alamat" value={data.setting.desaAlamat} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama Pemilik Usaha" value={getValue("nama")} />
<Row
label="Tempat/Tanggal Lahir"
value={getValue("tempat tanggal lahir")}
/>
<Row label="Alamat Pemilik Usaha" value={getValue("alamat")} />
<Row label="Nomor KTP" value={getValue("nik")} />
</div>
<br />
<div>
Benar yang bersangkutan memiliki tempat usaha dengan keterangan
seperti berikut:
</div>
<div>
<Row label="Nama Usaha" value={getValue("nama usaha")} />
<Row label="Bidang Usaha" value={getValue("bidang usaha")} />
<Row label="Alamat Usaha" value={getValue("alamat usaha")} />
<Row
label="Status Tempat Usaha"
value={getValue("status tempat usaha")}
/>
<Row
label="Luas Tempat Usaha"
value={getValue("luas tempat usaha")}
/>
<Row label="Jumlah Karyawan" value={getValue("jumlah karyawan")} />
</div>
<p style={{ textAlign: "justify" }}>
Surat keterangan ini dibuat untuk keperluan{" "}
<b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini dibuat dengan sebenarnya untuk dapat
dipergunakan sebagaimana mestinya.
</p>
<div>
<Row label="Dikeluarkan di" value={data.setting.desaNama} />
<Row label="Pada tanggal" value={data.surat.createdAt} />
</div>
<br />
<br />
{/* TANDA TANGAN */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u>
<br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
</div>
);
} }
function Row({ label, value }: { label: string, value: string }) { function Row({ label, value }: { label: string; value: string }) {
return ( return (
<div style={{ display: "flex", marginBottom: "4px" }}> <div style={{ display: "flex", marginBottom: "4px" }}>
<div style={{ width: "180px" }}>{label}</div> <div style={{ width: "180px" }}>{label}</div>
<div style={{ width: "10px" }}>:</div> <div style={{ width: "10px" }}>:</div>
<div>{value}</div> <div>{value}</div>
</div> </div>
); );
} }

View File

@@ -3,110 +3,118 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKTidakMampu({ data }: { data: any }) { export default function SKTidakMampu({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) => const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || ""); _.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => {
try {
setViewImg("");
if (!data.setting.perbekelTTD) return;
const loadImage = async () => { const urlApi =
try { "/api/pengaduan/image?folder=lainnya&fileName=" +
setViewImg(""); data.setting.perbekelTTD;
if (!data.setting.perbekelTTD) return; // Fetch manual agar mendapatkan Response asli
const res = await fetch(urlApi);
if (!res.ok)
return notification({
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; setViewImg(url);
// Fetch manual agar mendapatkan Response asli } catch (err) {
const res = await fetch(urlApi); console.error("Gagal load gambar:", err);
if (!res.ok) }
return notification({ };
title: "Error",
message: "Failed to load image sign",
type: "error",
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); useEffect(() => {
} catch (err) { loadImage();
console.error("Gagal load gambar:", err); }, [data]);
}
};
useEffect(() => { return (
loadImage(); <div style={{ lineHeight: "1.5" }}>
}, [data]); {/* TITLE */}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
return ( <b style={{ fontSize: "16px" }}>SURAT KETERANGAN TIDAK MAMPU</b>
<div style={{ lineHeight: "1.5" }}> <br />
{/* TITLE */} Nomor: {data.surat.noSurat}
<div style={{ textAlign: "center", marginBottom: "20px" }}>
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TIDAK MAMPU</b><br />
Nomor: {data.surat.noSurat}
</div>
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Alamat" value={data.setting.desaAlamat} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama" value={getValue("nama")} />
<Row label="Tempat Tgl Lahir" value={getValue("tempat tanggal lahir")} />
<Row label="Alamat" value={getValue("alamat")} />
<Row label="NIK" value={getValue("nik")} />
</div>
<br />
<p style={{ textAlign: "justify" }}>
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan termasuk keluarga tidak mampu.
Surat keterangan ini dipergunakan untuk
<b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini kami buat dengan sebenar-benarnya untuk dapat dipergunakan
sebagaimana mestinya.
</p>
<br /><br />
{/* TANDA TANGAN */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /><br />
<u>{data.setting.perbekelNama}</u><br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
</div> </div>
);
{/* ISI */}
<div>
<div style={{ marginBottom: "10px" }}>
Yang bertanda tangan dibawah ini, saya
</div>
{/* DATA PEJABAT */}
<div>
<Row label="Nama" value={data.setting.perbekelNama} />
<Row label="Alamat" value={data.setting.desaAlamat} />
<Row label="Jabatan" value={data.setting.perbekelJabatan} />
</div>
<br />
<div>Dengan ini menerangkan bahwa:</div>
{/* DATA WARGA */}
<div>
<Row label="Nama" value={getValue("nama")} />
<Row
label="Tempat Tgl Lahir"
value={getValue("tempat tanggal lahir")}
/>
<Row label="Alamat" value={getValue("alamat")} />
<Row label="NIK" value={getValue("nik")} />
</div>
<br />
<p style={{ textAlign: "justify" }}>
Orang tersebut benar-benar penduduk desa {data.setting.desaNama} dan
termasuk keluarga tidak mampu. Surat keterangan ini dipergunakan untuk
<b>{getValue("alasan permohonan")}.</b>
</p>
<p style={{ textAlign: "justify" }}>
Demikian surat keterangan ini kami buat dengan sebenar-benarnya untuk
dapat dipergunakan sebagaimana mestinya.
</p>
<br />
<br />
{/* TANDA TANGAN */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
{data.setting.desaKabupaten}, {data.surat.createdAt} <br /> <br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u>
<br />
{data.setting.perbekelJabatan + " " + data.setting.desaNama}
</div>
</div>
</div>
</div>
);
} }
function Row({ label, value }: { label: string, value: string }) { function Row({ label, value }: { label: string; value: string }) {
return ( return (
<div style={{ display: "flex", marginBottom: "4px" }}> <div style={{ display: "flex", marginBottom: "4px" }}>
<div style={{ width: "180px" }}>{label}</div> <div style={{ width: "180px" }}>{label}</div>
<div style={{ width: "10px" }}>:</div> <div style={{ width: "10px" }}>:</div>
<div>{value}</div> <div>{value}</div>
</div> </div>
); );
} }

View File

@@ -3,147 +3,217 @@ import { useEffect, useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKUsaha({ data }: { data: any }) { export default function SKUsaha({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
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 () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useEffect(() => { useEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}<br />
Kode Pos: {data.setting.desaPos}
</div>
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "15px 0" }}>
<b><u>SURAT KETERANGAN USAHA</u></b><br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan dengan sesungguhnya bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Nama</td><td style={{ width: "10px" }}>:</td><td>{getValue("nama")}</td></tr>
<tr><td>Jenis Kelamin</td><td>:</td><td>{getValue("jenis kelamin")}</td></tr>
<tr><td>Tempat / Tanggal Lahir</td><td>:</td><td>{getValue("tempat tanggal lahir")}</td></tr>
<tr><td>Warga Negara</td><td>:</td><td>{getValue("negara")}</td></tr>
<tr><td>Agama</td><td>:</td><td>{getValue("agama")}</td></tr>
<tr><td>Status</td><td>:</td><td>{getValue("status perkawinan")}</td></tr>
<tr><td>Pekerjaan</td><td>:</td><td>{getValue("pekerjaan")}</td></tr>
<tr><td>Alamat</td><td>:</td><td>{getValue("alamat")}</td></tr>
</tbody>
</table>
</div>
{/* DOMISILI */}
<div style={{ marginTop: "20px" }}>
Bahwa orang tersebut di atas benar-benar penduduk:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Desa / Kelurahan</td><td style={{ width: "10px" }}>:</td><td>{data.setting.desaNama}</td></tr>
<tr><td>Kecamatan</td><td>:</td><td>{data.setting.desaKecamatan}</td></tr>
<tr><td>Kabupaten</td><td>:</td><td>{data.setting.desaKabupaten}</td></tr>
</tbody>
</table>
</div>
{/* USAHA */}
<div style={{ marginTop: "20px" }}>
Dan yang bersangkutan benar memiliki usaha:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr><td style={{ width: "160px" }}>Jenis Usaha</td><td style={{ width: "10px" }}>:</td><td>{getValue("jenis usaha")}</td></tr>
<tr><td>Alamat Usaha</td><td>:</td><td>{getValue("alamat usaha")}</td></tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div style={{ marginTop: "10px", display: "flex", justifyContent: "flex-end", width: "100%" }}>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA / KELURAHAN {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}
<br />
Kode Pos: {data.setting.desaPos}
</div> </div>
);
{/* JUDUL */}
<div style={{ textAlign: "center", margin: "15px 0" }}>
<b>
<u>SURAT KETERANGAN USAHA</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
{/* YANG BERTANDA TANGAN */}
<div style={{ marginTop: "15px" }}>
Yang bertanda tangan di bawah ini:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>:</td>
<td>{data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* IDENTITAS ORANG YG MEMINTA SURAT */}
<div style={{ marginTop: "20px" }}>
Dengan ini menerangkan dengan sesungguhnya bahwa:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Nama</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("nama")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>:</td>
<td>{getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Tempat / Tanggal Lahir</td>
<td>:</td>
<td>{getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Warga Negara</td>
<td>:</td>
<td>{getValue("negara")}</td>
</tr>
<tr>
<td>Agama</td>
<td>:</td>
<td>{getValue("agama")}</td>
</tr>
<tr>
<td>Status</td>
<td>:</td>
<td>{getValue("status perkawinan")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>:</td>
<td>{getValue("pekerjaan")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>:</td>
<td>{getValue("alamat")}</td>
</tr>
</tbody>
</table>
</div>
{/* DOMISILI */}
<div style={{ marginTop: "20px" }}>
Bahwa orang tersebut di atas benar-benar penduduk:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Desa / Kelurahan</td>
<td style={{ width: "10px" }}>:</td>
<td>{data.setting.desaNama}</td>
</tr>
<tr>
<td>Kecamatan</td>
<td>:</td>
<td>{data.setting.desaKecamatan}</td>
</tr>
<tr>
<td>Kabupaten</td>
<td>:</td>
<td>{data.setting.desaKabupaten}</td>
</tr>
</tbody>
</table>
</div>
{/* USAHA */}
<div style={{ marginTop: "20px" }}>
Dan yang bersangkutan benar memiliki usaha:
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "160px" }}>Jenis Usaha</td>
<td style={{ width: "10px" }}>:</td>
<td>{getValue("jenis usaha")}</td>
</tr>
<tr>
<td>Alamat Usaha</td>
<td>:</td>
<td>{getValue("alamat usaha")}</td>
</tr>
</tbody>
</table>
</div>
<div style={{ marginTop: "20px" }}>
Surat keterangan ini dibuat dengan sebenarnya untuk dipergunakan
sebagaimana mestinya.
</div>
<div style={{ marginTop: "20px" }}>
Dikeluarkan di {data.setting.desaNama} <br />
Pada tanggal {data.surat.createdAt}
</div>
{/* TANDA TANGAN */}
<div
style={{
marginTop: "10px",
display: "flex",
justifyContent: "flex-end",
width: "100%",
}}
>
<div style={{ textAlign: "center" }}>
<br />
Kepala Desa / Lurah {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -4,191 +4,209 @@ import { useState } from "react";
import notification from "../notificationGlobal"; import notification from "../notificationGlobal";
export default function SKYatim({ data }: { data: any }) { export default function SKYatim({ data }: { data: any }) {
const [viewImg, setViewImg] = useState<string>(""); const [viewImg, setViewImg] = useState<string>("");
const getValue = (key: string) => const getValue = (key: string) =>
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || ""); _.upperFirst(
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
);
const loadImage = async () => { const loadImage = async () => {
try { try {
setViewImg(""); setViewImg("");
if (!data.setting.perbekelTTD) return; if (!data.setting.perbekelTTD) return;
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD; const urlApi =
// Fetch manual agar mendapatkan Response asli "/api/pengaduan/image?folder=lainnya&fileName=" +
const res = await fetch(urlApi); data.setting.perbekelTTD;
if (!res.ok) // Fetch manual agar mendapatkan Response asli
return notification({ const res = await fetch(urlApi);
title: "Error", if (!res.ok)
message: "Failed to load image sign", return notification({
type: "error", title: "Error",
}); message: "Failed to load image sign",
const blob = await res.blob(); type: "error",
const url = URL.createObjectURL(blob); });
const blob = await res.blob();
const url = URL.createObjectURL(blob);
setViewImg(url); setViewImg(url);
} catch (err) { } catch (err) {
console.error("Gagal load gambar:", err); console.error("Gagal load gambar:", err);
} }
}; };
useShallowEffect(() => { useShallowEffect(() => {
loadImage(); loadImage();
}, [data]); }, [data]);
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b><br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b><br />
<b>DESA {_.upperCase(data.setting.desaNama)}</b><br />
Alamat: {data.setting.desaAlamat}. Kode Pos: {data.setting.desaPos}
</div>
<div style={{ textAlign: "center", marginTop: "15px" }}>
<b><u>SURAT KETERANGAN YATIM / PIATU / YATIM PIATU</u></b><br />
Nomor: {data.surat.noSurat}
</div>
<br />
{/* BAGIAN PENANDATANGAN */}
<div>Yang bertanda tangan di bawah ini:</div>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>: {data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Alamat Kantor</td>
<td>: {data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
<br />
{/* BAGIAN IDENTITAS ANAK */}
<div>Dengan ini menerangkan bahwa:</div>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {getValue("nama")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>: {getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>: {getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>: {getValue("alamat")}</td>
</tr>
<tr>
<td>NIK</td>
<td>: {getValue("nik")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>: {getValue("pekerjaan")}</td>
</tr>
</tbody>
</table>
<br />
{/* KETERANGAN ORANG TUA */}
<div>
Benar bahwa yang bersangkutan adalah <b>anak (Yatim / Piatu / Yatim Piatu)</b>,
dengan keterangan sebagai berikut:
</div>
<br />
<div><b>1. Nama Ayah</b></div>
<table style={{ width: "100%" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ayah</td>
<td>: {getValue("nama ayah")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ayah")}</td>
</tr>
</tbody>
</table>
<br />
<div><b>2. Nama Ibu</b></div>
<table style={{ width: "100%" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ibu</td>
<td>: {getValue("nama ibu")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ibu")}</td>
</tr>
</tbody>
</table>
<br />
<div>
Dengan demikian, berdasarkan keterangan pihak keluarga dan data di Kantor Desa,
maka benar bahwa yang bersangkutan adalah
<b> anak (Yatim / Piatu / Yatim Piatu).</b>
</div>
<br />
<div>
Surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan sebagaimana mestinya.
</div>
<br />
{/* TANGGAL & TEMPAT */}
<table style={{ width: "100%", marginTop: "10px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Dikeluarkan di</td>
<td>: {data.setting.desaNama}</td>
</tr>
<tr>
<td>Pada tanggal</td>
<td>: {data.surat.createdAt}</td>
</tr>
</tbody>
</table>
<br />
{/* TTD */}
<div style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br /><br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} /> <br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
return (
<div style={{ lineHeight: "1.3" }}>
{/* HEADER */}
<div style={{ textAlign: "center", marginBottom: "10px" }}>
<b>PEMERINTAH KABUPATEN {_.upperCase(data.setting.desaKabupaten)}</b>
<br />
<b>KECAMATAN {_.upperCase(data.setting.desaKecamatan)}</b>
<br />
<b>DESA {_.upperCase(data.setting.desaNama)}</b>
<br />
Alamat: {data.setting.desaAlamat}. Kode Pos: {data.setting.desaPos}
</div> </div>
);
<div style={{ textAlign: "center", marginTop: "15px" }}>
<b>
<u>SURAT KETERANGAN YATIM / PIATU / YATIM PIATU</u>
</b>
<br />
Nomor: {data.surat.noSurat}
</div>
<br />
{/* BAGIAN PENANDATANGAN */}
<div>Yang bertanda tangan di bawah ini:</div>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {data.setting.perbekelNama}</td>
</tr>
<tr>
<td>Jabatan</td>
<td>: {data.setting.perbekelJabatan}</td>
</tr>
<tr>
<td>Alamat Kantor</td>
<td>: {data.setting.desaAlamat}</td>
</tr>
</tbody>
</table>
<br />
{/* BAGIAN IDENTITAS ANAK */}
<div>Dengan ini menerangkan bahwa:</div>
<table style={{ width: "100%", marginTop: "5px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama</td>
<td>: {getValue("nama")}</td>
</tr>
<tr>
<td>Tempat/Tanggal Lahir</td>
<td>: {getValue("tempat tanggal lahir")}</td>
</tr>
<tr>
<td>Jenis Kelamin</td>
<td>: {getValue("jenis kelamin")}</td>
</tr>
<tr>
<td>Alamat</td>
<td>: {getValue("alamat")}</td>
</tr>
<tr>
<td>NIK</td>
<td>: {getValue("nik")}</td>
</tr>
<tr>
<td>Pekerjaan</td>
<td>: {getValue("pekerjaan")}</td>
</tr>
</tbody>
</table>
<br />
{/* KETERANGAN ORANG TUA */}
<div>
Benar bahwa yang bersangkutan adalah{" "}
<b>anak (Yatim / Piatu / Yatim Piatu)</b>, dengan keterangan sebagai
berikut:
</div>
<br />
<div>
<b>1. Nama Ayah</b>
</div>
<table style={{ width: "100%" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ayah</td>
<td>: {getValue("nama ayah")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ayah")}</td>
</tr>
</tbody>
</table>
<br />
<div>
<b>2. Nama Ibu</b>
</div>
<table style={{ width: "100%" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Nama Ibu</td>
<td>: {getValue("nama ibu")}</td>
</tr>
<tr>
<td>Status</td>
<td>: {getValue("status ibu")}</td>
</tr>
</tbody>
</table>
<br />
<div>
Dengan demikian, berdasarkan keterangan pihak keluarga dan data di
Kantor Desa, maka benar bahwa yang bersangkutan adalah
<b> anak (Yatim / Piatu / Yatim Piatu).</b>
</div>
<br />
<div>
Surat keterangan ini dibuat dengan sebenar-benarnya untuk dipergunakan
sebagaimana mestinya.
</div>
<br />
{/* TANGGAL & TEMPAT */}
<table style={{ width: "100%", marginTop: "10px" }}>
<tbody>
<tr>
<td style={{ width: "180px" }}>Dikeluarkan di</td>
<td>: {data.setting.desaNama}</td>
</tr>
<tr>
<td>Pada tanggal</td>
<td>: {data.surat.createdAt}</td>
</tr>
</tbody>
</table>
<br />
{/* TTD */}
<div
style={{ width: "100%", display: "flex", justifyContent: "flex-end" }}
>
<div style={{ textAlign: "center" }}>
Kepala Desa {data.setting.desaNama}
<br />
<br />
<img src={viewImg || undefined} alt="ttd perbekel" width={100} />{" "}
<br />
<u>{data.setting.perbekelNama}</u> <br />
NIP. {data.setting.perbekelNIP}
</div>
</div>
</div>
);
} }

View File

@@ -28,16 +28,19 @@ export default function Login() {
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"]; window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
break; break;
case "credential": case "credential":
window.location.href = clientRoutes["/scr/dashboard/credential/credential"]; window.location.href =
clientRoutes["/scr/dashboard/credential/credential"];
break; break;
case "setting": case "setting":
window.location.href = clientRoutes["/scr/dashboard/setting/detail-setting"]; window.location.href =
clientRoutes["/scr/dashboard/setting/detail-setting"];
break; break;
case "api_key": case "api_key":
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"]; window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
break; break;
case "pelayanan": case "pelayanan":
window.location.href = clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"]; window.location.href =
clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
break; break;
default: default:
window.location.href = clientRoutes["/scr/dashboard"]; window.location.href = clientRoutes["/scr/dashboard"];

View File

@@ -0,0 +1,331 @@
import apiFetch from "@/lib/apiFetch";
import {
ActionIcon,
Badge,
Box,
Button,
Card,
Container,
Divider,
Grid,
Group,
Select,
Stack,
Text,
TextInput,
Textarea,
Tooltip
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import {
IconBuildingCommunity,
IconInfoCircle,
IconMapPin,
IconUser
} from "@tabler/icons-react";
import React from "react";
import useSWR from "swr";
// =========================
// Reusable UI components
// =========================
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
return (
<Group justify="apart" gap="xs" align="center">
<Text fw={600}>{label}</Text>
{hint && (
<Tooltip label={hint} withArrow>
<ActionIcon size={24} variant="subtle">
<IconInfoCircle size={16} />
</ActionIcon>
</Tooltip>
)}
</Group>
);
}
function FormSection({
title,
icon,
children,
description,
}: {
title: string;
icon?: React.ReactNode;
children: React.ReactNode;
description?: string;
}) {
return (
<Card radius="md" shadow="sm" withBorder>
<Group justify="apart" align="center" mb="xs">
<Group align="center" gap="xs">
{icon}
<Text fw={700}>{title}</Text>
</Group>
{description && <Badge variant="light">{description}</Badge>}
</Group>
<Divider mb="sm" />
<Stack gap="sm">{children}</Stack>
</Card>
);
}
// =========================
// Main form component
// =========================
export default function FormSurat() {
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
apiFetch.api.pelayanan.category.get(),
);
const listCategory = data?.data || [];
useShallowEffect(() => {
mutate();
}, []);
return (
<Container size="md" w={"100%"}>
<Box>
<Stack gap="lg">
<Group justify="apart" align="center">
<Group align="center">
<IconBuildingCommunity size={28} />
<div>
<Text fw={800} size="xl">
Surat Keterangan Tidak Mampu (SKTM)
</Text>
<Text size="sm" c="dimmed">
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu
digunakan untuk keperluan pendidikan, kesehatan, atau
administrasi.
</Text>
</div>
</Group>
<Group>
<Badge radius="sm">Form Length: 3 Sections</Badge>
</Group>
</Group>
<form>
<Stack gap="lg">
{/* Header Section */}
<FormSection
title="Pemohon"
icon={<IconUser size={16} />}
description="Informasi identitas pemohon"
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nama Lengkap"
hint="Nama lengkap pemohon"
/>
}
placeholder="Budi Setiawan"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nomor Telephone"
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
/>
}
placeholder="08123456789"
/>
</Grid.Col>
<Grid.Col span={12}>
<Select
label={<FieldLabel label="Jenis Surat" hint="Jenis surat yang ingin diajukan" />}
placeholder="Pilih jenis surat"
data={listCategory.map((item: any) => ({
value: item.id,
label: item.name,
}))}
/>
</Grid.Col>
</Grid>
</FormSection>
<FormSection
title="Syarat Dokumen"
description="Syarat dokumen yang diperlukan"
>
<Grid>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Nama Lengkap"
hint="Sesuai KTP"
/>
}
placeholder="Nama lengkap"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="NIK"
hint="16 digit, tanpa spasi"
/>
}
placeholder="3201xxxxxxxxxxxx"
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={
<FieldLabel
label="Tempat, Tanggal Lahir"
hint="Contoh: Denpasar, 31-12-1990"
/>
}
placeholder="Tempat, tanggal lahir"
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Jenis Kelamin" />}
placeholder="Pilih jenis kelamin"
data={["Laki-laki", "Perempuan"]}
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Agama" />}
placeholder="Pilih agama"
data={[
"Islam",
"Kristen",
"Katolik",
"Hindu",
"Buddha",
"Konghucu",
"Lainnya",
]}
/>
</Grid.Col>
<Grid.Col span={6}>
<Select
label={<FieldLabel label="Status Perkawinan" />}
placeholder="Pilih status"
data={[
"Belum Kawin",
"Kawin",
"Cerai Hidup",
"Cerai Mati",
]}
/>
</Grid.Col>
<Grid.Col span={6}>
<TextInput
label={<FieldLabel label="Pekerjaan" />}
placeholder="Pekerjaan"
/>
</Grid.Col>
<Grid.Col span={12}>
<Textarea
label={<FieldLabel label="Alamat Lengkap" />}
placeholder="Alamat domisili"
minRows={2}
/>
</Grid.Col>
<Grid.Col span={2}>
<TextInput
label={<FieldLabel label="RT" />}
placeholder="001"
/>
</Grid.Col>
<Grid.Col span={2}>
<TextInput
label={<FieldLabel label="RW" />}
placeholder="002"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Desa / Kelurahan" />}
placeholder="Desa"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Kecamatan" />}
placeholder="Kecamatan"
/>
</Grid.Col>
<Grid.Col span={4}>
<TextInput
label={<FieldLabel label="Kabupaten / Kota" />}
placeholder="Kabupaten / Kota"
/>
</Grid.Col>
</Grid>
</FormSection>
{/* Keterangan Section */}
<FormSection
title="Keterangan"
icon={<IconMapPin size={18} />}
description="Isi pernyataan SKTM"
>
<Textarea
label={
<FieldLabel
label="Isi Surat"
hint="Jelaskan kondisi ekonomi secara singkat"
/>
}
placeholder="Pernyataan resmi bahwa yang bersangkutan benar-benar tergolong keluarga tidak mampu..."
minRows={4}
/>
<TextInput
label={
<FieldLabel
label="Keperluan"
hint="Contoh: Beasiswa pendidikan / Perawatan kesehatan"
/>
}
placeholder="Keperluan surat"
/>
</FormSection>
{/* Actions */}
<Group justify="right" mt="md">
<Button variant="default" onClick={() => { }}>
Reset
</Button>
<Button type="submit">Kirim / Simpan</Button>
</Group>
</Stack>
</form>
</Stack>
</Box>
</Container>
);
}

View File

@@ -9,7 +9,7 @@ import {
Progress, Progress,
Stack, Stack,
Text, Text,
Title Title,
} from "@mantine/core"; } from "@mantine/core";
export default function Dashboard() { export default function Dashboard() {

View File

@@ -285,45 +285,47 @@ function NavigationDashboard() {
return ( return (
<Stack gap="xs" p="sm"> <Stack gap="xs" p="sm">
{navItems.filter((item) => permissions.includes(item.key)).map((item) => ( {navItems
<NavLink .filter((item) => permissions.includes(item.key))
key={item.path} .map((item) => (
active={isActive(item.path as keyof typeof clientRoute)} <NavLink
leftSection={item.icon} key={item.path}
label={ active={isActive(item.path as keyof typeof clientRoute)}
<Flex align="center" gap={6}> leftSection={item.icon}
<Text fw={500}>{item.label}</Text> label={
{isActive(item.path as keyof typeof clientRoute) && ( <Flex align="center" gap={6}>
<Badge <Text fw={500}>{item.label}</Text>
variant="light" {isActive(item.path as keyof typeof clientRoute) && (
color="teal" <Badge
radius="sm" variant="light"
size="xs" color="teal"
style={{ textTransform: "none" }} radius="sm"
> size="xs"
Active style={{ textTransform: "none" }}
</Badge> >
)} Active
</Flex> </Badge>
} )}
description={item.description} </Flex>
onClick={() => }
navigate(clientRoutes[item.path as keyof typeof clientRoute]) description={item.description}
} onClick={() =>
style={{ navigate(clientRoutes[item.path as keyof typeof clientRoute])
backgroundColor: isActive(item.path as keyof typeof clientRoute) }
? "rgba(0,255,200,0.1)" style={{
: "transparent", backgroundColor: isActive(item.path as keyof typeof clientRoute)
borderRadius: "8px", ? "rgba(0,255,200,0.1)"
transition: "all 0.2s ease", : "transparent",
}} borderRadius: "8px",
styles={{ transition: "all 0.2s ease",
label: { color: "white" }, }}
description: { color: "#aaa" }, styles={{
section: { color: "teal" }, label: { color: "white" },
}} description: { color: "#aaa" },
/> section: { color: "teal" },
))} }}
/>
))}
</Stack> </Stack>
); );
} }

View File

@@ -19,7 +19,7 @@ import {
Text, Text,
Textarea, Textarea,
ThemeIcon, ThemeIcon,
Title Title,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { import {
@@ -29,7 +29,7 @@ import {
IconFileCheck, IconFileCheck,
IconMessageReport, IconMessageReport,
IconPhone, IconPhone,
IconUser IconUser,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import type { User } from "generated/prisma"; import type { User } from "generated/prisma";
import type { JsonValue } from "generated/prisma/runtime/library"; import type { JsonValue } from "generated/prisma/runtime/library";
@@ -59,7 +59,14 @@ export default function DetailPengajuanPage() {
<Grid> <Grid>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengajuan data={data?.data?.pengajuan} syaratDokumen={data?.data?.syaratDokumen} dataText={data?.data?.dataText} onAction={() => { mutate(); }} /> <DetailDataPengajuan
data={data?.data?.pengajuan}
syaratDokumen={data?.data?.syaratDokumen}
dataText={data?.data?.dataText}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} /> <DetailDataHistori data={data?.data?.history} />
</Stack> </Stack>
</Grid.Col> </Grid.Col>
@@ -71,7 +78,17 @@ export default function DetailPengajuanPage() {
); );
} }
function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data: any, syaratDokumen: any, dataText: any, onAction: () => void }) { 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 [keterangan, setKeterangan] = useState("");
@@ -88,7 +105,9 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
setHost(data?.user ?? null); setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) { if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pelayanan")); const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pelayanan"),
);
setPermissions(onlySetting); setPermissions(onlySetting);
} }
} }
@@ -99,10 +118,15 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
try { try {
const res = await apiFetch.api.pelayanan["update-status"].post({ const res = await apiFetch.api.pelayanan["update-status"].post({
id: data?.id, id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai', status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: "selesai",
keterangan: keterangan, keterangan: keterangan,
idUser: host?.id ?? "", idUser: host?.id ?? "",
noSurat: noSurat noSurat: noSurat,
}); });
if (res?.status === 200) { if (res?.status === 200) {
@@ -120,7 +144,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
type: "error", type: "error",
}); });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification({ notification({
@@ -129,7 +152,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
type: "error", type: "error",
}); });
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
if (viewImg) { if (viewImg) {
@@ -142,7 +165,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
<ModalFile <ModalFile
open={openedPreviewFile && !_.isEmpty(viewImg)} open={openedPreviewFile && !_.isEmpty(viewImg)}
onClose={() => { onClose={() => {
setOpenedPreviewFile(false) setOpenedPreviewFile(false);
}} }}
folder="syarat-dokumen" folder="syarat-dokumen"
fileName={viewImg} fileName={viewImg}
@@ -159,14 +182,25 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
{catModal === "tolak" ? ( {catModal === "tolak" ? (
<> <>
<Text> <Text>
Anda yakin ingin menolak pengajuan surat 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}
value={keterangan}
onChange={(e) => setKeterangan(e.target.value)}
/>
<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" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}> <Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
>
Tolak Tolak
</Button> </Button>
</Group> </Group>
@@ -174,21 +208,31 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
) : ( ) : (
<> <>
<Text> <Text>
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini? Anda yakin ingin{" "}
{ {data?.status == "antrian" ? "menerima" : "menyetujui"}{" "}
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat' pengajuan surat ini?
} {data.status == "diterima" &&
"Masukkan nomer surat yang akan dibuat"}
</Text> </Text>
{ {data.status == "diterima" && (
data.status == 'diterima' && ( <Textarea
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" /> 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}>
Tidak Tidak
</Button> </Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")} disabled={data.status == 'diterima' && noSurat.length < 1}> <Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
disabled={data.status == "diterima" && noSurat.length < 1}
>
Ya Ya
</Button> </Button>
</Group> </Group>
@@ -196,11 +240,13 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
)} )}
</Stack> </Stack>
</Modal> </Modal>
{ {data?.status == "selesai" && (
data?.status == "selesai" && <ModalSurat
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />) open={openedPreview}
} onClose={() => setOpenedPreview(false)}
surat={data?.idSurat}
/>
)}
<Card <Card
radius="md" radius="md"
@@ -263,7 +309,11 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
> >
{syaratDokumen?.map((v: any) => ( {syaratDokumen?.map((v: any) => (
<List.Item key={v.id}> <List.Item key={v.id}>
<Anchor onClick={() => { setViewImg(v.value) }}> <Anchor
onClick={() => {
setViewImg(v.value);
}}
>
{v.jenis} {v.jenis}
</Anchor> </Anchor>
</List.Item> </List.Item>
@@ -271,8 +321,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
</List> </List>
</Flex> </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} />
@@ -281,82 +329,85 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
<Table withRowBorders={false}> <Table withRowBorders={false}>
<Table.Tbody> <Table.Tbody>
{ {dataText?.map((item: any) => (
dataText?.map((item: any) => ( <Table.Tr key={item.id}>
<Table.Tr key={item.id}> <Table.Td
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td> style={{ whiteSpace: "nowrap", width: "10%" }}
<Table.Td>:</Table.Td> >
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td> {_.upperFirst(item.jenis)}
</Table.Tr> </Table.Td>
)) <Table.Td>:</Table.Td>
} <Table.Td style={{ width: "85%" }}>
{_.upperFirst(item.value)}
</Table.Td>
</Table.Tr>
))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Flex> </Flex>
</Stack> </Stack>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
{ {data?.status === "antrian" ? (
data?.status === "antrian" ? ( <Group justify="center" grow>
<Group justify="center" grow> <Button
<Button disabled={!permissions.includes("pelayanan.antrian.tolak")}
disabled={!permissions.includes("pelayanan.antrian.tolak")} variant="light"
variant="light" onClick={() => {
onClick={() => { setCatModal("tolak");
setCatModal("tolak"); open();
open(); }}
}} >
> Tolak
Tolak </Button>
</Button> <Button
<Button disabled={!permissions.includes("pelayanan.antrian.terima")}
disabled={!permissions.includes("pelayanan.antrian.terima")} variant="filled"
variant="filled" onClick={() => {
onClick={() => { setCatModal("terima");
setCatModal("terima"); open();
open(); }}
}} >
> Terima
Terima </Button>
</Button> </Group>
</Group> ) : data?.status === "diterima" ? (
) : data?.status === "diterima" ? ( <Group justify="center" grow>
<Group justify="center" grow> <Button
<Button disabled={!permissions.includes("pelayanan.diterima.tolak")}
disabled={!permissions.includes("pelayanan.diterima.tolak")} variant="light"
variant="light" onClick={() => {
onClick={() => { setCatModal("tolak");
setCatModal("tolak"); open();
open(); }}
}} >
> Tolak
Tolak </Button>
</Button> <Button
<Button disabled={
disabled={!permissions.includes("pelayanan.diterima.setujui")} !permissions.includes("pelayanan.diterima.setujui")
variant="filled" }
onClick={() => { variant="filled"
setCatModal("terima"); onClick={() => {
open(); setCatModal("terima");
}} open();
> }}
Setujui >
</Button> Setujui
</Group> </Button>
) : </Group>
data?.status === "selesai" ? ) : data?.status === "selesai" ? (
( <Group justify="center" grow>
<Group justify="center" grow> <Button
<Button variant="light"
variant="light" onClick={() => setOpenedPreview(!openedPreview)}
onClick={() => setOpenedPreview(!openedPreview)} >
> Surat
Surat </Button>
</Button> </Group>
</Group> ) : (
) <></>
: <></> )}
}
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Stack> </Stack>
@@ -395,26 +446,25 @@ function DetailDataHistori({ data }: { data: any }) {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{ {data?.map((item: any) => (
data?.map((item: any) => ( <Table.Tr key={item.id}>
<Table.Tr key={item.id}> <Table.Td style={{ whiteSpace: "nowrap" }}>
<Table.Td style={{ whiteSpace: "nowrap" }}> {item.createdAt.toLocaleString("id-ID", {
{ day: "2-digit",
item.createdAt.toLocaleString("id-ID", { month: "short",
day: "2-digit", year: "numeric",
month: "short", hour: "2-digit",
year: "numeric", minute: "2-digit",
hour: "2-digit", hour12: false,
minute: "2-digit", })}
hour12: false </Table.Td>
}) <Table.Td>{item.deskripsi}</Table.Td>
}</Table.Td> <Table.Td>{item.status}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td> <Table.Td style={{ whiteSpace: "nowrap" }}>
<Table.Td>{item.status}</Table.Td> {item.nameUser ? item.nameUser : "-"}
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
)) ))}
}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Stack> </Stack>

View File

@@ -20,7 +20,7 @@ import {
IconClockHour3, IconClockHour3,
IconFileSad, IconFileSad,
IconSearch, IconSearch,
IconUser 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";
@@ -123,7 +123,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
take: "", take: "",
page: page.toString(), page: page.toString(),
}, },
}) }),
); );
useShallowEffect(() => { useShallowEffect(() => {
@@ -131,12 +131,10 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
mutate(); mutate();
}, [status, value]); }, [status, value]);
useShallowEffect(() => { useShallowEffect(() => {
mutate(); mutate();
}, [page]); }, [page]);
useShallowEffect(() => { useShallowEffect(() => {
const unsubscribe = subscribe(state, () => mutate()); const unsubscribe = subscribe(state, () => mutate());
return () => unsubscribe(); return () => unsubscribe();
@@ -189,8 +187,16 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Grid.Col> </Grid.Col>
<Grid.Col span={3}> <Grid.Col span={3}>
<Group justify="flex-end"> <Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text> <Text
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} /> size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group> </Group>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
@@ -204,7 +210,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
</Stack> </Stack>
</Flex> </Flex>
) : ( ) : (
Array.isArray(list) && list?.map((v: any) => ( Array.isArray(list) &&
list?.map((v: any) => (
<Card <Card
key={v.id} key={v.id}
radius="lg" radius="lg"
@@ -266,7 +273,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
Tanggal Ajuan Tanggal Ajuan
</Text> </Text>
</Group> </Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text> <Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">

View File

@@ -16,7 +16,7 @@ import {
Table, Table,
Text, Text,
Textarea, Textarea,
Title Title,
} from "@mantine/core"; } from "@mantine/core";
import { useDisclosure, useShallowEffect } from "@mantine/hooks"; import { useDisclosure, useShallowEffect } from "@mantine/hooks";
import { import {
@@ -58,7 +58,12 @@ export default function DetailPengaduanPage() {
<Grid> <Grid>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} /> <DetailDataPengaduan
data={data?.data?.pengaduan}
onAction={() => {
mutate();
}}
/>
<DetailDataHistori data={data?.data?.history} /> <DetailDataHistori data={data?.data?.history} />
</Stack> </Stack>
</Grid.Col> </Grid.Col>
@@ -70,7 +75,13 @@ export default function DetailPengaduanPage() {
); );
} }
function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: () => void }) { function DetailDataPengaduan({
data,
onAction,
}: {
data: any | null;
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 [openedPreview, setOpenedPreview] = useState(false); const [openedPreview, setOpenedPreview] = useState(false);
@@ -84,7 +95,9 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
setHost(data?.user ?? null); setHost(data?.user ?? null);
if (data?.permissions && Array.isArray(data.permissions)) { if (data?.permissions && Array.isArray(data.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("pengaduan")); const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("pengaduan"),
);
setPermissions(onlySetting); setPermissions(onlySetting);
} }
} }
@@ -95,9 +108,16 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
try { try {
const res = await apiFetch.api.pengaduan["update-status"].post({ const res = await apiFetch.api.pengaduan["update-status"].post({
id: data?.id, id: data?.id,
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : data.status == 'diterima' ? 'dikerjakan' : 'selesai', status:
cat == "tolak"
? "ditolak"
: data.status == "antrian"
? "diterima"
: data.status == "diterima"
? "dikerjakan"
: "selesai",
keterangan: keterangan, keterangan: keterangan,
idUser: host?.id ?? "" idUser: host?.id ?? "",
}); });
if (res?.status === 200) { if (res?.status === 200) {
@@ -115,7 +135,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
type: "error", type: "error",
}); });
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
notification({ notification({
@@ -124,11 +143,10 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
type: "error", type: "error",
}); });
} }
} };
return ( return (
<> <>
{/* MODAL KONFIRMASI */} {/* MODAL KONFIRMASI */}
<Modal <Modal
opened={opened} opened={opened}
@@ -143,24 +161,46 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
</Text> </Text>
<Textarea size="md" minRows={5} value={keterangan} onChange={(e) => setKeterangan(e.target.value)} /> <Textarea
size="md"
minRows={5}
value={keterangan}
onChange={(e) => setKeterangan(e.target.value)}
/>
<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" disabled={keterangan.length < 1} onClick={() => handleKonfirmasi("tolak")}> <Button
variant="filled"
color="red"
disabled={keterangan.length < 1}
onClick={() => handleKonfirmasi("tolak")}
>
Tolak Tolak
</Button> </Button>
</Group> </Group>
</> </>
) : ( ) : (
<> <>
<Text>Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : data.status == 'diterima' ? 'mengerjakan' : 'menyelesaikan'} pengaduan ini?</Text> <Text>
Anda yakin ingin{" "}
{data?.status == "antrian"
? "menerima"
: data.status == "diterima"
? "mengerjakan"
: "menyelesaikan"}{" "}
pengaduan ini?
</Text>
<Group justify="center" grow> <Group justify="center" grow>
<Button variant="light" onClick={close}> <Button variant="light" onClick={close}>
Tidak Tidak
</Button> </Button>
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}> <Button
variant="filled"
color="green"
onClick={() => handleKonfirmasi("terima")}
>
Ya Ya
</Button> </Button>
</Group> </Group>
@@ -169,7 +209,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
</Stack> </Stack>
</Modal> </Modal>
{/* MODAL GAMBAR */} {/* MODAL GAMBAR */}
<ModalFile <ModalFile
open={openedPreview && !_.isEmpty(data?.image)} open={openedPreview && !_.isEmpty(data?.image)}
@@ -259,18 +298,20 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
<IconPhotoScan size={20} /> <IconPhotoScan size={20} />
<Text size="md">Gambar</Text> <Text size="md">Gambar</Text>
</Group> </Group>
{ {data?.image != null && data?.image != "" ? (
data?.image != null && data?.image != "" <Anchor
? href="#"
<Anchor href="#" onClick={() => { setOpenedPreview(true) }}> onClick={() => {
Lihat Gambar setOpenedPreview(true);
</Anchor> }}
: >
<Text size="md" c="white"> Lihat Gambar
- </Anchor>
</Text> ) : (
} <Text size="md" c="white">
-
</Text>
)}
</Flex> </Flex>
</Stack> </Stack>
</Grid.Col> </Grid.Col>
@@ -285,74 +326,76 @@ function DetailDataPengaduan({ data, onAction }: { data: any | null, onAction: (
{_.upperFirst(data?.detail)} {_.upperFirst(data?.detail)}
</Text> </Text>
</Flex> </Flex>
{ {data?.keterangan && (
data?.keterangan && ( <Flex direction={"column"} justify="flex-start">
<Flex direction={"column"} justify="flex-start"> <Group gap="xs">
<Group gap="xs"> <IconInfoTriangle size={20} />
<IconInfoTriangle size={20} /> <Text size="md">Keterangan</Text>
<Text size="md">Keterangan</Text> </Group>
</Group> <Text size="md" c={"white"}>
<Text size="md" c={"white"}> {_.upperFirst(data?.keterangan)}
{_.upperFirst(data?.keterangan)} </Text>
</Text> </Flex>
</Flex> )}
)
}
</Stack> </Stack>
</Grid.Col> </Grid.Col>
<Grid.Col span={12}> <Grid.Col span={12}>
{ {data?.status === "antrian" ? (
data?.status === "antrian" ? ( <Group justify="center" grow>
<Group justify="center" grow> <Button
<Button variant="light"
variant="light" disabled={!permissions.includes("pengaduan.antrian.tolak")}
disabled={!permissions.includes("pengaduan.antrian.tolak")} onClick={() => {
onClick={() => { setCatModal("tolak");
setCatModal("tolak"); open();
open(); }}
}} >
> Tolak
Tolak </Button>
</Button> <Button
<Button variant="filled"
variant="filled" disabled={!permissions.includes("pengaduan.antrian.terima")}
disabled={!permissions.includes("pengaduan.antrian.terima")} onClick={() => {
onClick={() => { setCatModal("terima");
setCatModal("terima"); open();
open(); }}
}} >
> Terima
Terima </Button>
</Button> </Group>
</Group> ) : data?.status === "diterima" ? (
) : data?.status === "diterima" ? ( <Group justify="center" grow>
<Group justify="center" grow> <Button
<Button variant="filled"
variant="filled" disabled={
disabled={!permissions.includes("pengaduan.diterima.dikerjakan")} !permissions.includes("pengaduan.diterima.dikerjakan")
onClick={() => { }
setCatModal("terima"); onClick={() => {
open(); setCatModal("terima");
}} open();
> }}
Kerjakan >
</Button> Kerjakan
</Group> </Button>
) : data?.status === "dikerjakan" ? ( </Group>
<Group justify="center" grow> ) : data?.status === "dikerjakan" ? (
<Button <Group justify="center" grow>
variant="filled" <Button
disabled={!permissions.includes("pengaduan.dikerjakan.selesai")} variant="filled"
onClick={() => { disabled={
setCatModal("terima"); !permissions.includes("pengaduan.dikerjakan.selesai")
open(); }
}} onClick={() => {
> setCatModal("terima");
Selesai open();
</Button> }}
</Group> >
) : <></> Selesai
} </Button>
</Group>
) : (
<></>
)}
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Stack> </Stack>
@@ -391,25 +434,25 @@ function DetailDataHistori({ data }: { data: any }) {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{ {data?.map((item: any) => (
data?.map((item: any) => ( <Table.Tr key={item.id}>
<Table.Tr key={item.id}> <Table.Td style={{ whiteSpace: "nowrap" }}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{ {item.createdAt.toLocaleString("id-ID", {
item.createdAt.toLocaleString("id-ID", { day: "2-digit",
day: "2-digit", month: "short",
month: "short", year: "numeric",
year: "numeric", hour: "2-digit",
hour: "2-digit", minute: "2-digit",
minute: "2-digit", hour12: false,
hour12: false })}
}) </Table.Td>
}</Table.Td> <Table.Td>{item.deskripsi}</Table.Td>
<Table.Td>{item.deskripsi}</Table.Td> <Table.Td>{item.status}</Table.Td>
<Table.Td>{item.status}</Table.Td> <Table.Td style={{ whiteSpace: "nowrap" }}>
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td> {item.nameUser ? item.nameUser : "-"}
</Table.Tr> </Table.Td>
)) </Table.Tr>
} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Stack> </Stack>

View File

@@ -134,7 +134,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
take: "", take: "",
page: page.toString(), page: page.toString(),
}, },
}) }),
); );
useShallowEffect(() => { useShallowEffect(() => {
@@ -175,7 +175,6 @@ function ListPengaduan({ status }: { status: StatusKey }) {
const pageNow = data?.data?.page || 1; const pageNow = data?.data?.page || 1;
const toDate = (d: any) => new Date(d); const toDate = (d: any) => new Date(d);
return ( return (
<Stack gap="xl"> <Stack gap="xl">
<Grid> <Grid>
@@ -197,8 +196,16 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Grid.Col> </Grid.Col>
<Grid.Col span={3}> <Grid.Col span={3}>
<Group justify="flex-end"> <Group justify="flex-end">
<Text size="sm" c="gray.5">{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text> <Text
<Pagination total={totalPage} value={page} onChange={setPage} withPages={false} /> size="sm"
c="gray.5"
>{`${pageSize * (page - 1) + 1} ${Math.min(total, pageSize * page)} of ${total}`}</Text>
<Pagination
total={totalPage}
value={page}
onChange={setPage}
withPages={false}
/>
</Group> </Group>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
@@ -212,7 +219,8 @@ function ListPengaduan({ status }: { status: StatusKey }) {
</Stack> </Stack>
</Flex> </Flex>
) : ( ) : (
Array.isArray(list) && list?.map((v: any) => ( Array.isArray(list) &&
list?.map((v: any) => (
<Card <Card
key={v.id} key={v.id}
radius="lg" radius="lg"
@@ -272,7 +280,13 @@ function ListPengaduan({ status }: { status: StatusKey }) {
Tanggal Aduan Tanggal Aduan
</Text> </Text>
</Group> </Group>
<Text size="md">{toDate(v.createdAt).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })}</Text> <Text size="md">
{toDate(v.createdAt).toLocaleDateString("id-ID", {
day: "numeric",
month: "long",
year: "numeric",
})}
</Text>
</Flex> </Flex>
<Flex direction={"column"} justify="flex-start"> <Flex direction={"column"} justify="flex-start">
<Group gap="xs"> <Group gap="xs">

View File

@@ -5,19 +5,14 @@ import ProfileUser from "@/components/ProfileUser";
import UserRoleSetting from "@/components/UserRoleSetting"; import UserRoleSetting from "@/components/UserRoleSetting";
import UserSetting from "@/components/UserSetting"; import UserSetting from "@/components/UserSetting";
import apiFetch from "@/lib/apiFetch"; import apiFetch from "@/lib/apiFetch";
import { import { Card, Container, Grid, NavLink } from "@mantine/core";
Card,
Container,
Grid,
NavLink
} from "@mantine/core";
import { import {
IconBuildingBank, IconBuildingBank,
IconCategory2, IconCategory2,
IconMailSpark, IconMailSpark,
IconUserCog, IconUserCog,
IconUserScreen, IconUserScreen,
IconUsersGroup IconUsersGroup,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import type { JsonValue } from "generated/prisma/runtime/library"; import type { JsonValue } from "generated/prisma/runtime/library";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@@ -33,7 +28,9 @@ export default function DetailSettingPage() {
async function fetchPermissions() { async function fetchPermissions() {
const { data } = await apiFetch.api.user.find.get(); const { data } = await apiFetch.api.user.find.get();
if (Array.isArray(data?.permissions)) { if (Array.isArray(data?.permissions)) {
const onlySetting = data.permissions.filter((p: any) => p.startsWith("setting")); const onlySetting = data.permissions.filter((p: any) =>
p.startsWith("setting"),
);
setPermissions(onlySetting); setPermissions(onlySetting);
} else { } else {
setPermissions([]); setPermissions([]);
@@ -42,7 +39,6 @@ export default function DetailSettingPage() {
fetchPermissions(); fetchPermissions();
}, []); }, []);
const navItems = [ const navItems = [
{ {
key: "setting.profile", key: "setting.profile",
@@ -85,8 +81,7 @@ export default function DetailSettingPage() {
icon: <IconBuildingBank size={20} />, icon: <IconBuildingBank size={20} />,
label: "Desa", label: "Desa",
description: "Manage desa information", description: "Manage desa information",
} },
]; ];
return ( return (
@@ -104,17 +99,19 @@ export default function DetailSettingPage() {
boxShadow: "0 0 20px rgba(0,255,200,0.08)", boxShadow: "0 0 20px rgba(0,255,200,0.08)",
}} }}
> >
{ {navItems
navItems.filter((item) => permissions.includes(item.key)).map((item) => ( .filter((item) => permissions.includes(item.key))
.map((item) => (
<NavLink <NavLink
key={item.key} key={item.key}
href={'?type=' + item.path} href={"?type=" + item.path}
label={item.label} label={item.label}
leftSection={item.icon} leftSection={item.icon}
active={type === item.path || (!type && item.path === 'profile')} active={
type === item.path || (!type && item.path === "profile")
}
/> />
)) ))}
}
</Card> </Card>
</Grid.Col> </Grid.Col>
<Grid.Col span={9}> <Grid.Col span={9}>
@@ -130,17 +127,47 @@ export default function DetailSettingPage() {
}} }}
> >
{type === "cat-pengaduan" ? ( {type === "cat-pengaduan" ? (
<KategoriPengaduan permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pengaduan"))} /> <KategoriPengaduan
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pengaduan"),
)}
/>
) : type === "cat-pelayanan" ? ( ) : type === "cat-pelayanan" ? (
<KategoriPelayananSurat permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.kategori_pelayanan"))} /> <KategoriPelayananSurat
permissions={permissions.filter(
(p) =>
typeof p === "string" &&
p.startsWith("setting.kategori_pelayanan"),
)}
/>
) : type === "desa" ? ( ) : type === "desa" ? (
<DesaSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.desa"))} /> <DesaSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.desa"),
)}
/>
) : type === "user" ? ( ) : type === "user" ? (
<UserSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user."))} /> <UserSetting
permissions={permissions.filter(
(p) => typeof p === "string" && p.startsWith("setting.user."),
)}
/>
) : type === "role" ? ( ) : type === "role" ? (
<UserRoleSetting permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.user_role"))} /> <UserRoleSetting
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.user_role"),
)}
/>
) : ( ) : (
<ProfileUser permissions={permissions.filter((p) => typeof p === 'string' && p.startsWith("setting.profile"))} /> <ProfileUser
permissions={permissions.filter(
(p) =>
typeof p === "string" && p.startsWith("setting.profile"),
)}
/>
)} )}
</Card> </Card>
</Grid.Col> </Grid.Col>

View File

@@ -37,10 +37,13 @@ export default function DetailWargaPage() {
mutate(); mutate();
}, []); }, []);
return ( return (
<> <>
<LoadingOverlay visible={isLoading} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} /> <LoadingOverlay
visible={isLoading}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 2 }}
/>
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Grid> <Grid>
<Grid.Col span={4}> <Grid.Col span={4}>
@@ -48,18 +51,29 @@ export default function DetailWargaPage() {
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={8}>
<Stack gap={"xl"}> <Stack gap={"xl"}>
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" /> <DetailDataHistori
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" /> data={data?.data?.pengaduan}
kategori="pengaduan"
/>
<DetailDataHistori
data={data?.data?.pelayanan}
kategori="pelayanan"
/>
</Stack> </Stack>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Container> </Container>
</> </>
); );
} }
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) { function DetailDataHistori({
data,
kategori,
}: {
data: any;
kategori: "pengaduan" | "pelayanan";
}) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@@ -85,43 +99,47 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
<Table.Thead> <Table.Thead>
<Table.Tr> <Table.Tr>
<Table.Th>No {_.upperFirst(kategori)}</Table.Th> <Table.Th>No {_.upperFirst(kategori)}</Table.Th>
<Table.Th>{kategori == "pengaduan" ? "Judul" : "Kategori"}</Table.Th> <Table.Th>
{kategori == "pengaduan" ? "Judul" : "Kategori"}
</Table.Th>
<Table.Th>Status</Table.Th> <Table.Th>Status</Table.Th>
<Table.Th></Table.Th> <Table.Th></Table.Th>
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{ {data?.length > 0 ? (
data?.length > 0 ? ( data?.map((item: any, index: number) => (
data?.map((item: any, index: number) => ( <Table.Tr key={index}>
<Table.Tr key={index}> <Table.Td>{item.noPengaduan}</Table.Td>
<Table.Td>{item.noPengaduan}</Table.Td> <Table.Td>
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td> {kategori == "pengaduan" ? item.title : item.category}
<Table.Td>{item.status}</Table.Td> </Table.Td>
<Table.Td> <Table.Td>{item.status}</Table.Td>
<Button <Table.Td>
variant="outline" <Button
onClick={() => { variant="outline"
kategori == "pengaduan" ? onClick={() => {
navigate( kategori == "pengaduan"
? navigate(
`/scr/dashboard/pengaduan/detail?id=${item.id}`, `/scr/dashboard/pengaduan/detail?id=${item.id}`,
) :
navigate(
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
) )
}} : navigate(
> `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
Detail );
</Button> }}
</Table.Td> >
</Table.Tr> Detail
)) </Button>
) : ( </Table.Td>
<Table.Tr>
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
</Table.Tr> </Table.Tr>
) ))
} ) : (
<Table.Tr>
<Table.Td colSpan={4} align="center">
Tidak ada data
</Table.Td>
</Table.Tr>
)}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Stack> </Stack>

View File

@@ -39,7 +39,6 @@ export default function ListWargaPage() {
const pageSize = data?.data?.pageSize || 10; const pageSize = data?.data?.pageSize || 10;
const pageNow = data?.data?.page || 1; const pageNow = data?.data?.page || 1;
useShallowEffect(() => { useShallowEffect(() => {
setPages(1); setPages(1);
mutate(); mutate();
@@ -49,7 +48,6 @@ export default function ListWargaPage() {
mutate(); mutate();
}, [pages]); }, [pages]);
return ( return (
<Container size="xl" py="xl" w={"100%"}> <Container size="xl" py="xl" w={"100%"}>
<Card <Card
@@ -82,7 +80,12 @@ export default function ListWargaPage() {
/> />
<Group> <Group>
<Text size="sm">{`${pageSize * (pages - 1) + 1} ${Math.min(total, pageSize * pages)} of ${total}`}</Text> <Text size="sm">{`${pageSize * (pages - 1) + 1} ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
<Pagination total={totalPage} value={pages} onChange={setPages} withPages={false} /> <Pagination
total={totalPage}
value={pages}
onChange={setPages}
withPages={false}
/>
</Group> </Group>
</Flex> </Flex>
<Divider my={0} /> <Divider my={0} />
@@ -95,32 +98,33 @@ export default function ListWargaPage() {
</Table.Tr> </Table.Tr>
</Table.Thead> </Table.Thead>
<Table.Tbody> <Table.Tbody>
{ {Array.isArray(list) && list?.length === 0 ? (
Array.isArray(list) && list?.length === 0 ? ( <Table.Tr>
<Table.Tr> <Table.Td colSpan={3} align="center">
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td> Tidak ada data
</Table.Td>
</Table.Tr>
) : (
Array.isArray(list) &&
list?.map((item, i) => (
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr> </Table.Tr>
) : ( ))
Array.isArray(list) && list?.map((item, i) => ( )}
<Table.Tr key={i}>
<Table.Td>{item.name}</Table.Td>
<Table.Td w={250}>{item.phone}</Table.Td>
<Table.Td w={150}>
<Button
variant="outline"
onClick={() => {
navigate(
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
);
}}
>
Detail
</Button>
</Table.Td>
</Table.Tr>
))
)
}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
</Stack> </Stack>

View File

@@ -102,6 +102,24 @@ const PelayananRoute = new Elysia({
description: `tool untuk delete kategori pelayanan surat` description: `tool untuk delete kategori pelayanan surat`
} }
}) })
.get("/category/detail", async ({ query }) => {
const { id } = query
const data = await prisma.categoryPelayanan.findUnique({
where:{
id
}
})
return data
}, {
query: t.Object({
id: t.String({ minLength: 1, error: "id harus diisi" }),
}),
detail: {
summary: "Detail Kategori Pelayanan Surat by ID",
description: `tool untuk mendapatkan detail kategori pelayanan surat berdasarkan id`,
}
})
// --- PELAYANAN SURAT --- // --- PELAYANAN SURAT ---