Compare commits
101 Commits
amalia/28-
...
amalia/17-
| Author | SHA1 | Date | |
|---|---|---|---|
| 84161db7f2 | |||
| a1766538b2 | |||
| 7c6e4ac9eb | |||
| d8cf7833a9 | |||
| a13e51a724 | |||
| c585d2481d | |||
| 18b541116a | |||
| e2d523d535 | |||
| 18d3b40700 | |||
| 719ba0186b | |||
| baf00b1ba8 | |||
| 026f74cc44 | |||
| dfc5c9144f | |||
| 2badded9c3 | |||
| f91e2dd87b | |||
| 6fb6ab9750 | |||
| 5524f72712 | |||
| 11a78d7371 | |||
| 93de9ebe9a | |||
| 3baba059ab | |||
| 12eb71b96d | |||
| dcd072034c | |||
| 4cef5148ad | |||
| ee27813da7 | |||
| 7a6ea5b13d | |||
| 29f6ecfd23 | |||
| 8d28e7ae6a | |||
| d6882d4b3a | |||
| b7f0f4da48 | |||
| 286c989bcf | |||
| 031e408640 | |||
| c797d1fc46 | |||
| 6f6905a414 | |||
| 91e5f6a77e | |||
| 3f567b57b2 | |||
| c98cfd21ce | |||
| fdf7b0a13f | |||
| d76a702d2d | |||
| 6bc6a9d357 | |||
| dee32b8cfd | |||
| ff0b0273bf | |||
| f8dcffa9c5 | |||
| 20e3056e04 | |||
| 84c9f405d6 | |||
| 22597c0159 | |||
| 0f9af404e1 | |||
| 676edaa22b | |||
| 7f6f495eaa | |||
| b5af41b07d | |||
| 6428f5084e | |||
| bfc292ec6c | |||
| 3b71976863 | |||
| 5680466c98 | |||
| 270f3687a3 | |||
| f5cc45937c | |||
| 5b4164b151 | |||
| 225c58b346 | |||
| b8b3aed86e | |||
| fc530399dd | |||
| 281e34ea69 | |||
| f928fc504f | |||
| 4fb98d0480 | |||
| bfb33e2105 | |||
| 2579714000 | |||
| d69189cf7d | |||
| 20e24a03aa | |||
| c256f4b729 | |||
| 9430ad3728 | |||
| c6c3ba95f8 | |||
|
|
3c58230c3a | ||
| d22b4b973f | |||
| 700fbe3bd7 | |||
| 9b7a61e134 | |||
| 2d376663bb | |||
| 7c669f3494 | |||
| 0ed9dc6ddd | |||
| b9984c6337 | |||
| 48a7d43713 | |||
| 6a52d10faa | |||
| 2b94684570 | |||
| cc7c8eb704 | |||
| 4996da4189 | |||
| c32cce838f | |||
| 5af9b720ca | |||
| 35618bb438 | |||
| eee8aadb1a | |||
| 1f95c7d7d8 | |||
| 23df516aad | |||
| 70175cedc6 | |||
| f52f5f87ca | |||
| ea17357638 | |||
| 9c7c9d8595 | |||
| 5ecf264155 | |||
| 4cc28c4311 | |||
| c25e5eeba0 | |||
| 574603e290 | |||
| ba76eb5e59 | |||
| 6ae83ec19c | |||
| ba0414a99c | |||
| 4dd66dbd9a | |||
| dff1aa61c5 |
3
kirim.sh
Normal file
3
kirim.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
curl -X POST https://cld-dkr-prod-jenna-mcp.wibudev.com/api/pengaduan/upload-file-form-data \
|
||||
-H "Accept: application/json" \
|
||||
-F "file=@image.png"
|
||||
@@ -14,6 +14,7 @@ import FormSuratKeteranganBelumKawin from "./pages/darmasaba/form_surat_keterang
|
||||
import FormKeteranganKelahiran from "./pages/darmasaba/form_keterangan_kelahiran";
|
||||
import FormSuratKeteranganTempatUsaha from "./pages/darmasaba/form_surat_keterangan_tempat_usaha";
|
||||
import FormSuratKeteranganKelakuanBaik from "./pages/darmasaba/form_surat_keterangan_kelakuan_baik";
|
||||
import Surat from "./pages/darmasaba/surat";
|
||||
import Home from "./pages/Home";
|
||||
import CredentialPage from "./pages/scr/dashboard/credential/credential_page";
|
||||
import DashboardHome from "./pages/scr/dashboard/dashboard_home";
|
||||
@@ -84,6 +85,7 @@ export default function AppRoutes() {
|
||||
path="/darmasaba/surat-keterangan-kelakuan-baik"
|
||||
element={<FormSuratKeteranganKelakuanBaik />}
|
||||
/>
|
||||
<Route path="/darmasaba/surat" element={<Surat />} />
|
||||
</Route>
|
||||
<Route path="/" element={<Home />} />
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ const clientRoutes = {
|
||||
"/darmasaba/keterangan-kelahiran": "/darmasaba/keterangan-kelahiran",
|
||||
"/darmasaba/surat-keterangan-tempat-usaha": "/darmasaba/surat-keterangan-tempat-usaha",
|
||||
"/darmasaba/surat-keterangan-kelakuan-baik": "/darmasaba/surat-keterangan-kelakuan-baik",
|
||||
"/darmasaba/surat": "/darmasaba/surat",
|
||||
"/": "/",
|
||||
"/scr": "/scr",
|
||||
"/scr/dashboard": "/scr/dashboard",
|
||||
|
||||
@@ -1,98 +1,101 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { Card, Flex, Grid, Group, Stack, Text } from "@mantine/core";
|
||||
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";
|
||||
|
||||
export default function DashboardCountData() {
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.dashboard.count.get()
|
||||
);
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
apiFetch.api.dashboard.count.get(),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconMessageReport size={28} />}
|
||||
label="Pengaduan Hari Ini"
|
||||
value={String(data?.data?.pengaduan?.today)}
|
||||
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
|
||||
color={(data?.data?.pengaduan?.kenaikan || 0) > 0 ? "teal" : "gray"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconFileCertificate size={28} />}
|
||||
label="Pengajuan Surat Hari Ini"
|
||||
value={String(data?.data?.pelayanan?.today)}
|
||||
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
|
||||
color={(data?.data?.pelayanan?.kenaikan || 0) > 0 ? "teal" : "gray"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconUsers size={28} />}
|
||||
label="Warga"
|
||||
value={String(data?.data?.warga)}
|
||||
color="blue"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
return (
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconMessageReport size={28} />}
|
||||
label="Pengaduan Hari Ini"
|
||||
value={String(data?.data?.pengaduan?.today)}
|
||||
change={String(data?.data?.pengaduan?.kenaikan) + "%"}
|
||||
color={"gray"}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconFileCertificate size={28} />}
|
||||
label="Pengajuan Surat Hari Ini"
|
||||
value={String(data?.data?.pelayanan?.today)}
|
||||
change={String(data?.data?.pelayanan?.kenaikan) + "%"}
|
||||
color="gray"
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<MetricCard
|
||||
icon={<IconUsers size={28} />}
|
||||
label="Warga"
|
||||
value={String(data?.data?.warga)}
|
||||
color="blue"
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function MetricCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
change,
|
||||
color,
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
change,
|
||||
color,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
color: string;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
change?: string;
|
||||
color: string;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="md"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
|
||||
}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Group gap={6}>
|
||||
{icon}
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Text fw={600} size="xl" c="gray.0">
|
||||
{value}
|
||||
</Text>
|
||||
{change && (
|
||||
<Text size="sm" c={color}>
|
||||
{change}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="md"
|
||||
withBorder
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(145deg, rgba(30,30,30,0.95), rgba(55,55,55,0.9))",
|
||||
borderColor: "rgba(100,100,100,0.2)",
|
||||
transition: "transform 0.15s ease, box-shadow 0.15s ease",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.boxShadow = "0 0 10px rgba(0,255,200,0.2)")
|
||||
}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.boxShadow = "none")}
|
||||
>
|
||||
<Stack gap={6}>
|
||||
<Group gap={6}>
|
||||
{icon}
|
||||
<Text size="sm" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Text fw={600} size="xl" c="gray.0">
|
||||
{value}
|
||||
</Text>
|
||||
{change && (
|
||||
<Text size="sm" c={color}>
|
||||
{change}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,77 +7,71 @@ import { useEffect, useState } from "react";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function DashboardGrafik() {
|
||||
const [options, setOptions] = useState<EChartsOption>({});
|
||||
const { data, mutate, isLoading } = useSWR(
|
||||
"grafik-dashboard",
|
||||
async () => {
|
||||
return apiFetch.api.dashboard.grafik.get().then(res => res.data);
|
||||
}
|
||||
);
|
||||
const [options, setOptions] = useState<EChartsOption>({});
|
||||
const { data, mutate, isLoading } = useSWR("grafik-dashboard", async () => {
|
||||
return apiFetch.api.dashboard.grafik.get().then((res) => res.data);
|
||||
});
|
||||
|
||||
const loadData = () => {
|
||||
if (!data) return;
|
||||
const option: EChartsOption = {
|
||||
darkMode: true,
|
||||
animation: true,
|
||||
legend: {
|
||||
textStyle: { color: "#fff" }
|
||||
},
|
||||
tooltip: {},
|
||||
dataset: {
|
||||
dimensions: data.dimensions,
|
||||
source: data.source
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: { color: "#fff" }
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
minInterval: 1,
|
||||
axisLabel: { color: "#fff" }
|
||||
},
|
||||
color: ["#1abc9c", "#10816aff"],
|
||||
series: [
|
||||
{ type: "bar" },
|
||||
{ type: "bar" }
|
||||
]
|
||||
};
|
||||
const loadData = () => {
|
||||
if (!data) return;
|
||||
const option: EChartsOption = {
|
||||
darkMode: true,
|
||||
animation: true,
|
||||
legend: {
|
||||
textStyle: { color: "#fff" },
|
||||
},
|
||||
tooltip: {},
|
||||
dataset: {
|
||||
dimensions: data.dimensions,
|
||||
source: data.source,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
minInterval: 1,
|
||||
axisLabel: { color: "#fff" },
|
||||
},
|
||||
color: ["#1abc9c", "#10816aff"],
|
||||
series: [{ type: "bar" }, { type: "bar" }],
|
||||
};
|
||||
|
||||
setOptions(option);
|
||||
};
|
||||
setOptions(option);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) loadData();
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
if (data) loadData();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
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" justify="space-between">
|
||||
<Flex direction={"column"}>
|
||||
<Title order={4} c="gray.0">
|
||||
Grafik Pengaduan dan Pelayanan Surat
|
||||
</Title>
|
||||
<Text size="sm">7 Hari Terakhir</Text>
|
||||
</Flex>
|
||||
<IconChartBar size={20} color="gray" />
|
||||
</Flex>
|
||||
<Divider my="xs" />
|
||||
<Stack gap="sm">
|
||||
<EChartsReact style={{ height: 400 }} option={options} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Card
|
||||
radius="lg"
|
||||
p="xl"
|
||||
withBorder
|
||||
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" justify="space-between">
|
||||
<Flex direction={"column"}>
|
||||
<Title order={4} c="gray.0">
|
||||
Grafik Pengaduan dan Pelayanan Surat
|
||||
</Title>
|
||||
<Text size="sm">7 Hari Terakhir</Text>
|
||||
</Flex>
|
||||
<IconChartBar size={20} color="gray" />
|
||||
</Flex>
|
||||
<Divider my="xs" />
|
||||
<Stack gap="sm">
|
||||
<EChartsReact style={{ height: 400 }} option={options} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,133 +1,210 @@
|
||||
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 { useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function DashboardLastData() {
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("last-update", async () => {
|
||||
const res = await apiFetch.api.dashboard["last-update"].get();
|
||||
return res.data
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("last-update", async () => {
|
||||
const res = await apiFetch.api.dashboard["last-update"].get();
|
||||
return res.data;
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
useShallowEffect(() => {
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<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 pelayanan surat
|
||||
</Title>
|
||||
<Button variant="subtle" size="xs" radius="md" onClick={() => navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)}>View All</Button>
|
||||
</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>
|
||||
);
|
||||
<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 pelayanan surat
|
||||
</Title>
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="xs"
|
||||
radius="md"
|
||||
onClick={() =>
|
||||
navigate(`/scr/dashboard/pelayanan-surat/list-pelayanan`)
|
||||
}
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</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' }) {
|
||||
const navigate = useNavigate();
|
||||
function PengaduanSection({
|
||||
id,
|
||||
nomer,
|
||||
judul,
|
||||
status,
|
||||
updated,
|
||||
kategori,
|
||||
}: {
|
||||
id: string;
|
||||
nomer: string;
|
||||
judul: string;
|
||||
status: string;
|
||||
updated: string;
|
||||
kategori: "pengaduan" | "pelayanan";
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
gap="xs"
|
||||
onClick={() => navigate(kategori == "pelayanan" ? `/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${id}` : `/scr/dashboard/pengaduan/detail?id=${id}`)}
|
||||
return (
|
||||
<Stack
|
||||
gap="xs"
|
||||
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"}>
|
||||
<Text size="md" c="gray.2" lineClamp={1}>
|
||||
{judul}
|
||||
</Text>
|
||||
<Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
#{nomer} ∙ {updated}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Tooltip label={status}>
|
||||
<Badge size="xs" circle color={
|
||||
status === "diterima"
|
||||
? "green"
|
||||
: status === "ditolak"
|
||||
? "red"
|
||||
: status === "selesai"
|
||||
? "blue"
|
||||
: status === "dikerjakan"
|
||||
? "gray"
|
||||
: "yellow"
|
||||
} />
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
<Flex direction={"column"}>
|
||||
<Text size="md" c="gray.2" lineClamp={1}>
|
||||
{judul}
|
||||
</Text>
|
||||
<Group>
|
||||
<Text size="sm" c="dimmed">
|
||||
#{nomer} ∙ {updated}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Tooltip label={status}>
|
||||
<Badge
|
||||
size="xs"
|
||||
circle
|
||||
color={
|
||||
status === "diterima"
|
||||
? "green"
|
||||
: status === "ditolak"
|
||||
? "red"
|
||||
: status === "selesai"
|
||||
? "blue"
|
||||
: status === "dikerjakan"
|
||||
? "gray"
|
||||
: "yellow"
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Stack,
|
||||
Table,
|
||||
Title,
|
||||
Tooltip
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit } from "@tabler/icons-react";
|
||||
@@ -23,11 +23,15 @@ import useSWR from "swr";
|
||||
import ModalFile from "./ModalFile";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function DesaSetting({ permissions }: { permissions: JsonValue[] }) {
|
||||
export default function DesaSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(false);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [img, setImg] = useState<any>()
|
||||
const [img, setImg] = useState<any>();
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
const [viewImg, setViewImg] = useState("");
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
@@ -51,13 +55,19 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
|
||||
let finalData = { ...dataEdit }; // ← buffer data terbaru
|
||||
|
||||
if (dataEdit.name === "TTD") {
|
||||
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({ file: dataEdit.value, folder: "lainnya" });
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({ file: img, folder: "lainnya" });
|
||||
const oldImg = await apiFetch.api.pengaduan["delete-image"].post({
|
||||
file: dataEdit.value,
|
||||
folder: "lainnya",
|
||||
});
|
||||
const resImg = await apiFetch.api.pengaduan.upload.post({
|
||||
file: img,
|
||||
folder: "lainnya",
|
||||
});
|
||||
|
||||
if (resImg.status === 200) {
|
||||
finalData = {
|
||||
...finalData,
|
||||
value: resImg.data?.filename || ""
|
||||
value: resImg.data?.filename || "",
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (res.status === 200) {
|
||||
@@ -100,8 +109,11 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function chooseEdit({ data }: { data: { id: string; value: string; name: string }; }) {
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; value: string; name: string };
|
||||
}) {
|
||||
setDataEdit(data);
|
||||
open();
|
||||
}
|
||||
@@ -133,31 +145,27 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
|
||||
overlayProps={{ backgroundOpacity: 0.55, blur: 3 }}
|
||||
>
|
||||
<Stack gap="ld">
|
||||
{
|
||||
dataEdit.name == "TTD"
|
||||
?
|
||||
(
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<FileInput
|
||||
clearable
|
||||
placeholder="Upload TTD"
|
||||
accept="image/*"
|
||||
onChange={(e) => { setImg(e) }}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)
|
||||
:
|
||||
(
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input
|
||||
value={dataEdit.value}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "value", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)
|
||||
}
|
||||
{dataEdit.name == "TTD" ? (
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<FileInput
|
||||
clearable
|
||||
placeholder="Upload TTD"
|
||||
accept="image/*"
|
||||
onChange={(e) => {
|
||||
setImg(e);
|
||||
}}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
) : (
|
||||
<Input.Wrapper label={dataEdit.name}>
|
||||
<Input
|
||||
value={dataEdit.value}
|
||||
onChange={(e) =>
|
||||
onValidation({ kat: "value", value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</Input.Wrapper>
|
||||
)}
|
||||
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
@@ -203,21 +211,33 @@ export default function DesaSetting({ permissions }: { permissions: JsonValue[]
|
||||
<Table.Tr key={v.id}>
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
{
|
||||
v.name == "TTD"
|
||||
?
|
||||
v.value ?
|
||||
<Anchor href="#" onClick={() => { setViewImg(v.value); setOpenedPreview(true); }} underline="always">
|
||||
Lihat
|
||||
</Anchor>
|
||||
:
|
||||
"-"
|
||||
:
|
||||
v.value
|
||||
}
|
||||
{v.name == "TTD" ? (
|
||||
v.value ? (
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={() => {
|
||||
setViewImg(v.value);
|
||||
setOpenedPreview(true);
|
||||
}}
|
||||
underline="always"
|
||||
>
|
||||
Lihat
|
||||
</Anchor>
|
||||
) : (
|
||||
"-"
|
||||
)
|
||||
) : (
|
||||
v.value
|
||||
)}
|
||||
</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
|
||||
variant="light"
|
||||
size="sm"
|
||||
|
||||
@@ -23,7 +23,11 @@ import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPelayananSurat({ permissions }: { permissions: JsonValue[] }) {
|
||||
export default function KategoriPelayananSurat({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [openedDetail, { open: openDetail, close: closeDetail }] =
|
||||
@@ -53,7 +57,6 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
@@ -535,19 +538,17 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pelayanan Surat
|
||||
</Title>
|
||||
{
|
||||
permissions.includes("setting.kategori_pelayanan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{permissions.includes("setting.kategori_pelayanan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pelayanan Surat">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -578,7 +579,15 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
|
||||
<IconEye size={20} />
|
||||
</ActionIcon>
|
||||
</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
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -587,12 +596,24 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
|
||||
setDataChoose(v);
|
||||
open();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pelayanan.edit")}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pelayanan.edit",
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</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
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -602,7 +623,11 @@ export default function KategoriPelayananSurat({ permissions }: { permissions: J
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pelayanan.delete")}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pelayanan.delete",
|
||||
)
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -20,7 +20,11 @@ import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function KategoriPengaduan({ permissions }: { permissions: JsonValue[] }) {
|
||||
export default function KategoriPengaduan({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
@@ -294,19 +298,17 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
|
||||
<Title order={4} c="gray.2">
|
||||
Kategori Pengaduan
|
||||
</Title>
|
||||
{
|
||||
permissions.includes("setting.kategori_pengaduan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{permissions.includes("setting.kategori_pengaduan.tambah") && (
|
||||
<Tooltip label="Tambah Kategori Pengaduan">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -323,18 +325,38 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
|
||||
<Table.Td>{v.name}</Table.Td>
|
||||
<Table.Td>
|
||||
<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
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
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} />
|
||||
</ActionIcon>
|
||||
</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
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -344,7 +366,11 @@ export default function KategoriPengaduan({ permissions }: { permissions: JsonVa
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes("setting.kategori_pengaduan.delete") || v.id == "lainnya"}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.kategori_pengaduan.delete",
|
||||
) || v.id == "lainnya"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -3,92 +3,100 @@ import { Flex, Image, Loader, Modal } from "@mantine/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function ModalFile({ open, onClose, folder, 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);
|
||||
export default function ModalFile({
|
||||
open,
|
||||
onClose,
|
||||
folder,
|
||||
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(() => {
|
||||
if (open && fileName) {
|
||||
loadImage();
|
||||
useEffect(() => {
|
||||
if (open && fileName) {
|
||||
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 () => {
|
||||
try {
|
||||
setViewFile("");
|
||||
setLoading(true);
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
onClose();
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// 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",
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,122 +18,129 @@ import SKTidakMampu from "./surat/SKTidakMampu";
|
||||
import SKUsaha from "./surat/SKUsaha";
|
||||
import SKYatim from "./surat/SKYatimPiatu";
|
||||
|
||||
export default function ModalSurat({ open, onClose, surat }: { open: boolean, onClose: () => void, surat: string }) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
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,
|
||||
},
|
||||
}),
|
||||
);
|
||||
export default function ModalSurat({
|
||||
open,
|
||||
onClose,
|
||||
surat,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
surat: string;
|
||||
}) {
|
||||
const A4Style = {
|
||||
width: "210mm",
|
||||
height: "297mm",
|
||||
padding: "20mm",
|
||||
background: "#fff",
|
||||
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(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
useShallowEffect(() => {
|
||||
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 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 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 pageWidth = 210; // A4 width mm
|
||||
const pageHeight = 297; // A4 height mm
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
|
||||
const imgWidth = pageWidth;
|
||||
const imgHeight = (canvas.height * pageWidth) / canvas.width;
|
||||
pdf.addImage(imgData, "JPEG", 0, 0, imgWidth, imgHeight);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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>
|
||||
<Flex gap={8}>
|
||||
<ActionIcon size={32} variant="default">
|
||||
<IconDownload size={20} onClick={downloadPDF} />
|
||||
</ActionIcon>
|
||||
|
||||
<Flex gap={8}>
|
||||
<ActionIcon size={32} variant="default">
|
||||
<IconDownload size={20} onClick={downloadPDF} />
|
||||
</ActionIcon>
|
||||
|
||||
<ActionIcon size={32} variant="default" onClick={onClose}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{
|
||||
data && data.data
|
||||
? data.data.surat.idCategory == "skusaha"
|
||||
? <SKUsaha data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelahiran"
|
||||
? <SKKelahiran data={data.data} />
|
||||
: data.data.surat.idCategory == "skkelakuanbaik"
|
||||
? <SKKelakuanBaik data={data.data} />
|
||||
: data.data.surat.idCategory == "skpenghasilan"
|
||||
? <SKPenghasilan data={data.data} />
|
||||
: data.data.surat.idCategory == "sktidakmampu"
|
||||
? <SKTidakMampu data={data.data} />
|
||||
: data.data.surat.idCategory == "skyatimpiatu"
|
||||
? <SKYatim data={data.data} />
|
||||
: data.data.surat.idCategory == "skdomisiliorganisasi"
|
||||
? <SKDomisiliOrganisasi data={data.data} />
|
||||
: data.data.surat.idCategory == "skbedabiodata"
|
||||
? <SKBedaBiodataDiri 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<ActionIcon size={32} variant="default" onClick={onClose}>
|
||||
<IconX size={20} />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
>
|
||||
<div ref={hiddenRef} style={A4Style}>
|
||||
{data && data.data ? (
|
||||
data.data.surat.idCategory == "skusaha" ? (
|
||||
<SKUsaha data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelahiran" ? (
|
||||
<SKKelahiran data={data.data} />
|
||||
) : data.data.surat.idCategory == "skkelakuanbaik" ? (
|
||||
<SKKelakuanBaik data={data.data} />
|
||||
) : data.data.surat.idCategory == "skpenghasilan" ? (
|
||||
<SKPenghasilan data={data.data} />
|
||||
) : data.data.surat.idCategory == "sktidakmampu" ? (
|
||||
<SKTidakMampu data={data.data} />
|
||||
) : data.data.surat.idCategory == "skyatimpiatu" ? (
|
||||
<SKYatim data={data.data} />
|
||||
) : data.data.surat.idCategory == "skdomisiliorganisasi" ? (
|
||||
<SKDomisiliOrganisasi data={data.data} />
|
||||
) : data.data.surat.idCategory == "skbedabiodata" ? (
|
||||
<SKBedaBiodataDiri 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,65 +3,66 @@ import { Anchor, Flex, Stack, Text } from "@mantine/core";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Node {
|
||||
label: string;
|
||||
children: any;
|
||||
actions: string[];
|
||||
label: string;
|
||||
children: any;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
function RenderNode({ node }: { node: Node }) {
|
||||
const sub = Object.values(node.children || {});
|
||||
const sub = Object.values(node.children || {});
|
||||
|
||||
return (
|
||||
<Stack pl="md" gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">- {node.label}</Text>
|
||||
return (
|
||||
<Stack pl="md" gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">- {node.label}</Text>
|
||||
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode key={i} node={child} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode key={i} node={child} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderNode2({ node }: { node: Node }) {
|
||||
const sub = Object.values(node.children || {});
|
||||
const sub = Object.values(node.children || {});
|
||||
|
||||
return (
|
||||
<Flex direction={"row"} wrap={'wrap'} gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">{node.label},</Text>
|
||||
return (
|
||||
<Flex direction={"row"} wrap={"wrap"} gap={6}>
|
||||
{/* Title */}
|
||||
<Text size="sm">{node.label},</Text>
|
||||
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode2 key={i} node={child} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
{/* Children */}
|
||||
{sub.map((child: any, i) => (
|
||||
<RenderNode2 key={i} node={child} />
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PermissionRole({ permissions }: { permissions: string[] }) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
if (!permissions?.length) return <Text c="dimmed">-</Text>;
|
||||
export default function PermissionRole({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: string[];
|
||||
}) {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
if (!permissions?.length) return <Text c="dimmed">-</Text>;
|
||||
|
||||
const groups = groupPermissions(permissions);
|
||||
const rootNodes = Object.values(groups);
|
||||
const groups = groupPermissions(permissions);
|
||||
const rootNodes = Object.values(groups);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{
|
||||
showAll ?
|
||||
rootNodes.map((node: any, idx) => (
|
||||
<RenderNode key={idx} node={node} />
|
||||
))
|
||||
:
|
||||
rootNodes.slice(0, 2).map((node: any, idx) => (
|
||||
<RenderNode2 key={idx} node={node} />
|
||||
))
|
||||
}
|
||||
<Anchor size="xs" onClick={() => setShowAll(!showAll)} >
|
||||
{showAll ? "View less" : "View more"}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
);
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
{showAll
|
||||
? rootNodes.map((node: any, idx) => (
|
||||
<RenderNode key={idx} node={node} />
|
||||
))
|
||||
: rootNodes
|
||||
.slice(0, 2)
|
||||
.map((node: any, idx) => <RenderNode2 key={idx} node={node} />)}
|
||||
<Anchor size="xs" onClick={() => setShowAll(!showAll)}>
|
||||
{showAll ? "View less" : "View more"}
|
||||
</Anchor>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,177 +1,183 @@
|
||||
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 { useState } from "react";
|
||||
|
||||
interface Node {
|
||||
label: string;
|
||||
key: string;
|
||||
children?: Node[];
|
||||
label: string;
|
||||
key: string;
|
||||
children?: Node[];
|
||||
}
|
||||
|
||||
export default function PermissionTree({
|
||||
selected,
|
||||
onChange,
|
||||
selected,
|
||||
onChange,
|
||||
}: {
|
||||
selected: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
selected: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
}) {
|
||||
// Ambil semua child dari node
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
|
||||
// Ambil semua child dari node
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
|
||||
|
||||
function toggleNode(label: string) {
|
||||
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
|
||||
}
|
||||
function toggleNode(label: string) {
|
||||
setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
|
||||
}
|
||||
|
||||
function getAllChildKeys(node: Node): string[] {
|
||||
let result: string[] = [];
|
||||
if (node.children) {
|
||||
node.children.forEach((c) => {
|
||||
result.push(c.key);
|
||||
result = [...result, ...getAllChildKeys(c)];
|
||||
});
|
||||
function getAllChildKeys(node: Node): string[] {
|
||||
let result: string[] = [];
|
||||
if (node.children) {
|
||||
node.children.forEach((c) => {
|
||||
result.push(c.key);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
} else {
|
||||
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
|
||||
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
|
||||
return updateParent(next, getParentKey(parentKey));
|
||||
}
|
||||
// Rekursif naik ke atas
|
||||
return updateParent(next, getParentKey(parentKey));
|
||||
}
|
||||
|
||||
// dapatkan child dari string key
|
||||
function findAllChildKeysFromKey(parentKey: string) {
|
||||
const list: string[] = [];
|
||||
// dapatkan child dari string key
|
||||
function findAllChildKeysFromKey(parentKey: string) {
|
||||
const list: string[] = [];
|
||||
|
||||
function traverse(nodes: Node[]) {
|
||||
nodes.forEach((n) => {
|
||||
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
|
||||
list.push(n.key);
|
||||
}
|
||||
if (n.children) traverse(n.children);
|
||||
});
|
||||
}
|
||||
function traverse(nodes: Node[]) {
|
||||
nodes.forEach((n) => {
|
||||
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
|
||||
list.push(n.key);
|
||||
}
|
||||
if (n.children) traverse(n.children);
|
||||
});
|
||||
}
|
||||
|
||||
traverse(permissionConfig.menus);
|
||||
return list;
|
||||
}
|
||||
traverse(permissionConfig.menus);
|
||||
return list;
|
||||
}
|
||||
|
||||
const RenderMenu = ({ menu }: { menu: Node }) => {
|
||||
const hasChild = menu.children && menu.children.length > 0;
|
||||
const open = openNodes[menu.label] ?? false;
|
||||
const childKeys = getAllChildKeys(menu);
|
||||
const isChecked = selected.includes(menu.key);
|
||||
const isIndeterminate =
|
||||
!isChecked &&
|
||||
selected.some(
|
||||
(x) =>
|
||||
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>
|
||||
const RenderMenu = ({ menu }: { menu: Node }) => {
|
||||
const hasChild = menu.children && menu.children.length > 0;
|
||||
const open = openNodes[menu.label] ?? false;
|
||||
const childKeys = getAllChildKeys(menu);
|
||||
const isChecked = selected.includes(menu.key);
|
||||
const isIndeterminate =
|
||||
!isChecked &&
|
||||
selected.some(
|
||||
(x) => typeof x === "string" && x.startsWith(menu.key + "."),
|
||||
);
|
||||
};
|
||||
|
||||
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} />
|
||||
))}
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,11 @@ import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useEffect, useState } from "react";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function ProfileUser({ permissions }: { permissions: JsonValue[] }) {
|
||||
export default function ProfileUser({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [openedPassword, setOpenedPassword] = useState(false);
|
||||
const [pwdBaru, setPwdBaru] = useState("");
|
||||
@@ -127,21 +131,17 @@ export default function ProfileUser({ permissions }: { permissions: JsonValue[]
|
||||
Profile Pengguna
|
||||
</Title>
|
||||
<Group gap="md">
|
||||
{
|
||||
permissions.includes("setting.profile.edit") && (
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{permissions.includes("setting.profile.edit") && (
|
||||
<Button variant="light" onClick={() => setOpened(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{
|
||||
permissions.includes("setting.profile.password") && (
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{permissions.includes("setting.profile.password") && (
|
||||
<Button variant="light" onClick={() => setOpenedPassword(true)}>
|
||||
Ubah Password
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip
|
||||
ActionIcon,
|
||||
Button,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Modal,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { IconEdit, IconPlus, IconTrash } from "@tabler/icons-react";
|
||||
@@ -24,404 +24,449 @@ import PermissionRole from "./PermissionRole";
|
||||
import PermissionTree from "./PermissionTree";
|
||||
|
||||
interface MenuNode {
|
||||
key: string;
|
||||
label: string;
|
||||
default: boolean;
|
||||
children?: MenuNode[];
|
||||
key: string;
|
||||
label: string;
|
||||
default: boolean;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
export default function UserRoleSetting({ permissions }: { permissions: JsonValue[] }) {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const {
|
||||
data: dataRole,
|
||||
mutate: mutateRole,
|
||||
isLoading: isLoadingRole,
|
||||
} = useSWR("user-role", () => apiFetch.api.user.role.get());
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("role-list", () =>
|
||||
apiFetch.api.user.role.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const listRole = dataRole?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
permissions: false,
|
||||
});
|
||||
export default function UserRoleSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [openedDelete, { open: openDelete, close: closeDelete }] =
|
||||
useDisclosure(false);
|
||||
const [dataDelete, setDataDelete] = useState("");
|
||||
const {
|
||||
data: dataRole,
|
||||
mutate: mutateRole,
|
||||
isLoading: isLoadingRole,
|
||||
} = useSWR("user-role", () => apiFetch.api.user.role.get());
|
||||
const [openedTambah, { open: openTambah, close: closeTambah }] =
|
||||
useDisclosure(false);
|
||||
const { data, mutate, isLoading } = useSWR("role-list", () =>
|
||||
apiFetch.api.user.role.get(),
|
||||
);
|
||||
const list = data?.data || [];
|
||||
const listRole = dataRole?.data || [];
|
||||
const [dataEdit, setDataEdit] = useState({
|
||||
id: "",
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [dataTambah, setDataTambah] = useState({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
name: false,
|
||||
permissions: false,
|
||||
});
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user["role-create"].post(dataTambah as any);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your role have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
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 });
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setBtnLoading(true);
|
||||
const res = await apiFetch.api.user["role-create"].post(
|
||||
dataTambah as any,
|
||||
);
|
||||
if (res.status === 200) {
|
||||
mutate();
|
||||
closeTambah();
|
||||
setDataTambah({
|
||||
name: "",
|
||||
permissions: [],
|
||||
});
|
||||
notification({
|
||||
title: "Success",
|
||||
message: "Your role have been saved",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
setBtnDisable(false);
|
||||
setError({ ...error, [kat]: false });
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create role",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
if (aksi === "edit") {
|
||||
setDataEdit({ ...dataEdit, [kat]: value });
|
||||
} else {
|
||||
setDataTambah({ ...dataTambah, [kat]: value });
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to create role",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (dataEdit.name.length > 0) {
|
||||
setBtnDisable(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",
|
||||
});
|
||||
}
|
||||
}, [dataEdit.id]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
title: "Error",
|
||||
message: "Failed to edit role",
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
setBtnLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
{/* 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>
|
||||
function chooseEdit({
|
||||
data,
|
||||
}: {
|
||||
data: { id: string; name: string; permissions: [] };
|
||||
}) {
|
||||
setDataEdit({
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
permissions: data.permissions ? data.permissions : [],
|
||||
});
|
||||
open();
|
||||
}
|
||||
|
||||
{/* 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>
|
||||
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 {
|
||||
setBtnDisable(false);
|
||||
setError({ ...error, [kat]: false });
|
||||
}
|
||||
|
||||
<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}
|
||||
if (aksi === "edit") {
|
||||
setDataEdit({ ...dataEdit, [kat]: value });
|
||||
} else {
|
||||
setDataTambah({ ...dataTambah, [kat]: value });
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
</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"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={!permissions.includes('setting.user_role.edit') || v.id == "developer"}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={permissions.includes('setting.user_role.delete') ? "Delete Role" : "Delete Role - Anda tidak memiliki akses"}>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes('setting.user_role.delete') || v.id == "developer"}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => chooseEdit({ data: v })}
|
||||
disabled={
|
||||
!permissions.includes("setting.user_role.edit") ||
|
||||
v.id == "developer"
|
||||
}
|
||||
>
|
||||
<IconEdit size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
label={
|
||||
permissions.includes("setting.user_role.delete")
|
||||
? "Delete Role"
|
||||
: "Delete Role - Anda tidak memiliki akses"
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
size="sm"
|
||||
color="red"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
onClick={() => {
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={
|
||||
!permissions.includes(
|
||||
"setting.user_role.delete",
|
||||
) || v.id == "developer"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import { useState } from "react";
|
||||
import useSWR from "swr";
|
||||
import notification from "./notificationGlobal";
|
||||
|
||||
export default function UserSetting({ permissions }: { permissions: JsonValue[] }) {
|
||||
export default function UserSetting({
|
||||
permissions,
|
||||
}: {
|
||||
permissions: JsonValue[];
|
||||
}) {
|
||||
const [btnDisable, setBtnDisable] = useState(true);
|
||||
const [btnLoading, setBtnLoading] = useState(false);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
@@ -437,20 +441,17 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
|
||||
<Title order={4} c="gray.2">
|
||||
Daftar User
|
||||
</Title>
|
||||
{
|
||||
permissions.includes('setting.user.tambah') && (
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
{permissions.includes("setting.user.tambah") && (
|
||||
<Tooltip label="Tambah User">
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<IconPlus size={20} />}
|
||||
onClick={openTambah}
|
||||
>
|
||||
Tambah
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Stack gap={"md"}>
|
||||
@@ -474,18 +475,33 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
|
||||
<Table.Td>{v.nameRole}</Table.Td>
|
||||
<Table.Td>
|
||||
<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
|
||||
variant="light"
|
||||
size="sm"
|
||||
style={{ boxShadow: "0 0 8px rgba(0,255,200,0.2)" }}
|
||||
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} />
|
||||
</ActionIcon>
|
||||
</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
|
||||
variant="light"
|
||||
size="sm"
|
||||
@@ -495,7 +511,10 @@ export default function UserSetting({ permissions }: { permissions: JsonValue[]
|
||||
setDataDelete(v.id);
|
||||
openDelete();
|
||||
}}
|
||||
disabled={!permissions.includes('setting.user.delete') || v.roleId == "developer"}
|
||||
disabled={
|
||||
!permissions.includes("setting.user.delete") ||
|
||||
v.roleId == "developer"
|
||||
}
|
||||
>
|
||||
<IconTrash size={20} />
|
||||
</ActionIcon>
|
||||
|
||||
@@ -3,157 +3,242 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKBedaBiodataDiri({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,108 +3,166 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKBelumKawin({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>();
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,121 +3,163 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKDomisiliOrganisasi({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,142 +3,244 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKelahiran({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,143 +3,153 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKelakuanBaik({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,118 +3,201 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKKematian({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,141 +3,180 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKPenghasilan({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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 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>
|
||||
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 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,119 +3,142 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKTempatUsaha({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</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="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>
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<div style={{ textAlign: "center", marginBottom: "20px" }}>
|
||||
<b style={{ fontSize: "16px" }}>SURAT KETERANGAN TEMPAT USAHA</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="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 }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,110 +3,118 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKTidakMampu({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const urlApi =
|
||||
"/api/pengaduan/image?folder=lainnya&fileName=" +
|
||||
data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<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>
|
||||
return (
|
||||
<div style={{ lineHeight: "1.5" }}>
|
||||
{/* TITLE */}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string, value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ display: "flex", marginBottom: "4px" }}>
|
||||
<div style={{ width: "180px" }}>{label}</div>
|
||||
<div style={{ width: "10px" }}>:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,147 +3,217 @@ import { useEffect, useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKUsaha({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value || ""
|
||||
);
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (jenis: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((item: any) => item.jenis === jenis)?.value ||
|
||||
"",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,191 +4,209 @@ import { useState } from "react";
|
||||
import notification from "../notificationGlobal";
|
||||
|
||||
export default function SKYatim({ data }: { data: any }) {
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(data.surat.dataText.find((i: any) => i.jenis === key)?.value || "");
|
||||
const [viewImg, setViewImg] = useState<string>("");
|
||||
const getValue = (key: string) =>
|
||||
_.upperFirst(
|
||||
data.surat.dataText.find((i: any) => i.jenis === key)?.value || "",
|
||||
);
|
||||
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
const loadImage = async () => {
|
||||
try {
|
||||
setViewImg("");
|
||||
if (!data.setting.perbekelTTD) return;
|
||||
|
||||
const urlApi = '/api/pengaduan/image?folder=lainnya&fileName=' + data.setting.perbekelTTD;
|
||||
// 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;
|
||||
// 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);
|
||||
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
setViewImg(url);
|
||||
} catch (err) {
|
||||
console.error("Gagal load gambar:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
useShallowEffect(() => {
|
||||
loadImage();
|
||||
}, [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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,16 +28,19 @@ export default function Login() {
|
||||
window.location.href = clientRoutes["/scr/dashboard/warga/list-warga"];
|
||||
break;
|
||||
case "credential":
|
||||
window.location.href = clientRoutes["/scr/dashboard/credential/credential"];
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/credential/credential"];
|
||||
break;
|
||||
case "setting":
|
||||
window.location.href = clientRoutes["/scr/dashboard/setting/detail-setting"];
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/setting/detail-setting"];
|
||||
break;
|
||||
case "api_key":
|
||||
window.location.href = clientRoutes["/scr/dashboard/apikey/apikey"];
|
||||
break;
|
||||
case "pelayanan":
|
||||
window.location.href = clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
|
||||
window.location.href =
|
||||
clientRoutes["/scr/dashboard/pelayanan-surat/list-pelayanan"];
|
||||
break;
|
||||
default:
|
||||
window.location.href = clientRoutes["/scr/dashboard"];
|
||||
|
||||
377
src/pages/darmasaba/surat.tsx
Normal file
377
src/pages/darmasaba/surat.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import notification from "@/components/notificationGlobal";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import { capitalizeWords, fromSlug, toSlug } from "@/server/lib/slug_converter";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
FileButton,
|
||||
Grid,
|
||||
Group,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
IconBuildingCommunity,
|
||||
IconInfoCircle,
|
||||
IconUpload,
|
||||
IconUser
|
||||
} from "@tabler/icons-react";
|
||||
import React, { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import useSWR from "swr";
|
||||
|
||||
type DataItem = {
|
||||
jenis: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FormSurat = {
|
||||
kategoryId: string;
|
||||
nama: string;
|
||||
phone: string;
|
||||
dataText: DataItem[];
|
||||
syaratDokumen: DataItem[];
|
||||
};
|
||||
|
||||
|
||||
export default function FormSurat() {
|
||||
const navigate = useNavigate();
|
||||
const { search } = useLocation();
|
||||
const query = new URLSearchParams(search);
|
||||
const jenisSurat = query.get("jenis");
|
||||
const { data, mutate, isLoading } = useSWR("category-pelayanan-list", () =>
|
||||
apiFetch.api.pelayanan.category.get(),
|
||||
);
|
||||
const [jenisSuratFix, setJenisSuratFix] = useState({ name: "", id: "" });
|
||||
const [dataSurat, setDataSurat] = useState<any>({})
|
||||
const [formSurat, setFormSurat] = useState<FormSurat>({
|
||||
nama: "",
|
||||
phone: "",
|
||||
kategoryId: "",
|
||||
dataText: [],
|
||||
syaratDokumen: [],
|
||||
})
|
||||
|
||||
const listCategory = data?.data || [];
|
||||
|
||||
function onGetJenisSurat() {
|
||||
try {
|
||||
if (!jenisSurat || jenisSurat == "null") {
|
||||
setJenisSuratFix({ name: "", id: "" });
|
||||
} else {
|
||||
const namaJenis = fromSlug(jenisSurat);
|
||||
const data = listCategory.find((item: any) => item.name == namaJenis);
|
||||
if (!data) return;
|
||||
setJenisSuratFix(data);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function getDetailJenisSurat() {
|
||||
try {
|
||||
const get: any = await apiFetch.api.pelayanan.category.detail.get({
|
||||
query: {
|
||||
id: jenisSuratFix.id,
|
||||
},
|
||||
})
|
||||
setDataSurat(get.data)
|
||||
setFormSurat({
|
||||
kategoryId: jenisSuratFix.id,
|
||||
nama: "",
|
||||
phone: "",
|
||||
dataText: (get.data?.dataText || []).map((item: string) => ({
|
||||
jenis: item,
|
||||
value: "",
|
||||
})),
|
||||
syaratDokumen: (get.data?.syaratDokumen || []).map(
|
||||
(item: { name: string }) => ({
|
||||
jenis: item.name,
|
||||
value: "",
|
||||
})
|
||||
),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (listCategory.length > 0) {
|
||||
onGetJenisSurat();
|
||||
}
|
||||
}, [jenisSurat, listCategory]);
|
||||
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (jenisSuratFix.id != "") {
|
||||
getDetailJenisSurat();
|
||||
}
|
||||
}, [jenisSuratFix.id]);
|
||||
|
||||
function onSubmit() {
|
||||
const isFormKosong = Object.values(formSurat).some((value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
value.length === 0 ||
|
||||
value.some((item) => !item.value?.trim())
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isFormKosong) {
|
||||
return notification({
|
||||
title: "Gagal",
|
||||
message: "Silahkan lengkapi form surat",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
console.log("READY SUBMIT", formSurat);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Container size="md" w={"100%"}>
|
||||
<Box>
|
||||
<Stack gap="lg">
|
||||
<Group justify="apart" align="center">
|
||||
<Group align="center">
|
||||
<IconBuildingCommunity size={28} />
|
||||
<div>
|
||||
<Text fw={800} size="xl">
|
||||
Surat Keterangan Tidak Mampu (SKTM)
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Blangko resmi untuk pengajuan Surat Keterangan Tidak Mampu —
|
||||
digunakan untuk keperluan pendidikan, kesehatan, atau
|
||||
administrasi.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group>
|
||||
<Badge radius="sm">Form Length: 3 Sections</Badge>
|
||||
</Group>
|
||||
</Group>
|
||||
<Stack gap="lg">
|
||||
{/* Header Section */}
|
||||
<FormSection
|
||||
title="Pemohon"
|
||||
icon={<IconUser size={16} />}
|
||||
description="Informasi identitas pemohon"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nama Lengkap"
|
||||
hint="Nama lengkap pemohon"
|
||||
/>
|
||||
}
|
||||
placeholder="Budi Setiawan"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={6}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label="Nomor Telephone"
|
||||
hint="Nomor telephone yang dapat dihubungi / terhubung dengan whatsapp"
|
||||
/>
|
||||
}
|
||||
placeholder="08123456789"
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
label={<FieldLabel label="Jenis Surat" hint="Jenis surat yang ingin diajukan" />}
|
||||
placeholder="Pilih jenis surat"
|
||||
data={listCategory.map((item: any) => ({
|
||||
value: item.name,
|
||||
label: item.name,
|
||||
}))}
|
||||
value={jenisSuratFix.name}
|
||||
onChange={(value) => {
|
||||
const slug = toSlug(String(value))
|
||||
navigate("/darmasaba/surat?jenis=" + slug)
|
||||
}}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{
|
||||
jenisSuratFix.id != "" && dataSurat && dataSurat.dataText &&
|
||||
<>
|
||||
<FormSection
|
||||
title="Data Pelengkap"
|
||||
description="Data pelengkap yang diperlukan"
|
||||
>
|
||||
<Grid>
|
||||
{
|
||||
dataSurat.dataText.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<TextInput
|
||||
label={
|
||||
<FieldLabel
|
||||
label={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
|
||||
/>
|
||||
}
|
||||
placeholder={dataSurat.dataText[index] == "nik" ? "NIK" : capitalizeWords(dataSurat.dataText[index])}
|
||||
/>
|
||||
</Grid.Col>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
title="Syarat Dokumen"
|
||||
description="Syarat dokumen yang diperlukan"
|
||||
>
|
||||
<Grid>
|
||||
{
|
||||
dataSurat.syaratDokumen.map((item: any, index: number) => (
|
||||
<Grid.Col span={6} key={index}>
|
||||
<FieldLabelUpload
|
||||
label={item.desc}
|
||||
/>
|
||||
<FileButton
|
||||
onChange={async (file) => {
|
||||
if (!file) return;
|
||||
// const base64 = await fileToBase64(file);
|
||||
// form.setFieldValue("foto", base64);
|
||||
// setFotoName(file.name);
|
||||
}}
|
||||
accept="image/*"
|
||||
>
|
||||
{(props) => (
|
||||
<Button
|
||||
leftSection={<IconUpload size={16} />}
|
||||
{...props}
|
||||
mt="sm"
|
||||
>
|
||||
Upload File
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
</Grid.Col>
|
||||
))
|
||||
}
|
||||
</Grid>
|
||||
</FormSection>
|
||||
|
||||
{/* Actions */}
|
||||
<Group justify="right" mt="md">
|
||||
<Button variant="default" onClick={() => { }}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={onSubmit}>Kirim</Button>
|
||||
</Group>
|
||||
</>
|
||||
|
||||
}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ label, hint }: { label: string; hint?: string }) {
|
||||
return (
|
||||
<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 FieldLabelUpload({
|
||||
label,
|
||||
description,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Group justify="apart" style={{ width: "100%" }}>
|
||||
<Group gap={6}>
|
||||
<Text size="sm" fw={600}>
|
||||
{label}
|
||||
</Text>
|
||||
{description && (
|
||||
<ActionIcon size={18} variant="subtle" aria-hidden>
|
||||
<IconInfoCircle size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
{description && (
|
||||
<Text size="sm" c="dimmed" mt={4} style={{ lineHeight: 1.2 }}>
|
||||
{description}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormSection({
|
||||
title,
|
||||
icon,
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
Title
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
|
||||
export default function Dashboard() {
|
||||
|
||||
@@ -285,45 +285,47 @@ function NavigationDashboard() {
|
||||
|
||||
return (
|
||||
<Stack gap="xs" p="sm">
|
||||
{navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute)}
|
||||
leftSection={item.icon}
|
||||
label={
|
||||
<Flex align="center" gap={6}>
|
||||
<Text fw={500}>{item.label}</Text>
|
||||
{isActive(item.path as keyof typeof clientRoute) && (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="teal"
|
||||
radius="sm"
|
||||
size="xs"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
description={item.description}
|
||||
onClick={() =>
|
||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive(item.path as keyof typeof clientRoute)
|
||||
? "rgba(0,255,200,0.1)"
|
||||
: "transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
styles={{
|
||||
label: { color: "white" },
|
||||
description: { color: "#aaa" },
|
||||
section: { color: "teal" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{navItems
|
||||
.filter((item) => permissions.includes(item.key))
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
active={isActive(item.path as keyof typeof clientRoute)}
|
||||
leftSection={item.icon}
|
||||
label={
|
||||
<Flex align="center" gap={6}>
|
||||
<Text fw={500}>{item.label}</Text>
|
||||
{isActive(item.path as keyof typeof clientRoute) && (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="teal"
|
||||
radius="sm"
|
||||
size="xs"
|
||||
style={{ textTransform: "none" }}
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
}
|
||||
description={item.description}
|
||||
onClick={() =>
|
||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||
}
|
||||
style={{
|
||||
backgroundColor: isActive(item.path as keyof typeof clientRoute)
|
||||
? "rgba(0,255,200,0.1)"
|
||||
: "transparent",
|
||||
borderRadius: "8px",
|
||||
transition: "all 0.2s ease",
|
||||
}}
|
||||
styles={{
|
||||
label: { color: "white" },
|
||||
description: { color: "#aaa" },
|
||||
section: { color: "teal" },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
Text,
|
||||
Textarea,
|
||||
ThemeIcon,
|
||||
Title
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
IconFileCheck,
|
||||
IconMessageReport,
|
||||
IconPhone,
|
||||
IconUser
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import type { User } from "generated/prisma";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
@@ -59,7 +59,14 @@ export default function DetailPengajuanPage() {
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<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} />
|
||||
</Stack>
|
||||
</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 [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [keterangan, setKeterangan] = useState("");
|
||||
@@ -88,7 +105,9 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
setHost(data?.user ?? null);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -99,10 +118,15 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
try {
|
||||
const res = await apiFetch.api.pelayanan["update-status"].post({
|
||||
id: data?.id,
|
||||
status: cat == 'tolak' ? 'ditolak' : data.status == 'antrian' ? 'diterima' : 'selesai',
|
||||
status:
|
||||
cat == "tolak"
|
||||
? "ditolak"
|
||||
: data.status == "antrian"
|
||||
? "diterima"
|
||||
: "selesai",
|
||||
keterangan: keterangan,
|
||||
idUser: host?.id ?? "",
|
||||
noSurat: noSurat
|
||||
noSurat: noSurat,
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
@@ -120,7 +144,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
@@ -129,7 +152,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
if (viewImg) {
|
||||
@@ -142,7 +165,7 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
<ModalFile
|
||||
open={openedPreviewFile && !_.isEmpty(viewImg)}
|
||||
onClose={() => {
|
||||
setOpenedPreviewFile(false)
|
||||
setOpenedPreviewFile(false);
|
||||
}}
|
||||
folder="syarat-dokumen"
|
||||
fileName={viewImg}
|
||||
@@ -159,14 +182,25 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
{catModal === "tolak" ? (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan penolakan
|
||||
Anda yakin ingin menolak pengajuan surat ini? Berikan alasan
|
||||
penolakan
|
||||
</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>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</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
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -174,21 +208,31 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
) : (
|
||||
<>
|
||||
<Text>
|
||||
Anda yakin ingin {data?.status == 'antrian' ? 'menerima' : 'menyetujui'} pengajuan surat ini?
|
||||
{
|
||||
data.status == 'diterima' && 'Masukkan nomer surat yang akan dibuat'
|
||||
}
|
||||
Anda yakin ingin{" "}
|
||||
{data?.status == "antrian" ? "menerima" : "menyetujui"}{" "}
|
||||
pengajuan surat ini?
|
||||
{data.status == "diterima" &&
|
||||
"Masukkan nomer surat yang akan dibuat"}
|
||||
</Text>
|
||||
{
|
||||
data.status == 'diterima' && (
|
||||
<Textarea size="md" minRows={5} value={noSurat} onChange={(e) => setNoSurat(e.target.value)} placeholder="Contoh : 08/D-IV/11/2025" />
|
||||
)
|
||||
}
|
||||
{data.status == "diterima" && (
|
||||
<Textarea
|
||||
size="md"
|
||||
minRows={5}
|
||||
value={noSurat}
|
||||
onChange={(e) => setNoSurat(e.target.value)}
|
||||
placeholder="Contoh : 08/D-IV/11/2025"
|
||||
/>
|
||||
)}
|
||||
<Group justify="center" grow>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</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
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -196,11 +240,13 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
{
|
||||
data?.status == "selesai" &&
|
||||
(<ModalSurat open={openedPreview} onClose={() => setOpenedPreview(false)} surat={data?.idSurat} />)
|
||||
}
|
||||
|
||||
{data?.status == "selesai" && (
|
||||
<ModalSurat
|
||||
open={openedPreview}
|
||||
onClose={() => setOpenedPreview(false)}
|
||||
surat={data?.idSurat}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card
|
||||
radius="md"
|
||||
@@ -263,7 +309,11 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
>
|
||||
{syaratDokumen?.map((v: any) => (
|
||||
<List.Item key={v.id}>
|
||||
<Anchor onClick={() => { setViewImg(v.value) }}>
|
||||
<Anchor
|
||||
onClick={() => {
|
||||
setViewImg(v.value);
|
||||
}}
|
||||
>
|
||||
{v.jenis}
|
||||
</Anchor>
|
||||
</List.Item>
|
||||
@@ -271,8 +321,6 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
</List>
|
||||
</Flex>
|
||||
|
||||
|
||||
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconAlignJustified size={20} />
|
||||
@@ -281,79 +329,85 @@ function DetailDataPengajuan({ data, syaratDokumen, dataText, onAction }: { data
|
||||
|
||||
<Table withRowBorders={false}>
|
||||
<Table.Tbody>
|
||||
{
|
||||
dataText?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap", width: "10%" }}>{_.upperFirst(item.jenis)}</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>{_.upperFirst(item.value)}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
{dataText?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td
|
||||
style={{ whiteSpace: "nowrap", width: "10%" }}
|
||||
>
|
||||
{_.upperFirst(item.jenis)}
|
||||
</Table.Td>
|
||||
<Table.Td>:</Table.Td>
|
||||
<Table.Td style={{ width: "85%" }}>
|
||||
{_.upperFirst(item.value)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.terima")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.setujui")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setOpenedPreview(!openedPreview)}
|
||||
>
|
||||
Surat
|
||||
</Button>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
{data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.antrian.terima")}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
disabled={!permissions.includes("pelayanan.diterima.tolak")}
|
||||
variant="light"
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
!permissions.includes("pelayanan.diterima.setujui")
|
||||
}
|
||||
variant="filled"
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Setujui
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "selesai" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => setOpenedPreview(!openedPreview)}
|
||||
>
|
||||
Surat
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -392,16 +446,25 @@ function DetailDataHistori({ data }: { data: any }) {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
{data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.nameUser ? item.nameUser : "-"}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -18,7 +20,7 @@ import {
|
||||
IconClockHour3,
|
||||
IconFileSad,
|
||||
IconSearch,
|
||||
IconUser
|
||||
IconUser,
|
||||
} from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -113,23 +115,25 @@ type StatusKey =
|
||||
function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", async () => {
|
||||
const res = await apiFetch.api.pelayanan.list.get({
|
||||
const { data, mutate, isLoading } = useSwr("/", async () =>
|
||||
apiFetch.api.pelayanan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
page: page.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
useShallowEffect(() => {
|
||||
setPage(1);
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [page]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
@@ -155,26 +159,47 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data || [];
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
const toDate = (d: any) => new Date(d);
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group grow>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengajuan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
<Grid>
|
||||
<Grid.Col span={9}>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengajuan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Group justify="flex-end">
|
||||
<Text
|
||||
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>
|
||||
{Array.isArray(list) && list?.length === 0 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
@@ -185,7 +210,8 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : (
|
||||
Array.isArray(list) && list?.map((v: any) => (
|
||||
Array.isArray(list) &&
|
||||
list?.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
@@ -214,7 +240,7 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
#{v.noPengajuan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
{String(v.updatedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
@@ -247,7 +273,13 @@ function ListPelayananSurat({ status }: { status: StatusKey }) {
|
||||
Tanggal Ajuan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
<Text size="md">
|
||||
{toDate(v.createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Table,
|
||||
Text,
|
||||
Textarea,
|
||||
Title
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import {
|
||||
@@ -58,7 +58,12 @@ export default function DetailPengaduanPage() {
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataPengaduan data={data?.data?.pengaduan} onAction={() => { mutate(); }} />
|
||||
<DetailDataPengaduan
|
||||
data={data?.data?.pengaduan}
|
||||
onAction={() => {
|
||||
mutate();
|
||||
}}
|
||||
/>
|
||||
<DetailDataHistori data={data?.data?.history} />
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
@@ -70,7 +75,13 @@ export default function DetailPengaduanPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => void }) {
|
||||
function DetailDataPengaduan({
|
||||
data,
|
||||
onAction,
|
||||
}: {
|
||||
data: any | null;
|
||||
onAction: () => void;
|
||||
}) {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const [catModal, setCatModal] = useState<"tolak" | "terima">("tolak");
|
||||
const [openedPreview, setOpenedPreview] = useState(false);
|
||||
@@ -84,7 +95,9 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
setHost(data?.user ?? null);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -95,9 +108,16 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
try {
|
||||
const res = await apiFetch.api.pengaduan["update-status"].post({
|
||||
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,
|
||||
idUser: host?.id ?? ""
|
||||
idUser: host?.id ?? "",
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
@@ -115,7 +135,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification({
|
||||
@@ -124,11 +143,10 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
{/* MODAL KONFIRMASI */}
|
||||
<Modal
|
||||
opened={opened}
|
||||
@@ -143,24 +161,46 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
Anda yakin ingin menolak pengaduan ini? Berikan alasan penolakan
|
||||
</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>
|
||||
<Button variant="light" onClick={close}>
|
||||
Batal
|
||||
</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
|
||||
</Button>
|
||||
</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>
|
||||
<Button variant="light" onClick={close}>
|
||||
Tidak
|
||||
</Button>
|
||||
<Button variant="filled" color="green" onClick={() => handleKonfirmasi("terima")}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="green"
|
||||
onClick={() => handleKonfirmasi("terima")}
|
||||
>
|
||||
Ya
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -169,7 +209,6 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
|
||||
{/* MODAL GAMBAR */}
|
||||
<ModalFile
|
||||
open={openedPreview && !_.isEmpty(data?.image)}
|
||||
@@ -259,18 +298,20 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
<IconPhotoScan size={20} />
|
||||
<Text size="md">Gambar</Text>
|
||||
</Group>
|
||||
{
|
||||
data?.image != null && data?.image != ""
|
||||
?
|
||||
<Anchor href="#" onClick={() => { setOpenedPreview(true) }}>
|
||||
Lihat Gambar
|
||||
</Anchor>
|
||||
:
|
||||
<Text size="md" c="white">
|
||||
-
|
||||
</Text>
|
||||
}
|
||||
|
||||
{data?.image != null && data?.image != "" ? (
|
||||
<Anchor
|
||||
href="#"
|
||||
onClick={() => {
|
||||
setOpenedPreview(true);
|
||||
}}
|
||||
>
|
||||
Lihat Gambar
|
||||
</Anchor>
|
||||
) : (
|
||||
<Text size="md" c="white">
|
||||
-
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
@@ -285,74 +326,76 @@ function DetailDataPengaduan({ data, onAction }: { data: any, onAction: () => vo
|
||||
{_.upperFirst(data?.detail)}
|
||||
</Text>
|
||||
</Flex>
|
||||
{
|
||||
data?.keterangan && (
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">Keterangan</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
{_.upperFirst(data?.keterangan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
{data?.keterangan && (
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
<IconInfoTriangle size={20} />
|
||||
<Text size="md">Keterangan</Text>
|
||||
</Group>
|
||||
<Text size="md" c={"white"}>
|
||||
{_.upperFirst(data?.keterangan)}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{
|
||||
data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
disabled={!permissions.includes("pengaduan.antrian.tolak")}
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.antrian.terima")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.diterima.dikerjakan")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Kerjakan
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "dikerjakan" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.dikerjakan.selesai")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</Group>
|
||||
) : <></>
|
||||
}
|
||||
{data?.status === "antrian" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="light"
|
||||
disabled={!permissions.includes("pengaduan.antrian.tolak")}
|
||||
onClick={() => {
|
||||
setCatModal("tolak");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Tolak
|
||||
</Button>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={!permissions.includes("pengaduan.antrian.terima")}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Terima
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "diterima" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={
|
||||
!permissions.includes("pengaduan.diterima.dikerjakan")
|
||||
}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Kerjakan
|
||||
</Button>
|
||||
</Group>
|
||||
) : data?.status === "dikerjakan" ? (
|
||||
<Group justify="center" grow>
|
||||
<Button
|
||||
variant="filled"
|
||||
disabled={
|
||||
!permissions.includes("pengaduan.dikerjakan.selesai")
|
||||
}
|
||||
onClick={() => {
|
||||
setCatModal("terima");
|
||||
open();
|
||||
}}
|
||||
>
|
||||
Selesai
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
@@ -391,16 +434,25 @@ function DetailDataHistori({ data }: { data: any }) {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.createdAt}</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>{item.nameUser ? item.nameUser : "-"}</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
}
|
||||
{data?.map((item: any) => (
|
||||
<Table.Tr key={item.id}>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
})}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.deskripsi}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td style={{ whiteSpace: "nowrap" }}>
|
||||
{item.nameUser ? item.nameUser : "-"}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Grid,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -124,22 +126,25 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate, isLoading } = useSwr("/", async () => {
|
||||
const res = await apiFetch.api.pengaduan.list.get({
|
||||
const { data, mutate, isLoading } = useSwr("/", async () =>
|
||||
apiFetch.api.pengaduan.list.get({
|
||||
query: {
|
||||
status,
|
||||
search: value,
|
||||
take: "",
|
||||
page: "",
|
||||
page: page.toString(),
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return Array.isArray(res?.data) ? res.data : []; // ⬅ paksa return array
|
||||
});
|
||||
useShallowEffect(() => {
|
||||
setPage(1);
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [status, value]);
|
||||
}, [page]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
const unsubscribe = subscribe(state, () => mutate());
|
||||
@@ -163,31 +168,48 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
</Card>
|
||||
);
|
||||
|
||||
const list = data || [];
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
const toDate = (d: any) => new Date(d);
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
<Group grow>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengaduan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
<Grid>
|
||||
<Grid.Col span={9}>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari pengaduan..."
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
leftSection={<IconSearch size={16} />}
|
||||
rightSectionPointerEvents="all"
|
||||
rightSection={
|
||||
<CloseButton
|
||||
aria-label="Clear input"
|
||||
onClick={() => setValue("")}
|
||||
style={{ display: value ? undefined : "none" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={3}>
|
||||
<Group justify="flex-end">
|
||||
<Text
|
||||
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 justify="flex-end">
|
||||
<Text size="sm">Menampilkan {Number(data?.data?.length) * (page - 1) + 1} – {Math.min(10, Number(data?.data?.length) * page)} dari {Number(data?.data?.length)}</Text>
|
||||
<Pagination total={Number(data?.data?.length)} value={page} onChange={setPage} withPages={false} />
|
||||
</Group> */}
|
||||
</Group>
|
||||
{list.length === 0 ? (
|
||||
</Group>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
{Array.isArray(list) && list.length === 0 ? (
|
||||
<Flex justify="center" align="center" py={"xl"}>
|
||||
<Stack gap={4} align="center">
|
||||
<IconFileSad size={32} color="gray" />
|
||||
@@ -197,7 +219,8 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
</Stack>
|
||||
</Flex>
|
||||
) : (
|
||||
Array.isArray(list) && list?.map((v: any) => (
|
||||
Array.isArray(list) &&
|
||||
list?.map((v: any) => (
|
||||
<Card
|
||||
key={v.id}
|
||||
radius="lg"
|
||||
@@ -224,7 +247,7 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
#{v.noPengaduan}
|
||||
</Title>
|
||||
<Text size="sm" c="dimmed">
|
||||
{v.updatedAt}
|
||||
{String(v.updatedAt)}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
@@ -257,7 +280,13 @@ function ListPengaduan({ status }: { status: StatusKey }) {
|
||||
Tanggal Aduan
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="md">{v.createdAt}</Text>
|
||||
<Text size="md">
|
||||
{toDate(v.createdAt).toLocaleDateString("id-ID", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction={"column"} justify="flex-start">
|
||||
<Group gap="xs">
|
||||
|
||||
@@ -5,19 +5,14 @@ import ProfileUser from "@/components/ProfileUser";
|
||||
import UserRoleSetting from "@/components/UserRoleSetting";
|
||||
import UserSetting from "@/components/UserSetting";
|
||||
import apiFetch from "@/lib/apiFetch";
|
||||
import {
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
NavLink
|
||||
} from "@mantine/core";
|
||||
import { Card, Container, Grid, NavLink } from "@mantine/core";
|
||||
import {
|
||||
IconBuildingBank,
|
||||
IconCategory2,
|
||||
IconMailSpark,
|
||||
IconUserCog,
|
||||
IconUserScreen,
|
||||
IconUsersGroup
|
||||
IconUsersGroup,
|
||||
} from "@tabler/icons-react";
|
||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -33,7 +28,9 @@ export default function DetailSettingPage() {
|
||||
async function fetchPermissions() {
|
||||
const { data } = await apiFetch.api.user.find.get();
|
||||
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);
|
||||
} else {
|
||||
setPermissions([]);
|
||||
@@ -42,7 +39,6 @@ export default function DetailSettingPage() {
|
||||
fetchPermissions();
|
||||
}, []);
|
||||
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
key: "setting.profile",
|
||||
@@ -85,8 +81,7 @@ export default function DetailSettingPage() {
|
||||
icon: <IconBuildingBank size={20} />,
|
||||
label: "Desa",
|
||||
description: "Manage desa information",
|
||||
}
|
||||
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -104,17 +99,19 @@ export default function DetailSettingPage() {
|
||||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||||
}}
|
||||
>
|
||||
{
|
||||
navItems.filter((item) => permissions.includes(item.key)).map((item) => (
|
||||
{navItems
|
||||
.filter((item) => permissions.includes(item.key))
|
||||
.map((item) => (
|
||||
<NavLink
|
||||
key={item.key}
|
||||
href={'?type=' + item.path}
|
||||
href={"?type=" + item.path}
|
||||
label={item.label}
|
||||
leftSection={item.icon}
|
||||
active={type === item.path || (!type && item.path === 'profile')}
|
||||
active={
|
||||
type === item.path || (!type && item.path === "profile")
|
||||
}
|
||||
/>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
@@ -130,17 +127,47 @@ export default function DetailSettingPage() {
|
||||
}}
|
||||
>
|
||||
{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" ? (
|
||||
<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" ? (
|
||||
<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" ? (
|
||||
<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" ? (
|
||||
<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>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -37,10 +37,13 @@ export default function DetailWargaPage() {
|
||||
mutate();
|
||||
}, []);
|
||||
|
||||
|
||||
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%"}>
|
||||
<Grid>
|
||||
<Grid.Col span={4}>
|
||||
@@ -48,18 +51,29 @@ export default function DetailWargaPage() {
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack gap={"xl"}>
|
||||
<DetailDataHistori data={data?.data?.pengaduan} kategori="pengaduan" />
|
||||
<DetailDataHistori data={data?.data?.pelayanan} kategori="pelayanan" />
|
||||
<DetailDataHistori
|
||||
data={data?.data?.pengaduan}
|
||||
kategori="pengaduan"
|
||||
/>
|
||||
<DetailDataHistori
|
||||
data={data?.data?.pelayanan}
|
||||
kategori="pelayanan"
|
||||
/>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Container>
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan' | 'pelayanan' }) {
|
||||
function DetailDataHistori({
|
||||
data,
|
||||
kategori,
|
||||
}: {
|
||||
data: any;
|
||||
kategori: "pengaduan" | "pelayanan";
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
@@ -85,43 +99,47 @@ function DetailDataHistori({ data, kategori }: { data: any, kategori: 'pengaduan
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<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></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>{kategori == "pengaduan" ? item.title : item.category}</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
kategori == "pengaduan" ?
|
||||
navigate(
|
||||
{data?.length > 0 ? (
|
||||
data?.map((item: any, index: number) => (
|
||||
<Table.Tr key={index}>
|
||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
||||
<Table.Td>
|
||||
{kategori == "pengaduan" ? item.title : item.category}
|
||||
</Table.Td>
|
||||
<Table.Td>{item.status}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
kategori == "pengaduan"
|
||||
? navigate(
|
||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||
) :
|
||||
navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4} align="center">Tidak ada data</Table.Td>
|
||||
: navigate(
|
||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
}
|
||||
))
|
||||
) : (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={4} align="center">
|
||||
Tidak ada data
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
Container,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
Input,
|
||||
Pagination,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { useShallowEffect } from "@mantine/hooks";
|
||||
@@ -19,22 +22,31 @@ import useSWR from "swr";
|
||||
|
||||
export default function ListWargaPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, mutate, isLoading } = useSWR("/", () =>
|
||||
const [pages, setPages] = useState(1);
|
||||
const [value, setValue] = useState("");
|
||||
const { data, mutate } = useSWR("/", () =>
|
||||
apiFetch.api.warga.list.get({
|
||||
query: {
|
||||
search: value,
|
||||
page: pages,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const list = data?.data || [];
|
||||
|
||||
const [value, setValue] = useState("");
|
||||
const list = data?.data?.data || [];
|
||||
const total = data?.data?.total || 0;
|
||||
const totalPage = data?.data?.totalPages || 1;
|
||||
const pageSize = data?.data?.pageSize || 10;
|
||||
const pageNow = data?.data?.page || 1;
|
||||
|
||||
useShallowEffect(() => {
|
||||
setPages(1);
|
||||
mutate();
|
||||
}, [value]);
|
||||
|
||||
useShallowEffect(() => {
|
||||
mutate();
|
||||
}, [pages]);
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl" w={"100%"}>
|
||||
@@ -48,10 +60,10 @@ export default function ListWargaPage() {
|
||||
}}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Title order={3} c="gray.2">
|
||||
List Data Warga
|
||||
</Title>
|
||||
<Flex align="center" justify="space-between">
|
||||
<Title order={3} c="gray.2">
|
||||
List Data Warga
|
||||
</Title>
|
||||
<Input
|
||||
value={value}
|
||||
placeholder="Cari warga..."
|
||||
@@ -66,6 +78,15 @@ export default function ListWargaPage() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Group>
|
||||
<Text size="sm">{`${pageSize * (pages - 1) + 1} – ${Math.min(total, pageSize * pages)} of ${total}`}</Text>
|
||||
<Pagination
|
||||
total={totalPage}
|
||||
value={pages}
|
||||
onChange={setPages}
|
||||
withPages={false}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Divider my={0} />
|
||||
<Table>
|
||||
@@ -77,32 +98,33 @@ export default function ListWargaPage() {
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{
|
||||
Array.isArray(list) && list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">Tidak ada data</Table.Td>
|
||||
{Array.isArray(list) && list?.length === 0 ? (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={3} align="center">
|
||||
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>
|
||||
) : (
|
||||
Array.isArray(list) && list?.map((item, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{item.name}</Table.Td>
|
||||
<Table.Td>{item.phone}</Table.Td>
|
||||
<Table.Td>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(
|
||||
`/scr/dashboard/warga/detail-warga?id=${item.id}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Detail
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)
|
||||
}
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export function isValidPhone(number: string): boolean {
|
||||
const clean = number.replace(/[\s.-]/g, ""); // hapus spasi, titik, strip
|
||||
const regex = /^(?:\+62|62|0)8\d{7,12}$/;
|
||||
return regex.test(clean);
|
||||
}
|
||||
|
||||
export function normalizePhoneNumber({ phone }: { phone: string }) {
|
||||
// Hapus semua spasi, tanda hubung, atau karakter non-digit (+ tetap dipertahankan untuk dicek)
|
||||
let cleaned = phone.trim().replace(/[\s-]/g, "");
|
||||
let cleaned = phone.trim().replace(/[\s.-]/g, "");
|
||||
|
||||
// Jika diawali dengan +62 → ganti jadi 62
|
||||
if (cleaned.startsWith("+62")) {
|
||||
|
||||
18
src/server/lib/slug_converter.ts
Normal file
18
src/server/lib/slug_converter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function toSlug(text: string): string {
|
||||
return encodeURIComponent(
|
||||
text
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
);
|
||||
}
|
||||
|
||||
export function fromSlug(slug: string): string {
|
||||
return decodeURIComponent(slug)
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
|
||||
export function capitalizeWords(text: string): string {
|
||||
return text.replace(/\b\w/g, c => c.toUpperCase());
|
||||
}
|
||||
@@ -128,7 +128,8 @@ function convertToMcpContent(payload: any) {
|
||||
export async function executeTool(
|
||||
tool: any,
|
||||
args: Record<string, any> = {},
|
||||
baseUrl: string
|
||||
baseUrl: string,
|
||||
xPayload: Record<string, any> = {}
|
||||
) {
|
||||
const x = tool["x-props"] || {};
|
||||
const method = (x.method || "GET").toUpperCase();
|
||||
@@ -247,6 +248,9 @@ export async function executeTool(
|
||||
|
||||
// Execute fetch
|
||||
console.log(`[MCP] → ${method} ${url}`);
|
||||
for(const [key, value] of Object.entries(xPayload)) {
|
||||
opts.headers![key] = value;
|
||||
}
|
||||
const res = await fetch(url, opts);
|
||||
|
||||
const resContentType = (res.headers.get("content-type") || "").toLowerCase();
|
||||
@@ -281,7 +285,7 @@ export async function executeTool(
|
||||
/* -------------------------
|
||||
JSON-RPC Handler
|
||||
------------------------- */
|
||||
async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCResponse> {
|
||||
async function handleMCPRequestAsync(request: JSONRPCRequest, xPayload: Record<string, any>): Promise<JSONRPCResponse> {
|
||||
const { id, method, params } = request;
|
||||
|
||||
const makeError = (code: number, message: string, data?: any): JSONRPCResponse => ({
|
||||
@@ -331,7 +335,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
|
||||
const baseUrl = (params?.credentials?.baseUrl as string) || process.env.BUN_PUBLIC_BASE_URL || "http://localhost:3000";
|
||||
const args = params?.arguments || {};
|
||||
|
||||
const result = await executeTool(tool, args, baseUrl);
|
||||
const result = await executeTool(tool, args, baseUrl, xPayload);
|
||||
|
||||
// Extract the meaningful payload (prefer nested .data if present)
|
||||
const raw = extractRaw(result.data);
|
||||
@@ -365,7 +369,7 @@ async function handleMCPRequestAsync(request: JSONRPCRequest): Promise<JSONRPCRe
|
||||
Elysia App & Routes
|
||||
------------------------- */
|
||||
export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
|
||||
.post("/mcp", async ({ request, set }) => {
|
||||
.post("/mcp", async ({ request, set, headers }) => {
|
||||
set.headers["Content-Type"] = "application/json";
|
||||
set.headers["Access-Control-Allow-Origin"] = "*";
|
||||
|
||||
@@ -378,12 +382,17 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
|
||||
}
|
||||
}
|
||||
|
||||
const xPayload = {
|
||||
['x-user']: headers['x-user'] || "",
|
||||
['x-phone']: headers['x-phone'] || ""
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// If batch array -> allSettled for resilience
|
||||
if (Array.isArray(body)) {
|
||||
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req));
|
||||
const promises = body.map((req: JSONRPCRequest) => handleMCPRequestAsync(req, xPayload));
|
||||
const settled = await Promise.allSettled(promises);
|
||||
const responses = settled.map((s) =>
|
||||
s.status === "fulfilled"
|
||||
@@ -401,7 +410,7 @@ export const MCPRoute = new Elysia({ tags: ["MCP Server"] })
|
||||
return responses;
|
||||
}
|
||||
|
||||
const single = await handleMCPRequestAsync(body as JSONRPCRequest);
|
||||
const single = await handleMCPRequestAsync(body as JSONRPCRequest, xPayload);
|
||||
return single;
|
||||
} catch (err: any) {
|
||||
set.status = 400;
|
||||
|
||||
@@ -3,9 +3,10 @@ import type { StatusPengaduan } from "generated/prisma"
|
||||
import { createSurat } from "../lib/create-surat"
|
||||
import { getLastUpdated } from "../lib/get-last-updated"
|
||||
import { generateNoPengajuanSurat } from "../lib/no-pengajuan-surat"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
|
||||
|
||||
const PelayananRoute = new Elysia({
|
||||
prefix: "pelayanan",
|
||||
tags: ["pelayanan"],
|
||||
@@ -101,11 +102,54 @@ const PelayananRoute = new Elysia({
|
||||
description: `tool untuk delete kategori pelayanan surat`
|
||||
}
|
||||
})
|
||||
.get("/category/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
const data = await prisma.categoryPelayanan.findUnique({
|
||||
where: {
|
||||
id
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataText: string[] = Array.isArray(data.dataText)
|
||||
? data.dataText.filter((v): v is string => typeof v === "string")
|
||||
: [];
|
||||
|
||||
const syaratDokumen: { name: string }[] = Array.isArray(data.syaratDokumen)
|
||||
? data.syaratDokumen.filter(
|
||||
(v): v is { name: string } =>
|
||||
typeof v === "object" &&
|
||||
v !== null &&
|
||||
"name" in v &&
|
||||
typeof (v as any).name === "string"
|
||||
)
|
||||
: [];
|
||||
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
dataText,
|
||||
syaratDokumen,
|
||||
};
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Kategori Pelayanan Surat by ID",
|
||||
description: `tool untuk mendapatkan detail kategori pelayanan surat berdasarkan id`,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// --- PELAYANAN SURAT ---
|
||||
.get("/", async ({ query }) => {
|
||||
const { phone } = query
|
||||
.get("/", async ({ query, headers }) => {
|
||||
// const { phone } = query
|
||||
const phone = headers['x-phone'] || ""
|
||||
const data = await prisma.pelayananAjuan.findMany({
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
@@ -115,13 +159,34 @@ const PelayananRoute = new Elysia({
|
||||
Warga: {
|
||||
phone
|
||||
}
|
||||
},
|
||||
select: {
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return data
|
||||
|
||||
const dataFix = data.map((item) => {
|
||||
return {
|
||||
noPengajuan: item.noPengajuan,
|
||||
status: item.status,
|
||||
category: item.CategoryPelayanan.name,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
|
||||
}, {
|
||||
query: t.Object({
|
||||
phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||
}),
|
||||
// query: t.Object({
|
||||
// phone: t.String({ minLength: 1, error: "phone harus diisi" }),
|
||||
// }),
|
||||
detail: {
|
||||
summary: "List Ajuan Pelayanan Surat by Phone",
|
||||
description: `tool untuk mendapatkan list ajuan pelayanan surat`,
|
||||
@@ -130,17 +195,9 @@ const PelayananRoute = new Elysia({
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
const data = await prisma.pelayananAjuan.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
noPengajuan: id
|
||||
},
|
||||
{
|
||||
id: id
|
||||
}
|
||||
]
|
||||
id: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -170,6 +227,17 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
const datafix = {
|
||||
pengajuan: {},
|
||||
history: [],
|
||||
warga: {},
|
||||
syaratDokumen: [],
|
||||
dataText: [],
|
||||
}
|
||||
return datafix
|
||||
}
|
||||
|
||||
const dataSurat = await prisma.suratPelayanan.findFirst({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
@@ -250,14 +318,7 @@ const PelayananRoute = new Elysia({
|
||||
id: item.id,
|
||||
deskripsi: item.deskripsi,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}),
|
||||
createdAt: item.createdAt,
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
@@ -287,22 +348,25 @@ const PelayananRoute = new Elysia({
|
||||
syaratDokumen: dataSyaratFix,
|
||||
dataText: dataTextFix,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}, {
|
||||
query: t.Object({
|
||||
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Ajuan Pelayanan Surat",
|
||||
description: `tool untuk mendapatkan detail ajuan pelayanan surat`,
|
||||
tags: ["mcp"]
|
||||
summary: "Detail Ajuan Pelayanan Surat by ID",
|
||||
description: `tool untuk mendapatkan detail ajuan pelayanan surat berdasarkan id`,
|
||||
}
|
||||
})
|
||||
.post("/create", async ({ body }) => {
|
||||
const { kategoriId, wargaId, noTelepon, dataText, syaratDokumen } = body
|
||||
.post("/create", async ({ body, headers }) => {
|
||||
const { kategoriId, dataText, syaratDokumen } = body
|
||||
const namaWarga = headers['x-user'] || ""
|
||||
const noTelepon = headers['x-phone'] || ""
|
||||
const noPengajuan = await generateNoPengajuanSurat()
|
||||
let idCategoryFix = kategoriId
|
||||
let idWargaFix = wargaId
|
||||
let idWargaFix = ""
|
||||
|
||||
const category = await prisma.categoryPelayanan.findUnique({
|
||||
where: {
|
||||
id: kategoriId,
|
||||
@@ -324,36 +388,28 @@ const PelayananRoute = new Elysia({
|
||||
|
||||
}
|
||||
|
||||
const warga = await prisma.warga.findUnique({
|
||||
if (!isValidPhone(noTelepon)) {
|
||||
return { success: false, message: 'nomor telepon tidak valid, harap masukkan nomor yang benar' }
|
||||
}
|
||||
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const dataWarga = await prisma.warga.upsert({
|
||||
where: {
|
||||
id: wargaId,
|
||||
phone: nomorHP
|
||||
},
|
||||
create: {
|
||||
name: namaWarga,
|
||||
phone: nomorHP,
|
||||
},
|
||||
update: {
|
||||
name: namaWarga,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
if (!warga) {
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const cariWarga = await prisma.warga.findFirst({
|
||||
where: {
|
||||
phone: nomorHP,
|
||||
}
|
||||
})
|
||||
|
||||
if (!cariWarga) {
|
||||
const wargaCreate = await prisma.warga.create({
|
||||
data: {
|
||||
name: wargaId,
|
||||
phone: nomorHP,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
idWargaFix = wargaCreate.id
|
||||
} else {
|
||||
idWargaFix = cariWarga.id
|
||||
}
|
||||
|
||||
}
|
||||
idWargaFix = dataWarga.id
|
||||
|
||||
const pengaduan = await prisma.pelayananAjuan.create({
|
||||
data: {
|
||||
@@ -391,6 +447,7 @@ const PelayananRoute = new Elysia({
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
await prisma.syaratDokumenPelayanan.createMany({
|
||||
data: dataInsertSyaratDokumen,
|
||||
})
|
||||
@@ -407,40 +464,34 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'pengajuan surat sudah dibuat' }
|
||||
return { success: true, message: 'pengajuan layanan surat sudah dibuat dengan nomer ' + noPengajuan + ', nomer ini akan digunakan untuk mengakses pengajuan ini' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
kategoriId: t.String({
|
||||
minLength: 1,
|
||||
description: "ID atau nama kategori pelayanan surat yang dipilih. Jika berupa nama, sistem akan mencocokkan secara otomatis.",
|
||||
examples: ["skusaha"],
|
||||
error: "ID kategori harus diisi"
|
||||
}),
|
||||
// namaWarga: t.String({
|
||||
// description: "Nama warga",
|
||||
// examples: ["Budi Santoso"],
|
||||
// error: "Nama warga harus diisi"
|
||||
// }),
|
||||
|
||||
wargaId: t.String({
|
||||
minLength: 1,
|
||||
description: "ID warga atau nama warga. Jika ID tidak ditemukan, sistem akan mencari berdasarkan nama.",
|
||||
examples: ["Budi Santoso"],
|
||||
error: "ID warga harus diisi"
|
||||
}),
|
||||
|
||||
noTelepon: t.String({
|
||||
minLength: 8,
|
||||
description: "Nomor HP warga yang akan dinormalisasi. Jika data warga tidak ditemukan berdasarkan idWarga, pencarian dilakukan via nomor ini.",
|
||||
examples: ["081234567890"],
|
||||
error: "Nomor telepon harus diisi"
|
||||
}),
|
||||
// noTelepon: t.String({
|
||||
// error: "Nomor telepon harus diisi",
|
||||
// examples: ["08123456789", "+628123456789"],
|
||||
// description: "Nomor telepon warga pelapor"
|
||||
// }),
|
||||
|
||||
dataText: t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
minLength: 1,
|
||||
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
||||
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
minLength: 1,
|
||||
description: "Isi atau nilai dari jenis field terkait.",
|
||||
examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"],
|
||||
error: "value harus diisi"
|
||||
@@ -469,13 +520,11 @@ const PelayananRoute = new Elysia({
|
||||
syaratDokumen: t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
minLength: 1,
|
||||
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
|
||||
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
minLength: 1,
|
||||
description: "Nama file atau identifier file dokumen yang diupload.",
|
||||
examples: ["ktp_budi.png", "kk_budi.png"],
|
||||
error: "value harus diisi"
|
||||
@@ -495,11 +544,178 @@ const PelayananRoute = new Elysia({
|
||||
),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Create Pengajuan Pelayanan Surat",
|
||||
summary: "Buat Pengajuan Pelayanan Surat",
|
||||
description: `tool untuk membuat pengajuan pelayanan surat dengan syarat dokumen serta data text sesuai kategori pelayanan surat yang dipilih`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.post("/detail-data", async ({ body }) => {
|
||||
const { nomerPengajuan } = body
|
||||
const data = await prisma.pelayananAjuan.findFirst({
|
||||
where: {
|
||||
noPengajuan: nomerPengajuan
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengajuan: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPelayanan: {
|
||||
select: {
|
||||
name: true,
|
||||
dataText: true,
|
||||
syaratDokumen: true,
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
_count: {
|
||||
select: {
|
||||
Pengaduan: true,
|
||||
PelayananAjuan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
return { success: false, message: "Data tidak ditemukan" }
|
||||
}
|
||||
|
||||
const dataSurat = await prisma.suratPelayanan.findFirst({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
idCategory: true,
|
||||
}
|
||||
})
|
||||
|
||||
const dataSyarat = await prisma.syaratDokumenPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
jenis: true,
|
||||
value: true,
|
||||
}
|
||||
})
|
||||
|
||||
const dataText = await prisma.dataTextPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
isActive: true
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
value: true,
|
||||
jenis: true,
|
||||
}
|
||||
})
|
||||
const syaratDokumen = (data?.CategoryPelayanan?.syaratDokumen ?? []) as {
|
||||
name: string;
|
||||
desc: string;
|
||||
}[];
|
||||
|
||||
const dataSyaratFix = dataSyarat.map((item) => {
|
||||
// const desc = syaratDokumen.find((v) => v.name == item.jenis)?.desc
|
||||
return {
|
||||
id: item.id,
|
||||
jenis: item.jenis,
|
||||
value: item.value,
|
||||
}
|
||||
})
|
||||
|
||||
const dataTextFix = dataText.map((item) => {
|
||||
// const desc = data?.CategoryPelayanan?.dataText.find((v) => v == item.jenis)
|
||||
return {
|
||||
id: item.id,
|
||||
jenis: item.jenis,
|
||||
value: item.value,
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistory = await prisma.historyPelayanan.findMany({
|
||||
where: {
|
||||
idPengajuanLayanan: data?.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
deskripsi: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
deskripsi: item.deskripsi,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt,
|
||||
idUser: item.idUser,
|
||||
nameUser: item.User?.name,
|
||||
}
|
||||
})
|
||||
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
phone: data?.Warga?.phone,
|
||||
pengaduan: data?.Warga?._count.Pengaduan,
|
||||
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||
}
|
||||
|
||||
const dataPengajuan = {
|
||||
id: data?.id,
|
||||
noPengajuan: data?.noPengajuan,
|
||||
category: data?.CategoryPelayanan.name,
|
||||
status: data?.status,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
idSurat: dataSurat?.id,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
pengajuan: dataPengajuan,
|
||||
history: dataHistoryFix,
|
||||
warga: warga,
|
||||
syaratDokumen: dataSyaratFix,
|
||||
dataText: dataTextFix,
|
||||
}
|
||||
|
||||
console.log('detail data syarat==', dataSyaratFix)
|
||||
|
||||
return datafix
|
||||
|
||||
}, {
|
||||
body: t.Object({
|
||||
nomerPengajuan: t.String({
|
||||
description: "Nomor pengajuan pelayanan surat yang ingin diakses.",
|
||||
examples: ["PS-101225-001", "PS-101225-002"],
|
||||
error: "Nomor pengajuan harus diisi"
|
||||
})
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Pengajuan Pelayanan Surat By Nomor Pengajuan",
|
||||
description: `tool untuk mendapatkan detail pengajuan pelayanan surat berdasarkan nomor pengajuan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.post("/update-status", async ({ body }) => {
|
||||
const { id, status, keterangan, idUser, noSurat } = body
|
||||
let deskripsi = ""
|
||||
@@ -557,6 +773,200 @@ const PelayananRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Update Status Pengajuan Pelayanan Surat",
|
||||
description: `tool untuk update status pengajuan pelayanan surat`,
|
||||
}
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { nomerPengajuan, syaratDokumen, dataText } = body
|
||||
let dataUpdate = []
|
||||
console.log(body)
|
||||
|
||||
const pengajuan = await prisma.pelayananAjuan.findFirst({
|
||||
where: {
|
||||
noPengajuan: nomerPengajuan,
|
||||
}
|
||||
})
|
||||
|
||||
if (!pengajuan) {
|
||||
console.log("data pengajuan surat tidak ditemukan")
|
||||
return { success: false, message: 'data pengajuan surat tidak ditemukan' }
|
||||
}
|
||||
|
||||
|
||||
if (pengajuan.status != "ditolak" && pengajuan.status != "antrian") {
|
||||
console.log("pengajuan surat tidak dapat diupdate karena status " + pengajuan.status)
|
||||
return { success: false, message: 'pengajuan surat tidak dapat diupdate karena status ' + pengajuan.status }
|
||||
}
|
||||
|
||||
if (dataText && dataText.length > 0) {
|
||||
console.log("dataText")
|
||||
for (const item of dataText) {
|
||||
dataUpdate.push(item.jenis)
|
||||
const hasil = await prisma.dataTextPelayanan.findFirst({
|
||||
where: {
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
jenis: item.jenis,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const upd = await prisma.dataTextPelayanan.upsert({
|
||||
where: {
|
||||
id: hasil?.id
|
||||
},
|
||||
update: {
|
||||
value: item.value,
|
||||
},
|
||||
create: {
|
||||
value: item.value,
|
||||
jenis: item.jenis,
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
idCategory: pengajuan.idCategory,
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const category = await prisma.categoryPelayanan.findUnique({
|
||||
where: {
|
||||
id: pengajuan.idCategory,
|
||||
}
|
||||
})
|
||||
|
||||
type SyaratDokumen = {
|
||||
desc: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
const syarat = category?.syaratDokumen as SyaratDokumen[] | undefined
|
||||
|
||||
|
||||
if (syaratDokumen && syaratDokumen.length > 0) {
|
||||
console.log("syaratDokumen")
|
||||
for (const item of syaratDokumen) {
|
||||
const pilih = syarat?.find((cat) => cat.desc.toLowerCase() == item.jenis.toLowerCase() || cat.name.toLowerCase() == item.jenis.toLowerCase())?.name;
|
||||
console.log(syarat, pilih)
|
||||
dataUpdate.push(pilih)
|
||||
|
||||
const hasil = await prisma.syaratDokumenPelayanan.findFirst({
|
||||
where: {
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
jenis: pilih,
|
||||
}
|
||||
})
|
||||
console.log(hasil, item)
|
||||
|
||||
if (hasil && hasil.id) {
|
||||
const upd = await prisma.syaratDokumenPelayanan.upsert({
|
||||
where: {
|
||||
id: hasil.id
|
||||
},
|
||||
update: {
|
||||
value: item.value,
|
||||
},
|
||||
create: {
|
||||
value: item.value,
|
||||
jenis: hasil.jenis,
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
idCategory: pengajuan.idCategory,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return { success: false, message: 'dokumen tidak dapat diupload' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const keys = dataUpdate.join(", ");
|
||||
|
||||
if (pengajuan.status == "ditolak") {
|
||||
const updStatus = await prisma.pelayananAjuan.update({
|
||||
where: {
|
||||
id: pengajuan.id,
|
||||
},
|
||||
data: {
|
||||
status: "antrian",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const history = await prisma.historyPelayanan.create({
|
||||
data: {
|
||||
idPengajuanLayanan: pengajuan.id,
|
||||
deskripsi: `Pengajuan surat diupdate oleh warga (data yg diupdate: ${keys})`,
|
||||
status: "antrian",
|
||||
}
|
||||
})
|
||||
|
||||
console.log("pengajuan surat sudah diperbarui")
|
||||
|
||||
return { success: true, message: 'pengajuan surat sudah diperbarui' }
|
||||
|
||||
}, {
|
||||
body: t.Object({
|
||||
nomerPengajuan: t.String({
|
||||
error: "nomer pengajuan harus diisi",
|
||||
description: "Nomer pengajuan yang ingin diupdate"
|
||||
}),
|
||||
dataText: t.Optional(t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
description: "Jenis field yang dibutuhkan oleh kategori pelayanan. Biasanya dinamis.",
|
||||
examples: ["nama", "jenis kelamin", "tempat tanggal lahir", "negara", "agama", "status perkawinan", "alamat", "pekerjaan", "jenis usaha", "alamat usaha"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
description: "Isi atau nilai dari jenis field terkait.",
|
||||
examples: ["Budi Santoso", "Laki-laki", "Denpasar, 28 Februari 1990", "Indonesia", "Islam", "Belum menikah", "Jl. Mawar No. 10", "Karyawan Swasta", "usaha makanan", "Jl. Melati No. 21"],
|
||||
error: "value harus diisi"
|
||||
}),
|
||||
}),
|
||||
{
|
||||
description: "Kumpulan data text dinamis sesuai kategori layanan.",
|
||||
examples: [
|
||||
[
|
||||
{ jenis: "nama", value: "Budi Santoso" },
|
||||
{ jenis: "jenis kelamin", value: "Laki-laki" },
|
||||
{ jenis: "tempat tanggal lahir", value: "Denpasar, 28 Februari 1990" },
|
||||
{ jenis: "negara", value: "Indonesia" },
|
||||
{ jenis: "agama", value: "Islam" },
|
||||
{ jenis: "status perkawinan", value: "Belum menikah" },
|
||||
{ jenis: "alamat", value: "Jl. Mawar No. 10" },
|
||||
{ jenis: "pekerjaan", value: "Karyawan Swasta" },
|
||||
{ jenis: "jenis usaha", value: "usaha makanan" },
|
||||
{ jenis: "alamat usaha", value: "Jl. Melati No. 21" },
|
||||
]
|
||||
],
|
||||
}
|
||||
)),
|
||||
syaratDokumen: t.Optional(t.Array(
|
||||
t.Object({
|
||||
jenis: t.String({
|
||||
description: "Jenis dokumen persyaratan yang diminta oleh kategori layanan.",
|
||||
examples: ["ktp", "kk", "surat_pengantar_rt"],
|
||||
error: "jenis harus diisi"
|
||||
}),
|
||||
value: t.String({
|
||||
description: "Nama file atau identifier file dokumen yang diupload.",
|
||||
examples: ["ktp_budi.png", "kk_budi.png"],
|
||||
error: "value harus diisi"
|
||||
}),
|
||||
}),
|
||||
{
|
||||
description: "Kumpulan dokumen yang wajib diupload sesuai persyaratan layanan.",
|
||||
examples: [
|
||||
[
|
||||
{ jenis: "pengantar kelian", value: "pengantar_kelurahan_budi.png" },
|
||||
{ jenis: "ktp/kk", value: "kk_budi.png" },
|
||||
{ jenis: "foto lokasi", value: "foto_lokasi_budi.png" }
|
||||
]
|
||||
],
|
||||
}
|
||||
)),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Update Data Pengajuan Pelayanan Surat",
|
||||
description: `tool untuk update data pengajuan pelayanan surat`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
@@ -588,6 +998,14 @@ const PelayananRoute = new Elysia({
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Warga: {
|
||||
name: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -599,6 +1017,11 @@ const PelayananRoute = new Elysia({
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const totalData = await prisma.pelayananAjuan.count({
|
||||
where
|
||||
});
|
||||
|
||||
const data = await prisma.pelayananAjuan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
@@ -632,12 +1055,20 @@ const PelayananRoute = new Elysia({
|
||||
category: item.CategoryPelayanan.name,
|
||||
warga: item.Warga.name,
|
||||
status: item.status,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
const dataReturn = {
|
||||
data: dataFix,
|
||||
total: totalData,
|
||||
page: Number(page) || 1,
|
||||
pageSize: !take ? 10 : Number(take),
|
||||
totalPages: Math.ceil(totalData / (!take ? 10 : Number(take)))
|
||||
}
|
||||
|
||||
return dataReturn
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
|
||||
@@ -5,10 +5,10 @@ import { v4 as uuidv4 } from "uuid"
|
||||
import { getLastUpdated } from "../lib/get-last-updated"
|
||||
import { mimeToExtension } from "../lib/mimetypeToExtension"
|
||||
import { generateNoPengaduan } from "../lib/no-pengaduan"
|
||||
import { normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { isValidPhone, normalizePhoneNumber } from "../lib/normalizePhone"
|
||||
import { prisma } from "../lib/prisma"
|
||||
import { renameFile } from "../lib/rename-file"
|
||||
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileBase64 } from "../lib/seafile"
|
||||
import { catFile, defaultConfigSF, removeFile, uploadFile, uploadFileToFolder } from "../lib/seafile"
|
||||
|
||||
const PengaduanRoute = new Elysia({
|
||||
prefix: "pengaduan",
|
||||
@@ -107,8 +107,10 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
|
||||
// --- PENGADUAN ---
|
||||
.post("/create", async ({ body }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId, namaWarga, noTelepon } = body
|
||||
.post("/create", async ({ body, headers }) => {
|
||||
const { judulPengaduan, detailPengaduan, lokasi, namaGambar, kategoriId } = body
|
||||
const namaWarga = headers['x-user'] || ""
|
||||
const noTelepon = headers['x-phone'] || ""
|
||||
let imageFix = namaGambar
|
||||
const noPengaduan = await generateNoPengaduan()
|
||||
let idCategoryFix = kategoriId
|
||||
@@ -128,17 +130,23 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!cariCategory) {
|
||||
idCategoryFix = "lainnya"
|
||||
} else {
|
||||
idCategoryFix = cariCategory.id
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
idCategoryFix = "lainnya"
|
||||
}
|
||||
|
||||
if (!isValidPhone(noTelepon)) {
|
||||
return { success: false, message: `nomor telepon ${noTelepon} tidak valid, harap masukkan nomor yang benar` }
|
||||
}
|
||||
|
||||
const nomorHP = normalizePhoneNumber({ phone: noTelepon })
|
||||
const dataWarga = await prisma.warga.upsert({
|
||||
where: {
|
||||
@@ -212,20 +220,20 @@ const PengaduanRoute = new Elysia({
|
||||
})),
|
||||
|
||||
kategoriId: t.Optional(t.String({
|
||||
examples: ["kebersihan"],
|
||||
description: "ID atau nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
examples: ["kebersihan", "infrastruktur", "keamanan"],
|
||||
description: "Nama kategori pengaduan (contoh: kebersihan, keamanan, lainnya)"
|
||||
})),
|
||||
|
||||
namaWarga: t.Optional(t.String({
|
||||
examples: ["budiman"],
|
||||
description: "Nama warga yang melapor"
|
||||
})),
|
||||
// namaWarga: t.String({
|
||||
// examples: ["budiman"],
|
||||
// description: "Nama warga yang melapor"
|
||||
// }),
|
||||
|
||||
noTelepon: t.String({
|
||||
error: "Nomor telepon harus diisi",
|
||||
examples: ["08123456789", "+628123456789"],
|
||||
description: "Nomor telepon warga pelapor"
|
||||
}),
|
||||
// noTelepon: t.String({
|
||||
// error: "Nomor telepon harus diisi",
|
||||
// examples: ["08123456789", "+628123456789"],
|
||||
// description: "Nomor telepon warga pelapor"
|
||||
// }),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
@@ -285,18 +293,90 @@ const PengaduanRoute = new Elysia({
|
||||
description: `tool untuk update status pengaduan`
|
||||
}
|
||||
})
|
||||
.post("/update", async ({ body }) => {
|
||||
const { noPengaduan, judul, detail, lokasi, namaGambar } = body
|
||||
let dataUpdate = {}
|
||||
|
||||
const cek = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
noPengaduan,
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (!cek) {
|
||||
return { success: false, message: 'gagal update status pengaduan, nomer ' + noPengaduan + ' tidak ditemukan' }
|
||||
}
|
||||
|
||||
if (judul) {
|
||||
dataUpdate = { title: judul }
|
||||
}
|
||||
|
||||
if (detail) {
|
||||
dataUpdate = { ...dataUpdate, detail }
|
||||
}
|
||||
|
||||
if (lokasi) {
|
||||
dataUpdate = { ...dataUpdate, location: lokasi }
|
||||
}
|
||||
|
||||
if (namaGambar) {
|
||||
dataUpdate = { ...dataUpdate, image: namaGambar }
|
||||
}
|
||||
|
||||
const pengaduan = await prisma.pengaduan.updateMany({
|
||||
where: {
|
||||
noPengaduan
|
||||
},
|
||||
data: dataUpdate
|
||||
})
|
||||
|
||||
const keys = Object.keys(dataUpdate).join(", ");
|
||||
|
||||
await prisma.historyPengaduan.create({
|
||||
data: {
|
||||
idPengaduan: cek.id,
|
||||
deskripsi: `Pengaduan diupdate oleh warga (data yg diupdate: ${keys})`,
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, message: 'pengaduan dengan nomer ' + noPengaduan + ' sudah diupdate' }
|
||||
}, {
|
||||
body: t.Object({
|
||||
noPengaduan: t.String({
|
||||
error: "nomer pengaduan harus diisi",
|
||||
description: "Nomer pengaduan yang ingin diupdate"
|
||||
}),
|
||||
judul: t.Optional(t.String({
|
||||
error: "judul harus diisi",
|
||||
description: "Judul pengaduan yang ingin diupdate"
|
||||
})),
|
||||
detail: t.Optional(t.String({
|
||||
description: "detail pengaduan yang ingin diupdate"
|
||||
})),
|
||||
lokasi: t.Optional(t.String({
|
||||
description: "lokasi pengaduan yang ingin diupdate"
|
||||
})),
|
||||
namaGambar: t.Optional(t.String({
|
||||
description: "Nama file gambar yang telah diupload untuk update data pengaduan"
|
||||
})),
|
||||
}),
|
||||
|
||||
detail: {
|
||||
summary: "Update Data Pengaduan",
|
||||
description: `tool untuk update data pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
.get("/detail", async ({ query }) => {
|
||||
const { id } = query
|
||||
|
||||
const data = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
noPengaduan: id
|
||||
}, {
|
||||
id: id
|
||||
}
|
||||
]
|
||||
id: id
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -331,6 +411,16 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
const datafix = {
|
||||
pengaduan: {},
|
||||
history: [],
|
||||
warga: {},
|
||||
}
|
||||
|
||||
return datafix
|
||||
}
|
||||
|
||||
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||
where: {
|
||||
idPengaduan: data?.id,
|
||||
@@ -353,14 +443,7 @@ const PengaduanRoute = new Elysia({
|
||||
const dataHistoryFix = dataHistory.map((item: any) => ({
|
||||
..._.omit(item, ["User", "createdAt"]),
|
||||
nameUser: item.User?.name,
|
||||
createdAt: item.createdAt.toLocaleString("id-ID", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false
|
||||
}),
|
||||
createdAt: item.createdAt
|
||||
}))
|
||||
|
||||
|
||||
@@ -392,50 +475,51 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
|
||||
return datafix
|
||||
|
||||
}, {
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id atau nomer Pengaduan`,
|
||||
tags: ["mcp"]
|
||||
summary: "Detail Pengaduan Warga By ID",
|
||||
description: `tool untuk mendapatkan detail pengaduan warga / history pengaduan / mengecek status pengaduan berdasarkan id pengaduan`,
|
||||
}
|
||||
})
|
||||
.get("/", async ({ query }) => {
|
||||
const { take, page, search, phone } = query
|
||||
const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
.get("/", async ({ query, headers }) => {
|
||||
// const { take, page, search } = query
|
||||
const phone = headers['x-phone'] || ""
|
||||
// const skip = !page ? 0 : (Number(page) - 1) * (!take ? 10 : Number(take))
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
// skip,
|
||||
// take: !take ? 10 : Number(take),
|
||||
orderBy: {
|
||||
createdAt: "asc"
|
||||
},
|
||||
where: {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{
|
||||
title: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
noPengaduan: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
contains: search ?? "",
|
||||
mode: "insensitive"
|
||||
},
|
||||
}
|
||||
],
|
||||
AND: {
|
||||
Warga: {
|
||||
phone: phone
|
||||
}
|
||||
}
|
||||
// OR: [
|
||||
// {
|
||||
// title: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// noPengaduan: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// detail: {
|
||||
// contains: search ?? "",
|
||||
// mode: "insensitive"
|
||||
// },
|
||||
// }
|
||||
// ],
|
||||
// AND: {
|
||||
// Warga: {
|
||||
// phone: phone
|
||||
// }
|
||||
// }
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -470,12 +554,11 @@ const PengaduanRoute = new Elysia({
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
page: t.String({ optional: true }),
|
||||
search: t.String({ optional: true }),
|
||||
phone: t.String({ minLength: 11, error: "phone harus diisi" }),
|
||||
}),
|
||||
// query: t.Object({
|
||||
// take: t.String({ optional: true }),
|
||||
// page: t.String({ optional: true }),
|
||||
// search: t.String({ optional: true }),
|
||||
// }),
|
||||
detail: {
|
||||
summary: "List Pengaduan Warga By Phone",
|
||||
description: `tool untuk mendapatkan list pengaduan warga by phone`,
|
||||
@@ -516,14 +599,57 @@ const PengaduanRoute = new Elysia({
|
||||
detail: {
|
||||
summary: "Upload File (FormData)",
|
||||
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
.post("/upload-file-form-data", async ({ body }) => {
|
||||
const { file } = body;
|
||||
|
||||
// // Validasi file
|
||||
// if (!file) {
|
||||
// return { success: false, message: "File tidak ditemukan" };
|
||||
// }
|
||||
|
||||
// // Rename file
|
||||
// const renamedFile = renameFile({ oldFile: file, newName: 'random' });
|
||||
|
||||
|
||||
// // Upload ke Seafile (pastikan uploadFile menerima Blob atau ArrayBuffer)
|
||||
// // const buffer = await file.arrayBuffer();
|
||||
// const result = await uploadFile(defaultConfigSF, renamedFile, 'pengaduan');
|
||||
// if (result == 'gagal') {
|
||||
// return { success: false, message: "Upload gagal" };
|
||||
// }
|
||||
|
||||
return {
|
||||
success: true,
|
||||
file: JSON.stringify(file),
|
||||
fileInfo: {
|
||||
name: file.name || 'kosong',
|
||||
size: file.size || 0,
|
||||
type: file.type || 'kosong'
|
||||
}
|
||||
// message: "Upload berhasil",
|
||||
// filename: renamedFile.name,
|
||||
// size: renamedFile.size,
|
||||
// seafileResult: result
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
file: t.Any(),
|
||||
// folder: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (FormData)",
|
||||
description: "Tool untuk upload file ke folder tujuan dengan memakai FormData",
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
.post("/upload-base64", async ({ body }) => {
|
||||
const { data, mimetype } = body;
|
||||
const { data, mimetype, kategori } = body;
|
||||
const ext = mimeToExtension(mimetype)
|
||||
const name = `${uuidv4()}.${ext}`
|
||||
const kategoriFix = kategori === 'pengaduan' ? 'pengaduan' : 'syarat-dokumen';
|
||||
|
||||
// Validasi file
|
||||
if (!data) {
|
||||
@@ -535,7 +661,8 @@ const PengaduanRoute = new Elysia({
|
||||
// const base64String = Buffer.from(buffer).toString("base64");
|
||||
|
||||
// (Opsional) jika perlu dikirim ke Seafile sebagai base64
|
||||
const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||
// const result = await uploadFileBase64(defaultConfigSF, { name: name, data: data });
|
||||
const result = await uploadFileToFolder(defaultConfigSF, { name: name, data: data }, kategoriFix);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -544,17 +671,18 @@ const PengaduanRoute = new Elysia({
|
||||
name,
|
||||
mimetype,
|
||||
ext,
|
||||
kategori,
|
||||
}
|
||||
};
|
||||
}, {
|
||||
body: t.Object({
|
||||
data: t.String(),
|
||||
mimetype: t.String()
|
||||
mimetype: t.String(),
|
||||
kategori: t.String()
|
||||
}),
|
||||
detail: {
|
||||
summary: "Upload File (Base64)",
|
||||
description: "Tool untuk upload file ke Seafile dalam format Base64",
|
||||
tags: ["mcp"],
|
||||
consumes: ["multipart/form-data"]
|
||||
},
|
||||
})
|
||||
@@ -601,6 +729,10 @@ const PengaduanRoute = new Elysia({
|
||||
}
|
||||
}
|
||||
|
||||
const totalData = await prisma.pengaduan.count({
|
||||
where
|
||||
});
|
||||
|
||||
const data = await prisma.pengaduan.findMany({
|
||||
skip,
|
||||
take: !take ? 10 : Number(take),
|
||||
@@ -638,12 +770,20 @@ const PengaduanRoute = new Elysia({
|
||||
detail: item.detail,
|
||||
status: item.status,
|
||||
location: item.location,
|
||||
createdAt: item.createdAt.toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" }),
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: 'terakhir diperbarui ' + getLastUpdated(item.updatedAt),
|
||||
}
|
||||
})
|
||||
|
||||
return dataFix
|
||||
const dataReturn = {
|
||||
data: dataFix,
|
||||
total: totalData,
|
||||
page: Number(page) || 1,
|
||||
pageSize: !take ? 10 : Number(take),
|
||||
totalPages: Math.ceil(totalData / (!take ? 10 : Number(take)))
|
||||
}
|
||||
|
||||
return dataReturn
|
||||
}, {
|
||||
query: t.Object({
|
||||
take: t.String({ optional: true }),
|
||||
@@ -745,8 +885,118 @@ const PengaduanRoute = new Elysia({
|
||||
description: "Tool untuk delete file Seafile",
|
||||
},
|
||||
})
|
||||
.post("/detail-data", async ({ body }) => {
|
||||
const { nomerPengaduan } = body
|
||||
|
||||
const data = await prisma.pengaduan.findFirst({
|
||||
where: {
|
||||
noPengaduan: nomerPengaduan
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
noPengaduan: true,
|
||||
title: true,
|
||||
detail: true,
|
||||
location: true,
|
||||
image: true,
|
||||
idCategory: true,
|
||||
idWarga: true,
|
||||
status: true,
|
||||
keterangan: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
CategoryPengaduan: {
|
||||
select: {
|
||||
name: true
|
||||
}
|
||||
},
|
||||
Warga: {
|
||||
select: {
|
||||
name: true,
|
||||
phone: true,
|
||||
_count: {
|
||||
select: {
|
||||
Pengaduan: true,
|
||||
PelayananAjuan: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!data) {
|
||||
return { success: false, message: "Data tidak ditemukan" };
|
||||
}
|
||||
|
||||
const dataHistory = await prisma.historyPengaduan.findMany({
|
||||
where: {
|
||||
idPengaduan: data?.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
deskripsi: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
idUser: true,
|
||||
User: {
|
||||
select: {
|
||||
name: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const dataHistoryFix = dataHistory.map((item: any) => ({
|
||||
..._.omit(item, ["User", "createdAt"]),
|
||||
nameUser: item.User?.name,
|
||||
createdAt: item.createdAt
|
||||
}))
|
||||
|
||||
|
||||
const warga = {
|
||||
name: data?.Warga?.name,
|
||||
phone: data?.Warga?.phone,
|
||||
pengaduan: data?.Warga?._count.Pengaduan,
|
||||
pelayanan: data?.Warga?._count.PelayananAjuan,
|
||||
}
|
||||
|
||||
const dataPengaduan = {
|
||||
id: data?.id,
|
||||
noPengaduan: data?.noPengaduan,
|
||||
title: data?.title,
|
||||
detail: data?.detail,
|
||||
location: data?.location,
|
||||
image: data?.image,
|
||||
category: data?.CategoryPengaduan.name,
|
||||
status: data?.status,
|
||||
keterangan: data?.keterangan,
|
||||
createdAt: data?.createdAt,
|
||||
updatedAt: data?.updatedAt,
|
||||
}
|
||||
|
||||
const datafix = {
|
||||
pengaduan: dataPengaduan,
|
||||
history: dataHistoryFix,
|
||||
warga: warga,
|
||||
}
|
||||
|
||||
return datafix
|
||||
}, {
|
||||
body: t.Object({
|
||||
nomerPengaduan: t.String({
|
||||
description: "Nomer pengaduan yg ingin diakses",
|
||||
examples: ["PGD-101225-001", "PGD-101225-002"],
|
||||
error: "Nomer pengaduan harus diisi",
|
||||
}),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Detail Pengaduan Warga By Nomor Pengaduan",
|
||||
description: `tool untuk mendapatkan detail data pengaduan berdasarkan nomor pengaduan`,
|
||||
tags: ["mcp"]
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
export default PengaduanRoute
|
||||
|
||||
@@ -9,9 +9,31 @@ const WargaRoute = new Elysia({
|
||||
})
|
||||
|
||||
.get("/list", async ({ query }) => {
|
||||
const { search } = query
|
||||
const { search, page = 1 } = query
|
||||
const dataSkip = page == null || page == undefined ? 0 : Number(page) * 10 - 10;
|
||||
|
||||
const totalData = await prisma.warga.count({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
name: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
},
|
||||
{
|
||||
phone: {
|
||||
contains: search,
|
||||
mode: "insensitive"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const data = await prisma.warga.findMany({
|
||||
skip: dataSkip,
|
||||
take: 10,
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
@@ -33,7 +55,15 @@ const WargaRoute = new Elysia({
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
const dataFix = {
|
||||
data,
|
||||
total: totalData,
|
||||
page: Number(page) || 1,
|
||||
pageSize: 10,
|
||||
totalPages: Math.ceil(totalData / 10)
|
||||
};
|
||||
|
||||
return dataFix
|
||||
}, {
|
||||
detail: {
|
||||
summary: "List Warga",
|
||||
|
||||
Reference in New Issue
Block a user