Sinkronisasi UI & API Admin - User Submenu Pendapatan Asli Desa
This commit is contained in:
@@ -292,6 +292,9 @@ model PosisiOrganisasiPPID {
|
||||
pegawai PegawaiPPID[]
|
||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||
parentId String?
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||
}
|
||||
|
||||
@@ -348,18 +348,34 @@ const posisiOrganisasi = proxy({
|
||||
deskripsi: string | null;
|
||||
hierarki: number;
|
||||
}>,
|
||||
async load() {
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
posisiOrganisasi.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||
posisiOrganisasi.findMany.page = page;
|
||||
posisiOrganisasi.findMany.search = search;
|
||||
|
||||
try {
|
||||
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
||||
"find-many"
|
||||
].get();
|
||||
if (res.status === 200) {
|
||||
// The API now returns the id field, so we can use it directly
|
||||
this.data = res.data?.data ?? [];
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
posisiOrganisasi.findMany.data = res.data.data ?? [];
|
||||
posisiOrganisasi.findMany.totalPages = res.data.totalPages ?? 1;
|
||||
} else {
|
||||
posisiOrganisasi.findMany.data = [];
|
||||
posisiOrganisasi.findMany.totalPages = 1;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Find many error:", error);
|
||||
this.data = [];
|
||||
} catch (err) {
|
||||
console.error("Gagal fetch posisi organisasi paginated:", err);
|
||||
posisiOrganisasi.findMany.data = [];
|
||||
posisiOrganisasi.findMany.totalPages = 1;
|
||||
} finally {
|
||||
posisiOrganisasi.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -438,9 +454,9 @@ const pegawai = proxy({
|
||||
|
||||
try {
|
||||
pegawai.create.loading = true;
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||
"create"
|
||||
].post(pegawai.create.form);
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
|
||||
pegawai.create.form
|
||||
);
|
||||
if (res.status === 200) {
|
||||
toast.success("Pegawai berhasil ditambahkan");
|
||||
await pegawai.findMany.load();
|
||||
@@ -457,20 +473,33 @@ const pegawai = proxy({
|
||||
},
|
||||
|
||||
// In struktur-organisasi.ts
|
||||
findMany: {
|
||||
data: null as any[] | null,
|
||||
findMany: {
|
||||
data: null as
|
||||
| Prisma.PegawaiPPIDGetPayload<{
|
||||
include: {
|
||||
image: true;
|
||||
posisi: true;
|
||||
};
|
||||
}>[]
|
||||
| null,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
total: 0,
|
||||
loading: false,
|
||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
pegawai.findMany.loading = true; // Use the full path to access the property
|
||||
pegawai.findMany.page = page;
|
||||
pegawai.findMany.search = search;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
|
||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||
"find-many"
|
||||
].get({
|
||||
query: { page, limit },
|
||||
query,
|
||||
});
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
@@ -492,7 +521,7 @@ findMany: {
|
||||
pegawai.findMany.loading = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
findUnique: {
|
||||
data: null as
|
||||
| (Prisma.PegawaiGetPayload<{
|
||||
@@ -521,12 +550,9 @@ findMany: {
|
||||
if (!id) return toast.warn("ID tidak valid");
|
||||
try {
|
||||
pegawai.delete.loading = true;
|
||||
const res = await fetch(
|
||||
`/api/ppid/strukturppid/pegawai/del/${id}`,
|
||||
{
|
||||
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
});
|
||||
const json = await res.json();
|
||||
if (res.ok) {
|
||||
toast.success(json.message ?? "Berhasil hapus pegawai");
|
||||
@@ -555,15 +581,12 @@ findMany: {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/ppid/strukturppid/pegawai/${id}`,
|
||||
{
|
||||
const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
@@ -677,7 +700,7 @@ findMany: {
|
||||
const stateStrukturPPID = proxy({
|
||||
stateStruktur,
|
||||
posisiOrganisasi,
|
||||
pegawai
|
||||
pegawai,
|
||||
});
|
||||
|
||||
export default stateStrukturPPID;
|
||||
|
||||
@@ -4,7 +4,7 @@ import colors from '@/con/colors';
|
||||
import { Badge, Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, ThemeIcon } from '@mantine/core';
|
||||
import { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import HeaderSearch from '../../../_com/header';
|
||||
import JudulList from '../../../_com/judulList';
|
||||
@@ -39,22 +39,10 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
||||
} = stateOrganisasi.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
load(page, 10);
|
||||
}, [page]);
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!data) return [];
|
||||
return data.filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.namaLengkap?.toLowerCase().includes(keyword) ||
|
||||
item.gelarAkademik?.toLowerCase().includes(keyword) ||
|
||||
item.telepon?.toLowerCase().includes(keyword) ||
|
||||
item.posisi?.nama?.toLowerCase().includes(keyword)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => a.posisi?.hierarki - b.posisi?.hierarki);
|
||||
}, [data, search]);
|
||||
const filteredData = data || []
|
||||
|
||||
// Handle loading state
|
||||
if (loading || !data) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text } from '@mantine/core';
|
||||
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -33,9 +33,17 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
const [modalHapus, setModalHapus] = useState(false)
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateOrganisasi.findMany;
|
||||
|
||||
useEffect(() => {
|
||||
stateOrganisasi.findMany.load()
|
||||
}, [])
|
||||
load(page, 10, search);
|
||||
}, [page, search]);
|
||||
|
||||
const handleHapus = async () => {
|
||||
if (selectedId) {
|
||||
@@ -45,17 +53,9 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
|
||||
const keyword = search.toLowerCase();
|
||||
return (
|
||||
item.nama?.toLowerCase().includes(keyword) ||
|
||||
item.deskripsi?.toLowerCase().includes(keyword) ||
|
||||
item.hierarki?.toString().toLowerCase().includes(keyword)
|
||||
);
|
||||
})
|
||||
.sort((a, b) => a.hierarki - b.hierarki);
|
||||
const filteredData = data || []
|
||||
|
||||
if (!stateOrganisasi.findMany.data) {
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py={10}>
|
||||
<Skeleton h={500} />
|
||||
@@ -120,6 +120,14 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
||||
</Table>
|
||||
</Box>
|
||||
</Paper>
|
||||
<Center>
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
my={"md"}
|
||||
/>
|
||||
</Center>
|
||||
{/* Modal Hapus */}
|
||||
<ModalKonfirmasiHapus
|
||||
opened={modalHapus}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
@@ -5,12 +6,24 @@ import { Context } from "elysia";
|
||||
export default async function pegawaiFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || '';
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ namaLengkap: { contains: search, mode: 'insensitive' } },
|
||||
{ alamat: { contains: search, mode: 'insensitive' } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.pegawaiPPID.findMany({
|
||||
where: { isActive: true },
|
||||
where,
|
||||
include: {
|
||||
posisi: true,
|
||||
image: true,
|
||||
@@ -20,7 +33,7 @@ export default async function pegawaiFindMany(context: Context) {
|
||||
orderBy: { posisi: { hierarki: 'asc' } },
|
||||
}),
|
||||
prisma.pegawaiPPID.count({
|
||||
where: { isActive: true }
|
||||
where,
|
||||
})
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,17 +1,58 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// /api/posisi-organisasi/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
export default async function posisiOrganisasiFindMany() {
|
||||
const data = await prisma.posisiOrganisasiPPID.findMany();
|
||||
async function posisiOrganisasiFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ deskripsi: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const whereClause = {
|
||||
...where,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.posisiOrganisasiPPID.findMany({
|
||||
where: whereClause,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
orderBy: { hierarki: "asc" },
|
||||
}),
|
||||
prisma.posisiOrganisasiPPID.count({ where: whereClause }),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Berhasil mengambil semua data posisi organisasi",
|
||||
message: "Berhasil mengambil data posisi organisasi dengan pagination",
|
||||
data: data.map((item: any) => ({
|
||||
id: item.id,
|
||||
nama: item.nama,
|
||||
deskripsi: item.deskripsi,
|
||||
hierarki: item.hierarki,
|
||||
})),
|
||||
page,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Gagal mengambil data posisi organisasi",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default posisiOrganisasiFindMany;
|
||||
|
||||
@@ -1,21 +1,157 @@
|
||||
'use client'
|
||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa';
|
||||
import colors from '@/con/colors';
|
||||
import { Stack, Box, Text, Image, Paper } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
|
||||
|
||||
function Page() {
|
||||
const state = useProxy(PendapatanAsliDesa.ApbDesa);
|
||||
|
||||
useShallowEffect(() => {
|
||||
state.findMany.load();
|
||||
}, []);
|
||||
|
||||
useShallowEffect(() => {
|
||||
PendapatanAsliDesa.pembiayaan.findMany.load();
|
||||
PendapatanAsliDesa.belanja.findMany.load();
|
||||
PendapatanAsliDesa.pendapatan.findMany.load();
|
||||
}, []);
|
||||
|
||||
// Get the latest APB data
|
||||
const latestApb = state.findMany.data?.[0];
|
||||
|
||||
// Calculate totals
|
||||
const totalPendapatan = latestApb?.pendapatan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
const totalBelanja = latestApb?.belanja?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
const totalPembiayaan = latestApb?.pembiayaan?.reduce((sum, item) => sum + (item?.value || 0), 0) || 0;
|
||||
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap="lg">
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Text ta={"center"} fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
<Text ta="center" fz={{ base: "h1", md: "2.5rem" }} c={colors["blue-button"]} fw="bold">
|
||||
Pendapatan Asli Desa
|
||||
</Text>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Stack gap={'lg'} justify='center'>
|
||||
<Paper bg={colors['white-1']} p={"xl"}>
|
||||
<Image src="/pa-desa.png" alt=''/>
|
||||
<Stack gap="lg" justify="center">
|
||||
<Paper bg={colors['white-1']} p="xl">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||
{/* Pendapatan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pendapatan</Title>
|
||||
{PendapatanAsliDesa.pendapatan.findMany.data?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Pendapatan</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c={colors['blue-button']}>
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalPendapatan)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Belanja Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Belanja</Title>
|
||||
{PendapatanAsliDesa.belanja.findMany.data?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Belanja</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c="orange">
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalBelanja)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* Pembiayaan Card */}
|
||||
<Box p="md" style={{ border: '1px solid #e9ecef', borderRadius: '8px' }}>
|
||||
<Stack gap={"xs"}>
|
||||
<Title order={3}>Pembiayaan</Title>
|
||||
{PendapatanAsliDesa.pembiayaan.findMany.data?.map((item) => (
|
||||
<Box key={item.id}>
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{item.name}</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="md" fw={500}>{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(item.value)}</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
<Grid>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="lg" fw={600} mb="xs">Total Pembiayaan</Text>
|
||||
</GridCol>
|
||||
<GridCol span={{ base: 12, md: 6 }}>
|
||||
<Text fz="xl" fw={700} c="green">
|
||||
{new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
minimumFractionDigits: 0
|
||||
}).format(totalPembiayaan)}
|
||||
</Text>
|
||||
</GridCol>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Cell, Pie, PieChart } from 'recharts';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function GrafikBerdasarkanJenisKelamin() {
|
||||
const stategrafikBerdasarkanJenisKelamin = useProxy(grafikBerdasarkanJenisKelamin)
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [donutData, setDonutData] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, [])
|
||||
|
||||
const updateChartData = (data: any) => {
|
||||
if (data && data.length > 0) {
|
||||
const totalLaki = data.reduce((acc: number, cur: any) => acc + Number(cur.laki || 0), 0);
|
||||
const totalPerempuan = data.reduce((acc: number, cur: any) => acc + Number(cur.perempuan || 0), 0);
|
||||
|
||||
setDonutData([
|
||||
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'], key: 'laki-laki' },
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: '#FF6384', key: 'perempuan' }
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
await stategrafikBerdasarkanJenisKelamin.findMany.load();
|
||||
if (stategrafikBerdasarkanJenisKelamin.findMany.data) {
|
||||
updateChartData(stategrafikBerdasarkanJenisKelamin.findMany.data);
|
||||
}
|
||||
};
|
||||
|
||||
if(!stategrafikBerdasarkanJenisKelamin.findMany.data) return <Stack>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
|
||||
return (
|
||||
<Stack gap={"xl"}>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Jenis Kelamin Responden</Title>
|
||||
{mounted && donutData.length > 0 && (
|
||||
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
width={1000} height={530}
|
||||
data={donutData}
|
||||
>
|
||||
|
||||
<Pie
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
data={donutData}
|
||||
innerRadius={120}
|
||||
outerRadius={230}
|
||||
label={true}
|
||||
>
|
||||
{donutData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</Center>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#FF6384'} w={20} h={20} />
|
||||
<Text>Perempuan: {donutData.find((entry) => entry.name === 'Perempuan')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={colors['blue-button']} w={20} h={20} />
|
||||
<Text>Laki-laki: {donutData.find((entry) => entry.name === 'Laki-laki')?.value}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default GrafikBerdasarkanJenisKelamin;
|
||||
@@ -1,96 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import grafikBerdasarkanResponden from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanResponden';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Cell, Pie, PieChart } from 'recharts';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function GrafikBerdasarkanResponden() {
|
||||
const stategrafikBerdasarkanResponden = useProxy(grafikBerdasarkanResponden)
|
||||
const [donutData, setDonutData] = useState<any[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, [])
|
||||
|
||||
const updateChartData = (data: any) => {
|
||||
if (data && data.length > 0) {
|
||||
const totalSangatBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.sangatbaik || 0), 0);
|
||||
const totalBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.baik || 0), 0);
|
||||
const totalKurangBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.kurangbaik || 0), 0);
|
||||
const totalTidakBaik = data.reduce((acc: number, cur: any) => acc + Number(cur.tidakbaik || 0), 0);
|
||||
setDonutData([
|
||||
{ name: 'sangatbaik', value: totalSangatBaik, color: colors['blue-button'], key: 'sangatbaik' },
|
||||
{ name: 'baik', value: totalBaik, color: '#10A85AFF', key: 'baik' },
|
||||
{ name: 'kurangbaik', value: totalKurangBaik, color: '#B3AA12FF', key: 'kurangbaik' },
|
||||
{ name: 'tidakbaik', value: totalTidakBaik, color: '#B21313FF', key: 'tidakbaik' }
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
await stategrafikBerdasarkanResponden.findMany.load();
|
||||
if (stategrafikBerdasarkanResponden.findMany.data) {
|
||||
updateChartData(stategrafikBerdasarkanResponden.findMany.data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!stategrafikBerdasarkanResponden.findMany.data) return <Stack>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
return (
|
||||
<Stack>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
|
||||
{mounted && donutData.length > 0 && (
|
||||
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
width={1000} height={530}
|
||||
data={donutData}
|
||||
>
|
||||
<Pie
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
data={donutData}
|
||||
innerRadius={120}
|
||||
outerRadius={230}
|
||||
label={true}
|
||||
>
|
||||
{donutData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</Center>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={colors['blue-button']} w={20} h={20} />
|
||||
<Text>Sangat Baik: {donutData.find((entry) => entry.name === 'sangatbaik')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#10A85AFF'} w={20} h={20} />
|
||||
<Text>Baik: {donutData.find((entry) => entry.name === 'baik')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#B3AA12FF'} w={20} h={20} />
|
||||
<Text>Kurang Baik: {donutData.find((entry) => entry.name === 'kurangbaik')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#B21313FF'} w={20} h={20} />
|
||||
<Text>Tidak Baik: {donutData.find((entry) => entry.name === 'tidakbaik')?.value}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default GrafikBerdasarkanResponden;
|
||||
@@ -1,97 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import grafikBerdasarkanUmur from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanUmur';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Flex, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Cell, Pie, PieChart } from 'recharts';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function GrafikBerdasarakanUmur() {
|
||||
const stategrafikBerdasarkanUmur = useProxy(grafikBerdasarkanUmur)
|
||||
const [donutData, setDonutData] = useState<any[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
const updateChartData = (data: any) => {
|
||||
if (data && data.length > 0) {
|
||||
const totalRemaja = data.reduce((acc: number, cur: any) => acc + Number(cur.remaja || 0), 0);
|
||||
const totalDewasa = data.reduce((acc: number, cur: any) => acc + Number(cur.dewasa || 0), 0);
|
||||
const totalOrangtua = data.reduce((acc: number, cur: any) => acc + Number(cur.orangtua || 0), 0);
|
||||
const totalLansia = data.reduce((acc: number, cur: any) => acc + Number(cur.lansia || 0), 0);
|
||||
|
||||
setDonutData([
|
||||
{ name: 'Remaja', value: totalRemaja, color: colors['blue-button'], key: 'remaja' },
|
||||
{ name: 'Dewasa', value: totalDewasa, color: '#D32711FF', key: 'dewasa' },
|
||||
{ name: 'Orangtua', value: totalOrangtua, color: '#B46B04FF', key: 'orangtua' },
|
||||
{ name: 'Lansia', value: totalLansia, color: '#038617FF', key: 'lansia' }
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
useShallowEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
await stategrafikBerdasarkanUmur.findMany.load();
|
||||
if (stategrafikBerdasarkanUmur.findMany.data) {
|
||||
updateChartData(stategrafikBerdasarkanUmur.findMany.data);
|
||||
}
|
||||
}
|
||||
|
||||
if(!stategrafikBerdasarkanUmur.findMany.data) return <Stack>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Umur Responden</Title>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
return (
|
||||
<Stack>
|
||||
<Title pb={10} order={3}>Grafik Berdasarkan Umur Responden</Title>
|
||||
{mounted && donutData.length > 0 && (
|
||||
<Box style={{ width: '100%', height: 'auto', minHeight: 300 }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
width={1000} height={530}
|
||||
data={donutData}
|
||||
>
|
||||
<Pie
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
data={donutData}
|
||||
innerRadius={120}
|
||||
outerRadius={230}
|
||||
label={true}
|
||||
>
|
||||
{donutData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</Center>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={colors['blue-button']} w={20} h={20} />
|
||||
<Text>17 - 25 tahun: {donutData.find((entry) => entry.name === 'remaja')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#D32711FF'} w={20} h={20} />
|
||||
<Text>26 - 45 tahun: {donutData.find((entry) => entry.name === 'dewasa')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#B46B04FF'} w={20} h={20} />
|
||||
<Text>46 - 60 tahun: {donutData.find((entry) => entry.name === 'orangtua')?.value}</Text>
|
||||
</Flex>
|
||||
<Flex gap={"md"} align={"center"}>
|
||||
<Box bg={'#038617FF'} w={20} h={20} />
|
||||
<Text>di atas 60 tahun: {donutData.find((entry) => entry.name === 'lansia')?.value}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default GrafikBerdasarakanUmur;
|
||||
@@ -1,57 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import grafikHasilKepuasanMasyarakat from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikHasilKepuasan';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function GrafikHasilKepuasan() {
|
||||
const grafikHasilKepuasan = useProxy(grafikHasilKepuasanMasyarakat)
|
||||
const [chartData, setChartData] = useState<any[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const isTablet = useMediaQuery('(max-width: 1024px)')
|
||||
const isMobile = useMediaQuery('(max-width: 768px)')
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, [])
|
||||
|
||||
useShallowEffect(() => {
|
||||
const fetchData = async () => {
|
||||
await grafikHasilKepuasan.findMany.load();
|
||||
if (grafikHasilKepuasan.findMany.data && grafikHasilKepuasan.findMany.data.length > 0) {
|
||||
setChartData(grafikHasilKepuasan.findMany.data);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if(!grafikHasilKepuasan.findMany.data) return <Stack>
|
||||
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik</Title>
|
||||
<Skeleton h={500} />
|
||||
</Stack>
|
||||
|
||||
return (
|
||||
<Stack gap={"xl"}>
|
||||
<Text fw={"bold"} fz={{ base: 'h4', md: 'h3' }} ta={"center"}>
|
||||
Grafik Hasil Kepuasan Masyarakat Terhadap Pelayanan Publik
|
||||
</Text>
|
||||
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
|
||||
{mounted && chartData.length > 0 && (
|
||||
<BarChart width={isMobile ? 300 : isTablet ? 400 : 450} height={380} data={chartData} >
|
||||
<XAxis dataKey="label" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend style={{justifyContent: 'center'}} />
|
||||
<Bar dataKey="kepuasan" fill={colors['blue-button']} name="Kepuasan" />
|
||||
</BarChart>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default GrafikHasilKepuasan;
|
||||
@@ -1,47 +1,674 @@
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Paper, Stack, Text } from '@mantine/core';
|
||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||
import GrafikBerdasarkanJenisKelamin from './grafik_berdasarkan_jenis_kelamin/page';
|
||||
import GrafikBerdasarkanResponden from './grafik_berdasarkan_pilihan_responden/page';
|
||||
import GrafikBerdasarakanUmur from './grafik_berdasarkan_umur_responden/page';
|
||||
import GrafikHasilKepuasan from './grafik_hasil_kepuasan_masyarakat/page';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
|
||||
import colors from "@/con/colors";
|
||||
import { BarChart, PieChart } from '@mantine/charts';
|
||||
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||
import { useDisclosure, useShallowEffect } from "@mantine/hooks";
|
||||
import { useState } from "react";
|
||||
import { useProxy } from "valtio/utils";
|
||||
|
||||
function Page() {
|
||||
interface ChartDataItem {
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Kepuasan() {
|
||||
const state = useProxy(indeksKepuasanState.responden);
|
||||
const { data, loading } = state.findMany;
|
||||
const [donutDataJenisKelamin, setDonutDataJenisKelamin] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataRating, setDonutDataRating] = useState<ChartDataItem[]>([]);
|
||||
const [donutDataKelompokUmur, setDonutDataKelompokUmur] = useState<ChartDataItem[]>([]);
|
||||
const [barChartData, setBarChartData] = useState<Array<{ month: string; count: number }>>([]);
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
|
||||
const resetForm = () => {
|
||||
state.create.form = {
|
||||
...state.create.form,
|
||||
name: "",
|
||||
tanggal: "",
|
||||
jenisKelaminId: "",
|
||||
ratingId: "",
|
||||
kelompokUmurId: "",
|
||||
}
|
||||
}
|
||||
|
||||
useShallowEffect(() => {
|
||||
indeksKepuasanState.jenisKelaminResponden.findMany.load()
|
||||
indeksKepuasanState.pilihanRatingResponden.findMany.load()
|
||||
indeksKepuasanState.kelompokUmurResponden.findMany.load()
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = await state.create.create();
|
||||
if (typeof id !== 'undefined') {
|
||||
const idStr = String(id);
|
||||
await state.findUnique.load(idStr);
|
||||
}
|
||||
resetForm();
|
||||
close()
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load data on component mount
|
||||
useShallowEffect(() => {
|
||||
if (!data && !loading) {
|
||||
state.findMany.load(1, 1000); // Load first page with a large limit to get all data
|
||||
return;
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Hitung total berdasarkan jenis kelamin
|
||||
const totalLaki = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'laki-laki').length;
|
||||
const totalPerempuan = data.filter((item: any) => item.jenisKelamin?.name?.toLowerCase() === 'perempuan').length;
|
||||
|
||||
// Hitung total berdasarkan rating
|
||||
const totalSangatBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat baik').length;
|
||||
const totalBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'baik').length;
|
||||
const totalKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'kurang baik').length;
|
||||
const totalSangatKurangBaik = data.filter((item: any) => item.rating?.name?.toLowerCase() === 'sangat kurang baik').length;
|
||||
|
||||
// Hitung total berdasarkan kelompok umur
|
||||
const totalMuda = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'muda').length;
|
||||
const totalDewasa = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'dewasa').length;
|
||||
const totalLansia = data.filter((item: any) => item.kelompokUmur?.name?.toLowerCase() === 'lansia').length;
|
||||
|
||||
// Update gender chart data
|
||||
setDonutDataJenisKelamin([
|
||||
{ name: 'Laki-laki', value: totalLaki, color: colors['blue-button'] },
|
||||
{ name: 'Perempuan', value: totalPerempuan, color: '#10A85AFF' },
|
||||
]);
|
||||
|
||||
// Update rating chart data
|
||||
setDonutDataRating([
|
||||
{ name: 'Sangat Baik', value: totalSangatBaik, color: colors['blue-button'] },
|
||||
{ name: 'Baik', value: totalBaik, color: '#10A85AFF' },
|
||||
{ name: 'Kurang Baik', value: totalKurangBaik, color: '#FFA500' },
|
||||
{ name: 'Sangat Kurang Baik', value: totalSangatKurangBaik, color: '#FF4500' },
|
||||
]);
|
||||
|
||||
// Update age group chart data
|
||||
setDonutDataKelompokUmur([
|
||||
{ name: 'Muda', value: totalMuda, color: colors['blue-button'] },
|
||||
{ name: 'Dewasa', value: totalDewasa, color: '#10A85AFF' },
|
||||
{ name: 'Lansia', value: totalLansia, color: '#FFA500' },
|
||||
]);
|
||||
|
||||
// Process data for bar chart (group by month)
|
||||
const monthYearMap = new Map<string, number>();
|
||||
|
||||
data.forEach((item: any) => {
|
||||
// Try both createdAt and tanggal fields
|
||||
const dateValue = item.tanggal || item.createdAt;
|
||||
if (!dateValue) return;
|
||||
|
||||
const parsedDate = new Date(dateValue);
|
||||
if (isNaN(parsedDate.getTime())) return;
|
||||
|
||||
const month = parsedDate.getMonth() + 1;
|
||||
const year = parsedDate.getFullYear();
|
||||
const monthYearKey = `${year}-${String(month).padStart(2, '0')}`;
|
||||
|
||||
monthYearMap.set(monthYearKey, (monthYearMap.get(monthYearKey) || 0) + 1);
|
||||
});
|
||||
|
||||
// Convert map to array and sort by date
|
||||
const barData = Array.from(monthYearMap.entries())
|
||||
.map(([key, count]) => {
|
||||
const [year, month] = key.split('-');
|
||||
const monthName = new Date(Number(year), Number(month) - 1, 1)
|
||||
.toLocaleString('id-ID', { month: 'long' });
|
||||
return {
|
||||
month: `${monthName} ${year}`,
|
||||
count,
|
||||
sortKey: parseInt(`${year}${String(month).padStart(2, '0')}`, 10)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.sortKey - b.sortKey)
|
||||
.map(({ month, count }) => ({ month, count }));
|
||||
|
||||
setBarChartData(barData);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if ((loading && !data) || !data) {
|
||||
return (
|
||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||
<Box px={{ base: 'md', md: 100 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Stack py={10} px="xl">
|
||||
<Skeleton height={300} mb="md" />
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }}>
|
||||
<Skeleton height={300} />
|
||||
<Skeleton height={300} />
|
||||
<Skeleton height={300} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return (
|
||||
<Stack p="sm">
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||
<Center>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
</Center>
|
||||
<Center mt={10}>
|
||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||
</Center>
|
||||
</Container>
|
||||
<Box px={"xl"}>
|
||||
<Paper p={"lg"} bg={colors.Bg}>
|
||||
<Paper p={"lg"}>
|
||||
<Stack gap={"xs"}>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
|
||||
<Box>
|
||||
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Indeks Kepuasan Masyarakat (IKM)
|
||||
</Text>
|
||||
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
||||
Desa Darmasaba
|
||||
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
|
||||
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
|
||||
{state.findMany.total.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper bg={colors['white-1']} p={'xl'}>
|
||||
<GrafikHasilKepuasan />
|
||||
</Flex>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box py={"xl"}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 3
|
||||
}}
|
||||
>
|
||||
{/* Chart Jenis Kelamin */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Jenis Kelamin</Title>
|
||||
{donutDataJenisKelamin.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Stack gap="sm" mt="md">
|
||||
{donutDataJenisKelamin.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataRating.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Kelompok Umur */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Umur</Title>
|
||||
{donutDataKelompokUmur.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataKelompokUmur.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<GrafikBerdasarkanJenisKelamin/>
|
||||
{/* Modal */}
|
||||
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
value={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
type="date"
|
||||
placeholder="masukkan tanggal"
|
||||
value={state.create.form.tanggal}
|
||||
onChange={(val) => {
|
||||
state.create.form.tanggal = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
key={"jenisKelamin"}
|
||||
label={"Jenis Kelamin"}
|
||||
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
|
||||
value={state.create.form.jenisKelaminId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.jenisKelaminId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
key={"rating_responden"}
|
||||
label={"Rating"}
|
||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||
value={state.create.form.ratingId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.ratingId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
key={"kelompokUmur"}
|
||||
label={"Kelompok Umur"}
|
||||
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
|
||||
value={state.create.form.kelompokUmurId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.kelompokUmurId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||
/>
|
||||
<Button
|
||||
mt={10}
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Stack p={"sm"}>
|
||||
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||
<Stack gap={"xs"}>
|
||||
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||
<Group justify={"center"}>
|
||||
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Container>
|
||||
<Box px={"xl"}>
|
||||
<Paper p={"lg"} bg={colors.Bg}>
|
||||
<Paper p={"lg"}>
|
||||
<Stack gap={"xs"}>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Text fw={"bold"}>Pelayanan Terhadap Publik Desa Darmasaba</Text>
|
||||
<Box>
|
||||
<Text fz={"sm"} fw={"bold"} c={colors["blue-button"]}>Total Responden</Text>
|
||||
<Text ta={"end"} fz={"h1"} fw={"bold"} c={colors["blue-button"]}>
|
||||
{state.findMany.total.toLocaleString('id-ID')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
<BarChart
|
||||
h={300}
|
||||
data={barChartData}
|
||||
dataKey="month"
|
||||
series={[{ name: 'count', color: colors['blue-button'] }]}
|
||||
tickLine="y"
|
||||
xAxisLabel="Bulan"
|
||||
yAxisLabel="Jumlah Responden"
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Box py={"xl"}>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 1,
|
||||
lg: 1,
|
||||
xl: 3
|
||||
}}
|
||||
>
|
||||
{/* Chart Jenis Kelamin */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Jenis Kelamin</Title>
|
||||
{donutDataJenisKelamin.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withLabels
|
||||
withTooltip
|
||||
labelsType="percent"
|
||||
size={200}
|
||||
data={donutDataJenisKelamin}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Stack gap="sm" mt="md">
|
||||
{donutDataJenisKelamin.map((entry) => (
|
||||
<Flex key={entry.name} gap="md" align="center">
|
||||
<Box bg={entry.color} w={20} h={20} style={{ flexShrink: 0 }} />
|
||||
<Text size="sm">{entry.name}: {entry.value}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Rating */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Pilihan</Title>
|
||||
{donutDataRating.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={200}
|
||||
data={donutDataRating}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataRating.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Chart Kelompok Umur */}
|
||||
<Paper bg={colors['white-1']} p="md" radius="md">
|
||||
<Stack>
|
||||
<Title order={4}>Umur</Title>
|
||||
{donutDataKelompokUmur.every(item => item.value === 0) ? (
|
||||
<Text c="dimmed" ta="center" my="md">
|
||||
Belum ada data untuk ditampilkan dalam grafik
|
||||
</Text>
|
||||
) : (
|
||||
<Paper p="md" radius="md" withBorder>
|
||||
<Box style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||||
<Box style={{ position: 'relative', width: '100%' }}>
|
||||
<Center>
|
||||
<PieChart
|
||||
withTooltip
|
||||
tooltipAnimationDuration={200}
|
||||
withLabels
|
||||
labelsPosition="outside"
|
||||
labelsType="percent"
|
||||
withLabelsLine
|
||||
size={190}
|
||||
data={donutDataKelompokUmur}
|
||||
/>
|
||||
</Center>
|
||||
</Box>
|
||||
<Box mt="md" style={{ width: '100%' }}>
|
||||
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs">
|
||||
{donutDataKelompokUmur.map((entry) => (
|
||||
<Flex key={entry.name} gap="sm" align="center" style={{ overflow: 'hidden' }}>
|
||||
<Box bg={entry.color} w={16} h={16} style={{ flexShrink: 0 }} />
|
||||
<Text size="xs" lineClamp={1} style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
{entry.name}: {entry.value}
|
||||
</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<GrafikBerdasarkanResponden/>
|
||||
{/* Modal */}
|
||||
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
|
||||
<Paper bg={colors['white-1']} p={'md'}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
label="Nama"
|
||||
type='text'
|
||||
placeholder="masukkan nama"
|
||||
value={state.create.form.name}
|
||||
onChange={(val) => {
|
||||
state.create.form.name = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
label="Tanggal"
|
||||
type="date"
|
||||
placeholder="masukkan tanggal"
|
||||
value={state.create.form.tanggal}
|
||||
onChange={(val) => {
|
||||
state.create.form.tanggal = val.currentTarget.value;
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
key={"jenisKelamin"}
|
||||
label={"Jenis Kelamin"}
|
||||
placeholder={indeksKepuasanState.jenisKelaminResponden.findMany.loading ? 'Memuat...' : 'Pilih jenis kelamin'}
|
||||
value={state.create.form.jenisKelaminId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.jenisKelaminId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.jenisKelaminResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.jenisKelaminResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
key={"rating_responden"}
|
||||
label={"Rating"}
|
||||
placeholder={indeksKepuasanState.pilihanRatingResponden.findMany.loading ? 'Memuat...' : 'Pilih rating'}
|
||||
value={state.create.form.ratingId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.ratingId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.pilihanRatingResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.pilihanRatingResponden.findMany.loading}
|
||||
/>
|
||||
<Select
|
||||
key={"kelompokUmur"}
|
||||
label={"Kelompok Umur"}
|
||||
placeholder={indeksKepuasanState.kelompokUmurResponden.findMany.loading ? 'Memuat...' : 'Pilih kelompok umur'}
|
||||
value={state.create.form.kelompokUmurId || ""}
|
||||
onChange={(val) => {
|
||||
state.create.form.kelompokUmurId = val ?? "";
|
||||
}}
|
||||
data={
|
||||
(indeksKepuasanState.kelompokUmurResponden.findMany.data || [])
|
||||
.filter(Boolean) // Hapus null, undefined, dll
|
||||
.map((item) => ({
|
||||
value: item.id,
|
||||
label: item.name || 'Tanpa Nama',
|
||||
}))
|
||||
}
|
||||
disabled={indeksKepuasanState.kelompokUmurResponden.findMany.loading}
|
||||
/>
|
||||
<Button
|
||||
mt={10}
|
||||
bg={colors['blue-button']}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
<Box px={{ base: "md", md: 100 }}>
|
||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
||||
<GrafikBerdasarakanUmur/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
export default Kepuasan;
|
||||
|
||||
Reference in New Issue
Block a user