Compare commits
13 Commits
amalia/08-
...
amalia/13-
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fed41cbe8 | |||
| fc387fe8e6 | |||
| 80df579499 | |||
| 5bbbc15c27 | |||
| 82765f6ef0 | |||
| e8b5720118 | |||
| 01334ec573 | |||
| 98ad9b0d72 | |||
| c0471f47f3 | |||
| 3d641d2035 | |||
| 7de5078868 | |||
| ea5072d9ab | |||
| d63bf024d3 |
44
src/components/BreadCrumbs.tsx
Normal file
44
src/components/BreadCrumbs.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { ActionIcon, Anchor, Breadcrumbs, Card, Group } from "@mantine/core";
|
||||||
|
import { IconChevronLeft } from "@tabler/icons-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function BreadCrumbs({ dataLink, back, linkBack }: { dataLink: { title: string, link: string, active: boolean }[], back?: boolean, linkBack?: string }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="sm"
|
||||||
|
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 20px rgba(0,255,200,0.08)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
{
|
||||||
|
back &&
|
||||||
|
<ActionIcon variant="outline" aria-label="Settings" radius={"lg"} onClick={() => window.history.back()}>
|
||||||
|
<IconChevronLeft size={20} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
<Breadcrumbs>
|
||||||
|
{
|
||||||
|
dataLink.map((item, index) => (
|
||||||
|
<Anchor
|
||||||
|
c={item.active ? "gray.0" : "gray.5"}
|
||||||
|
onClick={() => item.active || item.link == "#" ? null : navigate(item.link)}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Anchor>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Breadcrumbs>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -207,7 +207,7 @@ export default function DesaSetting({
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{list?.map((v: any) => (
|
{list.length > 0 && list?.map((v: any) => (
|
||||||
<Table.Tr key={v.id}>
|
<Table.Tr key={v.id}>
|
||||||
<Table.Td>{v.name}</Table.Td>
|
<Table.Td>{v.name}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
|
|||||||
@@ -4,19 +4,17 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
|
||||||
Group,
|
Group,
|
||||||
Input,
|
|
||||||
List,
|
List,
|
||||||
Modal,
|
Modal,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconEdit, IconEye, IconPlus, IconTrash } from "@tabler/icons-react";
|
import { IconEye, IconTrash } from "@tabler/icons-react";
|
||||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -625,7 +623,7 @@ export default function KategoriPelayananSurat({
|
|||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
</Table.Thead>
|
</Table.Thead>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
{list?.map((v: any) => (
|
{list.length > 0 && list?.map((v: any) => (
|
||||||
<Table.Tr key={v.id}>
|
<Table.Tr key={v.id}>
|
||||||
<Table.Td>{v.name}</Table.Td>
|
<Table.Td>{v.name}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import clientRoutes from "@/clientRoutes";
|
import clientRoutes from "@/clientRoutes";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Center,
|
||||||
Group,
|
Paper,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import apiFetch from "../lib/apiFetch";
|
import apiFetch from "../lib/apiFetch";
|
||||||
@@ -73,25 +73,73 @@ export default function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Center
|
||||||
<Stack>
|
h="100vh"
|
||||||
<Text>Login</Text>
|
style={{
|
||||||
<TextInput
|
background:
|
||||||
placeholder="Email"
|
"radial-gradient(circle at top, #1f2d2b 0%, #0b0f0e 60%)",
|
||||||
value={email}
|
}}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
>
|
||||||
/>
|
<Paper
|
||||||
<PasswordInput
|
radius="lg"
|
||||||
placeholder="Password"
|
p="xl"
|
||||||
value={password}
|
w={420}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
style={{
|
||||||
/>
|
background: "rgba(20, 20, 20, 0.75)",
|
||||||
<Group justify="right">
|
backdropFilter: "blur(12px)",
|
||||||
<Button onClick={handleSubmit} disabled={loading}>
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||||
|
boxShadow: "0 20px 60px rgba(0,0,0,0.6)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Text
|
||||||
|
size="xl"
|
||||||
|
fw={700}
|
||||||
|
ta="center"
|
||||||
|
c="white"
|
||||||
|
>
|
||||||
|
Welcome Back
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
size="sm"
|
||||||
|
ta="center"
|
||||||
|
c="dimmed"
|
||||||
|
>
|
||||||
|
Sign in to continue to your dashboard
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Email"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
label="Password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
mt="md"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: "teal", to: "cyan", deg: 45 }}
|
||||||
|
style={{
|
||||||
|
transition: "all 0.2s ease",
|
||||||
|
}}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Stack>
|
||||||
</Stack>
|
</Paper>
|
||||||
</Container>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default function DashboardLayout() {
|
|||||||
<AppShell
|
<AppShell
|
||||||
padding="lg"
|
padding="lg"
|
||||||
navbar={{
|
navbar={{
|
||||||
width: 260,
|
width: 300,
|
||||||
breakpoint: "sm",
|
breakpoint: "sm",
|
||||||
collapsed: { mobile: !opened, desktop: !opened },
|
collapsed: { mobile: !opened, desktop: !opened },
|
||||||
}}
|
}}
|
||||||
@@ -290,22 +290,31 @@ function NavigationDashboard() {
|
|||||||
.map((item) => (
|
.map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.path}
|
key={item.path}
|
||||||
active={isActive(item.path as keyof typeof clientRoute)}
|
active={isActive(item.path as keyof typeof clientRoute) ||
|
||||||
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||||
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||||
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")}
|
||||||
leftSection={item.icon}
|
leftSection={item.icon}
|
||||||
label={
|
label={
|
||||||
<Flex align="center" gap={6}>
|
<Flex align="center" gap={6}>
|
||||||
<Text fw={500}>{item.label}</Text>
|
<Text fw={500}>{item.label}</Text>
|
||||||
{isActive(item.path as keyof typeof clientRoute) && (
|
{(
|
||||||
<Badge
|
isActive(item.path as keyof typeof clientRoute) ||
|
||||||
variant="light"
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||||
color="teal"
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||||
radius="sm"
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
||||||
size="xs"
|
)
|
||||||
style={{ textTransform: "none" }}
|
&& (
|
||||||
>
|
<Badge
|
||||||
Active
|
variant="light"
|
||||||
</Badge>
|
color="teal"
|
||||||
)}
|
radius="sm"
|
||||||
|
size="xs"
|
||||||
|
style={{ textTransform: "none" }}
|
||||||
|
>
|
||||||
|
Active
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
@@ -313,7 +322,10 @@ function NavigationDashboard() {
|
|||||||
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
navigate(clientRoutes[item.path as keyof typeof clientRoute])
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isActive(item.path as keyof typeof clientRoute)
|
backgroundColor: isActive(item.path as keyof typeof clientRoute) ||
|
||||||
|
(location.pathname == "/scr/dashboard/pelayanan-surat/detail-pelayanan" && item.path == "/scr/dashboard/pelayanan-surat/list-pelayanan") ||
|
||||||
|
(location.pathname == "/scr/dashboard/pengaduan/detail" && item.path == "/scr/dashboard/pengaduan/list") ||
|
||||||
|
(location.pathname == "/scr/dashboard/warga/detail-warga" && item.path == "/scr/dashboard/warga/list-warga")
|
||||||
? "rgba(0,255,200,0.1)"
|
? "rgba(0,255,200,0.1)"
|
||||||
: "transparent",
|
: "transparent",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||||
import ModalFile from "@/components/ModalFile";
|
import ModalFile from "@/components/ModalFile";
|
||||||
import ModalSurat from "@/components/ModalSurat";
|
import ModalSurat from "@/components/ModalSurat";
|
||||||
import notification from "@/components/notificationGlobal";
|
import notification from "@/components/notificationGlobal";
|
||||||
@@ -49,6 +50,11 @@ import { useLocation } from "react-router-dom";
|
|||||||
import useSwr from "swr";
|
import useSwr from "swr";
|
||||||
|
|
||||||
export default function DetailPengajuanPage() {
|
export default function DetailPengajuanPage() {
|
||||||
|
const dataMenu = [
|
||||||
|
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||||
|
{ title: "Pelayanan Surat", link: "/scr/dashboard/pelayanan-surat/list-pelayanan", active: false },
|
||||||
|
{ title: "Detail Pengajuan Surat", link: "#", active: true },
|
||||||
|
];
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const id = query.get("id");
|
const id = query.get("id");
|
||||||
@@ -67,6 +73,9 @@ export default function DetailPengajuanPage() {
|
|||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<BreadCrumbs dataLink={dataMenu} back />
|
||||||
|
</Grid.Col>
|
||||||
<Grid.Col span={8}>
|
<Grid.Col span={8}>
|
||||||
<Stack gap={"xl"}>
|
<Stack gap={"xl"}>
|
||||||
<DetailDataPengajuan
|
<DetailDataPengajuan
|
||||||
@@ -266,6 +275,7 @@ function DetailDataPengajuan({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
|
console.log('jalan', viewImg)
|
||||||
if (viewImg) {
|
if (viewImg) {
|
||||||
setOpenedPreviewFile(true);
|
setOpenedPreviewFile(true);
|
||||||
}
|
}
|
||||||
@@ -365,6 +375,7 @@ function DetailDataPengajuan({
|
|||||||
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
|
open={openedPreviewFile && !_.isEmpty(viewImg.file)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setOpenedPreviewFile(false);
|
setOpenedPreviewFile(false);
|
||||||
|
setViewImg({ file: "", folder: "" })
|
||||||
}}
|
}}
|
||||||
folder={viewImg.folder}
|
folder={viewImg.folder}
|
||||||
fileName={viewImg.file}
|
fileName={viewImg.file}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||||
import ModalFile from "@/components/ModalFile";
|
import ModalFile from "@/components/ModalFile";
|
||||||
import notification from "@/components/notificationGlobal";
|
import notification from "@/components/notificationGlobal";
|
||||||
import apiFetch from "@/lib/apiFetch";
|
import apiFetch from "@/lib/apiFetch";
|
||||||
@@ -40,6 +41,11 @@ import useSwr from "swr";
|
|||||||
|
|
||||||
|
|
||||||
export default function DetailPengaduanPage() {
|
export default function DetailPengaduanPage() {
|
||||||
|
const dataMenu = [
|
||||||
|
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||||
|
{ title: "Pengaduan", link: "/scr/dashboard/pengaduan/list", active: false },
|
||||||
|
{ title: "Detail Pengaduan", link: "#", active: true },
|
||||||
|
];
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const id = query.get("id");
|
const id = query.get("id");
|
||||||
@@ -58,6 +64,9 @@ export default function DetailPengaduanPage() {
|
|||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<BreadCrumbs dataLink={dataMenu} back />
|
||||||
|
</Grid.Col>
|
||||||
<Grid.Col span={8}>
|
<Grid.Col span={8}>
|
||||||
<Stack gap={"xl"}>
|
<Stack gap={"xl"}>
|
||||||
<DetailDataPengaduan
|
<DetailDataPengaduan
|
||||||
@@ -93,6 +102,7 @@ function DetailDataPengaduan({
|
|||||||
const [keterangan, setKeterangan] = useState("");
|
const [keterangan, setKeterangan] = useState("");
|
||||||
const [host, setHost] = useState<User | null>(null);
|
const [host, setHost] = useState<User | null>(null);
|
||||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchHost() {
|
async function fetchHost() {
|
||||||
@@ -111,6 +121,7 @@ function DetailDataPengaduan({
|
|||||||
|
|
||||||
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
const handleKonfirmasi = async (cat: "terima" | "tolak") => {
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
const res = await apiFetch.api.pengaduan["update-status"].post({
|
const res = await apiFetch.api.pengaduan["update-status"].post({
|
||||||
id: data?.id,
|
id: data?.id,
|
||||||
status:
|
status:
|
||||||
@@ -184,6 +195,8 @@ function DetailDataPengaduan({
|
|||||||
message: "Failed to update pengaduan",
|
message: "Failed to update pengaduan",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,6 +231,7 @@ function DetailDataPengaduan({
|
|||||||
color="red"
|
color="red"
|
||||||
disabled={keterangan.length < 1}
|
disabled={keterangan.length < 1}
|
||||||
onClick={() => handleKonfirmasi("tolak")}
|
onClick={() => handleKonfirmasi("tolak")}
|
||||||
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
Tolak
|
Tolak
|
||||||
</Button>
|
</Button>
|
||||||
@@ -242,6 +256,7 @@ function DetailDataPengaduan({
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
color="green"
|
color="green"
|
||||||
onClick={() => handleKonfirmasi("terima")}
|
onClick={() => handleKonfirmasi("terima")}
|
||||||
|
loading={isLoading}
|
||||||
>
|
>
|
||||||
Ya
|
Ya
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||||
import DesaSetting from "@/components/DesaSetting";
|
import DesaSetting from "@/components/DesaSetting";
|
||||||
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
import KategoriPelayananSurat from "@/components/KategoriPelayananSurat";
|
||||||
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
import KategoriPengaduan from "@/components/KategoriPengaduan";
|
||||||
@@ -15,14 +16,21 @@ import {
|
|||||||
IconUsersGroup,
|
IconUsersGroup,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { JsonValue } from "generated/prisma/runtime/library";
|
import type { JsonValue } from "generated/prisma/runtime/library";
|
||||||
|
import _ from "lodash";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function DetailSettingPage() {
|
export default function DetailSettingPage() {
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const type = query.get("type");
|
const type = query.get("type");
|
||||||
|
const navigate = useNavigate();
|
||||||
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
const [permissions, setPermissions] = useState<JsonValue[]>([]);
|
||||||
|
const dataMenu = [
|
||||||
|
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||||
|
{ title: "Setting", link: "#", active: false },
|
||||||
|
{ title: type == "cat-pengaduan" ? "Kategori Pengaduan" : type == "cat-pelayanan" ? "Kategori Pelayanan Surat" : type ? _.upperFirst(type) : "Profile", link: "#", active: true },
|
||||||
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchPermissions() {
|
async function fetchPermissions() {
|
||||||
@@ -87,6 +95,9 @@ export default function DetailSettingPage() {
|
|||||||
return (
|
return (
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<BreadCrumbs dataLink={dataMenu} back />
|
||||||
|
</Grid.Col>
|
||||||
<Grid.Col span={3}>
|
<Grid.Col span={3}>
|
||||||
<Card
|
<Card
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -104,7 +115,7 @@ export default function DetailSettingPage() {
|
|||||||
.map((item) => (
|
.map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.key}
|
key={item.key}
|
||||||
href={"?type=" + item.path}
|
onClick={()=>{navigate("?type=" + item.path)}}
|
||||||
label={item.label}
|
label={item.label}
|
||||||
leftSection={item.icon}
|
leftSection={item.icon}
|
||||||
active={
|
active={
|
||||||
|
|||||||
@@ -1,62 +1,75 @@
|
|||||||
|
import BreadCrumbs from "@/components/BreadCrumbs";
|
||||||
|
import notification from "@/components/notificationGlobal";
|
||||||
import apiFetch from "@/lib/apiFetch";
|
import apiFetch from "@/lib/apiFetch";
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
CloseButton,
|
||||||
Container,
|
Container,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
|
Input,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
|
Pagination,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
import { IconPhone } from "@tabler/icons-react";
|
import { IconPhone, IconSearch } from "@tabler/icons-react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { useState } from "react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import useSwr from "swr";
|
|
||||||
|
|
||||||
export default function DetailWargaPage() {
|
export default function DetailWargaPage() {
|
||||||
|
const dataMenu = [
|
||||||
|
{ title: "Dashboard", link: "/scr/dashboard/dashboard-home", active: false },
|
||||||
|
{ title: "Warga", link: "/scr/dashboard/warga/list-warga", active: false },
|
||||||
|
{ title: "Detail Warga", link: "#", active: true },
|
||||||
|
];
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const id = query.get("id");
|
const id = query.get("id");
|
||||||
const { data, mutate, isLoading } = useSwr("/", () =>
|
// const { data, mutate, isLoading } = useSwr("/", () =>
|
||||||
apiFetch.api.warga.detail.get({
|
// apiFetch.api.warga.detail.get({
|
||||||
query: {
|
// query: {
|
||||||
id: id!,
|
// id: id!,
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
);
|
// );
|
||||||
|
|
||||||
useShallowEffect(() => {
|
// useShallowEffect(() => {
|
||||||
mutate();
|
// mutate();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoadingOverlay
|
<LoadingOverlay
|
||||||
visible={isLoading}
|
// visible={isLoading}
|
||||||
zIndex={1000}
|
zIndex={1000}
|
||||||
overlayProps={{ radius: "sm", blur: 2 }}
|
overlayProps={{ radius: "sm", blur: 2 }}
|
||||||
/>
|
/>
|
||||||
<Container size="xl" py="xl" w={"100%"}>
|
<Container size="xl" py="xl" w={"100%"}>
|
||||||
<Grid>
|
<Grid>
|
||||||
|
<Grid.Col span={12}>
|
||||||
|
<BreadCrumbs dataLink={dataMenu} back />
|
||||||
|
</Grid.Col>
|
||||||
<Grid.Col span={4}>
|
<Grid.Col span={4}>
|
||||||
<DetailWarga data={data?.data?.warga} />
|
<DetailWarga id={id!} />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col span={8}>
|
<Grid.Col span={8}>
|
||||||
<Stack gap={"xl"}>
|
<Stack gap={"xl"}>
|
||||||
<DetailDataHistori
|
<DetailDataHistori
|
||||||
data={data?.data?.pengaduan}
|
id={id!}
|
||||||
kategori="pengaduan"
|
kategori="pengaduan"
|
||||||
/>
|
/>
|
||||||
<DetailDataHistori
|
<DetailDataHistori
|
||||||
data={data?.data?.pelayanan}
|
id={id!}
|
||||||
kategori="pelayanan"
|
kategori="pelayanan"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -68,13 +81,66 @@ export default function DetailWargaPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DetailDataHistori({
|
function DetailDataHistori({
|
||||||
data,
|
id,
|
||||||
kategori,
|
kategori,
|
||||||
}: {
|
}: {
|
||||||
data: any;
|
id: string;
|
||||||
kategori: "pengaduan" | "pelayanan";
|
kategori: "pengaduan" | "pelayanan";
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [data, setData] = useState<any>([]);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const [totalRows, setTotalRows] = useState(0);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
async function getData() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch.api.warga.detail.get({
|
||||||
|
query: {
|
||||||
|
id,
|
||||||
|
category: kategori,
|
||||||
|
page: String(page),
|
||||||
|
search
|
||||||
|
}
|
||||||
|
}) as { data: { success: boolean; data: any[]; totalPages: number, totalRows: number } };
|
||||||
|
|
||||||
|
if (res?.data?.success) {
|
||||||
|
setData(res.data.data)
|
||||||
|
setTotalPages(res?.data?.totalPages)
|
||||||
|
setTotalRows(res?.data?.totalRows)
|
||||||
|
} else {
|
||||||
|
setData([])
|
||||||
|
setTotalPages(1)
|
||||||
|
setTotalRows(0)
|
||||||
|
notification({
|
||||||
|
title: "Failed",
|
||||||
|
message: "Failed to get data",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notification({
|
||||||
|
title: "Failed",
|
||||||
|
message: "Failed to get data",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
getData()
|
||||||
|
}, [page])
|
||||||
|
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
setPage(1)
|
||||||
|
if (page == 1) {
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@@ -93,6 +159,36 @@ function DetailDataHistori({
|
|||||||
<Title order={4} c="gray.2">
|
<Title order={4} c="gray.2">
|
||||||
Histori {_.upperFirst(kategori)}
|
Histori {_.upperFirst(kategori)}
|
||||||
</Title>
|
</Title>
|
||||||
|
<Flex
|
||||||
|
gap="md"
|
||||||
|
justify="flex-start"
|
||||||
|
align="center"
|
||||||
|
direction="row"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
placeholder="Cari data..."
|
||||||
|
onChange={(event) => setSearch(event.currentTarget.value)}
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
rightSectionPointerEvents="all"
|
||||||
|
rightSection={
|
||||||
|
<CloseButton
|
||||||
|
aria-label="Clear input"
|
||||||
|
onClick={() => setSearch("")}
|
||||||
|
style={{ display: search ? undefined : "none" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Text size="sm" c="gray.5" >
|
||||||
|
{`${5 * (page - 1) + 1} – ${Math.min(totalRows, 5 * page)} of ${totalRows}`}
|
||||||
|
</Text>
|
||||||
|
<Pagination
|
||||||
|
total={totalPages}
|
||||||
|
value={page}
|
||||||
|
onChange={setPage}
|
||||||
|
withPages={false}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Divider my={0} />
|
<Divider my={0} />
|
||||||
<Table>
|
<Table>
|
||||||
@@ -110,7 +206,7 @@ function DetailDataHistori({
|
|||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
data?.map((item: any, index: number) => (
|
data?.map((item: any, index: number) => (
|
||||||
<Table.Tr key={index}>
|
<Table.Tr key={index}>
|
||||||
<Table.Td>{item.noPengaduan}</Table.Td>
|
<Table.Td w={"180"}>{item.noPengaduan}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{kategori == "pengaduan" ? item.title : item.category}
|
{kategori == "pengaduan" ? item.title : item.category}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
@@ -121,11 +217,11 @@ function DetailDataHistori({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
kategori == "pengaduan"
|
kategori == "pengaduan"
|
||||||
? navigate(
|
? navigate(
|
||||||
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
`/scr/dashboard/pengaduan/detail?id=${item.id}`,
|
||||||
)
|
)
|
||||||
: navigate(
|
: navigate(
|
||||||
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
`/scr/dashboard/pelayanan-surat/detail-pelayanan?id=${item.id}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
@@ -147,7 +243,33 @@ function DetailDataHistori({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailWarga({ data }: { data: any }) {
|
function DetailWarga({ id }: { id: string }) {
|
||||||
|
const [data, setData] = useState<any>(null);
|
||||||
|
|
||||||
|
async function getWarga() {
|
||||||
|
try {
|
||||||
|
const res = await apiFetch.api.warga.detail.get({
|
||||||
|
query: {
|
||||||
|
id: id,
|
||||||
|
category: "warga",
|
||||||
|
page: "1",
|
||||||
|
search: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setData(res.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notification({
|
||||||
|
title: "Failed",
|
||||||
|
message: "Failed to get data warga",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
getWarga();
|
||||||
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -97,68 +97,137 @@ const WargaRoute = new Elysia({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get("/detail", async ({ query }) => {
|
.get("/detail", async ({ query }) => {
|
||||||
const { id } = query
|
const { id, category, search, page } = query
|
||||||
|
const skip = !page ? 0 : (Number(page) - 1) * 5
|
||||||
|
|
||||||
const dataWarga = await prisma.warga.findUnique({
|
const dataWarga = await prisma.warga.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const dataPengaduan = await prisma.pengaduan.findMany({
|
if (!dataWarga)
|
||||||
orderBy: {
|
return { success: false, message: "data warga tidak ditemukan", data: null, totalPages: 1, totalRows: 0 }
|
||||||
createdAt: "desc"
|
|
||||||
},
|
if (category == "warga") {
|
||||||
where: {
|
return dataWarga
|
||||||
|
} else if (category == "pengaduan") {
|
||||||
|
const where: any = {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
idWarga: id
|
idWarga: id,
|
||||||
},
|
OR: [
|
||||||
select: {
|
{
|
||||||
id: true,
|
title: {
|
||||||
status: true,
|
contains: search ?? "",
|
||||||
noPengaduan: true,
|
mode: "insensitive"
|
||||||
title: true
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noPengaduan: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
|
const totalData = await prisma.pengaduan.count({
|
||||||
|
where
|
||||||
|
});
|
||||||
|
const dataPengaduan = await prisma.pengaduan.findMany({
|
||||||
|
skip,
|
||||||
|
take: 5,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc"
|
||||||
|
},
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
noPengaduan: true,
|
||||||
|
title: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataReturn = {
|
||||||
|
success: true,
|
||||||
|
message: "data pengaduan berhasil diambil",
|
||||||
|
data: dataPengaduan,
|
||||||
|
totalRows: totalData,
|
||||||
|
totalPages: Math.ceil(totalData / 5)
|
||||||
|
}
|
||||||
|
|
||||||
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
return dataReturn
|
||||||
orderBy: {
|
} else if (category == "pelayanan") {
|
||||||
createdAt: "desc"
|
const where: any = {
|
||||||
},
|
|
||||||
where: {
|
|
||||||
isActive: true,
|
isActive: true,
|
||||||
idWarga: id
|
idWarga: id,
|
||||||
},
|
OR: [
|
||||||
select: {
|
{
|
||||||
id: true,
|
CategoryPelayanan: {
|
||||||
noPengajuan: true,
|
name: {
|
||||||
status: true,
|
contains: search ?? "",
|
||||||
CategoryPelayanan: {
|
mode: "insensitive"
|
||||||
select: {
|
},
|
||||||
name: true
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
noPengajuan: {
|
||||||
|
contains: search ?? "",
|
||||||
|
mode: "insensitive"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalData = await prisma.pelayananAjuan.count({
|
||||||
|
where
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataPelayanan = await prisma.pelayananAjuan.findMany({
|
||||||
|
skip,
|
||||||
|
take: 5,
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc"
|
||||||
|
},
|
||||||
|
where,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
noPengajuan: true,
|
||||||
|
status: true,
|
||||||
|
CategoryPelayanan: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataPelayanFix = dataPelayanan.map((v: any) => ({
|
||||||
|
..._.omit(v, ["CategoryPelayanan"]),
|
||||||
|
id: v.id,
|
||||||
|
noPengaduan: v.noPengajuan,
|
||||||
|
status: v.status,
|
||||||
|
category: v.CategoryPelayanan.name
|
||||||
|
}))
|
||||||
|
|
||||||
|
const dataReturn = {
|
||||||
|
success: true,
|
||||||
|
message: "data pelayanan berhasil diambil",
|
||||||
|
data: dataPelayanFix,
|
||||||
|
totalRows: totalData,
|
||||||
|
totalPages: Math.ceil(totalData / 5)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const dataPelayanFix = dataPelayanan.map((v: any) => ({
|
return dataReturn
|
||||||
..._.omit(v, ["CategoryPelayanan"]),
|
|
||||||
id: v.id,
|
|
||||||
noPengaduan: v.noPengajuan,
|
|
||||||
status: v.status,
|
|
||||||
category: v.CategoryPelayanan.name
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
warga: dataWarga,
|
|
||||||
pengaduan: dataPengaduan,
|
|
||||||
pelayanan: dataPelayanFix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}, {
|
}, {
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
id: t.String({ minLength: 1, error: "id harus diisi" })
|
id: t.String({ minLength: 1, error: "id harus diisi" }),
|
||||||
|
category: t.String({ minLength: 1, error: "kategori harus diisi" }),
|
||||||
|
page: t.String({ optional: true }),
|
||||||
|
search: t.String({ optional: true }),
|
||||||
}),
|
}),
|
||||||
detail: {
|
detail: {
|
||||||
summary: "Detail Warga",
|
summary: "Detail Warga",
|
||||||
|
|||||||
Reference in New Issue
Block a user