Sinkronisasi UI & API Admin - User Submenu Pendapatan Asli Desa
This commit is contained in:
@@ -292,6 +292,9 @@ model PosisiOrganisasiPPID {
|
|||||||
pegawai PegawaiPPID[]
|
pegawai PegawaiPPID[]
|
||||||
strukturOrganisasi StrukturPPID[] // Relasi balik
|
strukturOrganisasi StrukturPPID[] // Relasi balik
|
||||||
parentId String?
|
parentId String?
|
||||||
|
isActive Boolean @default(true)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
parent PosisiOrganisasiPPID? @relation("Parent", fields: [parentId], references: [id])
|
||||||
children PosisiOrganisasiPPID[] @relation("Parent")
|
children PosisiOrganisasiPPID[] @relation("Parent")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,18 +348,34 @@ const posisiOrganisasi = proxy({
|
|||||||
deskripsi: string | null;
|
deskripsi: string | null;
|
||||||
hierarki: number;
|
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 {
|
try {
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi[
|
const query: any = { page, limit };
|
||||||
"find-many"
|
if (search) query.search = search;
|
||||||
].get();
|
|
||||||
if (res.status === 200) {
|
const res = await ApiFetch.api.ppid.strukturppid.posisiorganisasi["find-many"].get({ query });
|
||||||
// The API now returns the id field, so we can use it directly
|
|
||||||
this.data = res.data?.data ?? [];
|
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) {
|
} catch (err) {
|
||||||
console.error("Find many error:", error);
|
console.error("Gagal fetch posisi organisasi paginated:", err);
|
||||||
this.data = [];
|
posisiOrganisasi.findMany.data = [];
|
||||||
|
posisiOrganisasi.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
posisiOrganisasi.findMany.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -438,9 +454,9 @@ const pegawai = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
pegawai.create.loading = true;
|
pegawai.create.loading = true;
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai["create"].post(
|
||||||
"create"
|
pegawai.create.form
|
||||||
].post(pegawai.create.form);
|
);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
toast.success("Pegawai berhasil ditambahkan");
|
toast.success("Pegawai berhasil ditambahkan");
|
||||||
await pegawai.findMany.load();
|
await pegawai.findMany.load();
|
||||||
@@ -457,42 +473,55 @@ const pegawai = proxy({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// In struktur-organisasi.ts
|
// In struktur-organisasi.ts
|
||||||
findMany: {
|
findMany: {
|
||||||
data: null as any[] | null,
|
data: null as
|
||||||
page: 1,
|
| Prisma.PegawaiPPIDGetPayload<{
|
||||||
totalPages: 1,
|
include: {
|
||||||
total: 0,
|
image: true;
|
||||||
loading: false,
|
posisi: true;
|
||||||
load: async (page = 1, limit = 10) => { // Change to arrow function
|
};
|
||||||
pegawai.findMany.loading = true; // Use the full path to access the property
|
}>[]
|
||||||
pegawai.findMany.page = page;
|
| null,
|
||||||
try {
|
page: 1,
|
||||||
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
totalPages: 1,
|
||||||
"find-many"
|
total: 0,
|
||||||
].get({
|
loading: false,
|
||||||
query: { page, limit },
|
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;
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
const res = await ApiFetch.api.ppid.strukturppid.pegawai[
|
||||||
pegawai.findMany.data = res.data.data || [];
|
"find-many"
|
||||||
pegawai.findMany.total = res.data.total || 0;
|
].get({
|
||||||
pegawai.findMany.totalPages = res.data.totalPages || 1;
|
query,
|
||||||
} else {
|
});
|
||||||
console.error("Failed to load pegawai:", res.data?.message);
|
|
||||||
|
if (res.status === 200 && res.data?.success) {
|
||||||
|
pegawai.findMany.data = res.data.data || [];
|
||||||
|
pegawai.findMany.total = res.data.total || 0;
|
||||||
|
pegawai.findMany.totalPages = res.data.totalPages || 1;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to load pegawai:", res.data?.message);
|
||||||
|
pegawai.findMany.data = [];
|
||||||
|
pegawai.findMany.total = 0;
|
||||||
|
pegawai.findMany.totalPages = 1;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading pegawai:", error);
|
||||||
pegawai.findMany.data = [];
|
pegawai.findMany.data = [];
|
||||||
pegawai.findMany.total = 0;
|
pegawai.findMany.total = 0;
|
||||||
pegawai.findMany.totalPages = 1;
|
pegawai.findMany.totalPages = 1;
|
||||||
|
} finally {
|
||||||
|
pegawai.findMany.loading = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error("Error loading pegawai:", error);
|
|
||||||
pegawai.findMany.data = [];
|
|
||||||
pegawai.findMany.total = 0;
|
|
||||||
pegawai.findMany.totalPages = 1;
|
|
||||||
} finally {
|
|
||||||
pegawai.findMany.loading = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
|
||||||
findUnique: {
|
findUnique: {
|
||||||
data: null as
|
data: null as
|
||||||
| (Prisma.PegawaiGetPayload<{
|
| (Prisma.PegawaiGetPayload<{
|
||||||
@@ -521,12 +550,9 @@ findMany: {
|
|||||||
if (!id) return toast.warn("ID tidak valid");
|
if (!id) return toast.warn("ID tidak valid");
|
||||||
try {
|
try {
|
||||||
pegawai.delete.loading = true;
|
pegawai.delete.loading = true;
|
||||||
const res = await fetch(
|
const res = await fetch(`/api/ppid/strukturppid/pegawai/del/${id}`, {
|
||||||
`/api/ppid/strukturppid/pegawai/del/${id}`,
|
method: "DELETE",
|
||||||
{
|
});
|
||||||
method: "DELETE",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
toast.success(json.message ?? "Berhasil hapus pegawai");
|
toast.success(json.message ?? "Berhasil hapus pegawai");
|
||||||
@@ -555,15 +581,12 @@ findMany: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/ppid/strukturppid/pegawai/${id}`, {
|
||||||
`/api/ppid/strukturppid/pegawai/${id}`,
|
method: "GET",
|
||||||
{
|
headers: {
|
||||||
method: "GET",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
},
|
||||||
"Content-Type": "application/json",
|
});
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
@@ -677,7 +700,7 @@ findMany: {
|
|||||||
const stateStrukturPPID = proxy({
|
const stateStrukturPPID = proxy({
|
||||||
stateStruktur,
|
stateStruktur,
|
||||||
posisiOrganisasi,
|
posisiOrganisasi,
|
||||||
pegawai
|
pegawai,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default stateStrukturPPID;
|
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 { 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 { IconCheck, IconDeviceImacCog, IconSearch, IconX } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import JudulList from '../../../_com/judulList';
|
import JudulList from '../../../_com/judulList';
|
||||||
@@ -39,22 +39,10 @@ function ListPegawaiPPID({ search }: { search: string }) {
|
|||||||
} = stateOrganisasi.findMany;
|
} = stateOrganisasi.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load(page, 10);
|
load(page, 10, search);
|
||||||
}, [page]);
|
}, [page, search]);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const filteredData = data || []
|
||||||
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]);
|
|
||||||
|
|
||||||
// Handle loading state
|
// Handle loading state
|
||||||
if (loading || !data) {
|
if (loading || !data) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@@ -33,9 +33,17 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
|||||||
const [modalHapus, setModalHapus] = useState(false)
|
const [modalHapus, setModalHapus] = useState(false)
|
||||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = stateOrganisasi.findMany;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateOrganisasi.findMany.load()
|
load(page, 10, search);
|
||||||
}, [])
|
}, [page, search]);
|
||||||
|
|
||||||
const handleHapus = async () => {
|
const handleHapus = async () => {
|
||||||
if (selectedId) {
|
if (selectedId) {
|
||||||
@@ -45,17 +53,9 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredData = (stateOrganisasi.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
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);
|
|
||||||
|
|
||||||
if (!stateOrganisasi.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
@@ -120,6 +120,14 @@ function ListPosisiOrganisasiPPID({ search }: { search: string }) {
|
|||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
my={"md"}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
{/* Modal Hapus */}
|
{/* Modal Hapus */}
|
||||||
<ModalKonfirmasiHapus
|
<ModalKonfirmasiHapus
|
||||||
opened={modalHapus}
|
opened={modalHapus}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
@@ -5,12 +6,24 @@ import { Context } from "elysia";
|
|||||||
export default async function pegawaiFindMany(context: Context) {
|
export default async function pegawaiFindMany(context: Context) {
|
||||||
const page = Number(context.query.page) || 1;
|
const page = Number(context.query.page) || 1;
|
||||||
const limit = Number(context.query.limit) || 10;
|
const limit = Number(context.query.limit) || 10;
|
||||||
|
const search = (context.query.search as string) || '';
|
||||||
const skip = (page - 1) * limit;
|
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 {
|
try {
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
prisma.pegawaiPPID.findMany({
|
prisma.pegawaiPPID.findMany({
|
||||||
where: { isActive: true },
|
where,
|
||||||
include: {
|
include: {
|
||||||
posisi: true,
|
posisi: true,
|
||||||
image: true,
|
image: true,
|
||||||
@@ -20,7 +33,7 @@ export default async function pegawaiFindMany(context: Context) {
|
|||||||
orderBy: { posisi: { hierarki: 'asc' } },
|
orderBy: { posisi: { hierarki: 'asc' } },
|
||||||
}),
|
}),
|
||||||
prisma.pegawaiPPID.count({
|
prisma.pegawaiPPID.count({
|
||||||
where: { isActive: true }
|
where,
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,58 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// /api/posisi-organisasi/findManyPaginated.ts
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function posisiOrganisasiFindMany() {
|
async function posisiOrganisasiFindMany(context: Context) {
|
||||||
const data = await prisma.posisiOrganisasiPPID.findMany();
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Berhasil mengambil semua data posisi organisasi",
|
message: "Berhasil mengambil data posisi organisasi dengan pagination",
|
||||||
data: data.map((item: any) => ({
|
data: data.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
nama: item.nama,
|
nama: item.nama,
|
||||||
deskripsi: item.deskripsi,
|
deskripsi: item.deskripsi,
|
||||||
hierarki: item.hierarki,
|
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 colors from '@/con/colors';
|
||||||
import { Stack, Box, Text, Image, Paper } from '@mantine/core';
|
import { Box, Grid, GridCol, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||||
import React from 'react';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
|
||||||
|
|
||||||
function Page() {
|
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 (
|
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 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</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
|
Pendapatan Asli Desa
|
||||||
</Text>
|
</Text>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={'lg'} justify='center'>
|
<Stack gap="lg" justify="center">
|
||||||
<Paper bg={colors['white-1']} p={"xl"}>
|
<Paper bg={colors['white-1']} p="xl">
|
||||||
<Image src="/pa-desa.png" alt=''/>
|
<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>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</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';
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Box, Paper, Stack, Text } from '@mantine/core';
|
"use client";
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
import indeksKepuasanState from "@/app/admin/(dashboard)/_state/landing-page/indeks-kepuasan";
|
||||||
import GrafikBerdasarkanJenisKelamin from './grafik_berdasarkan_jenis_kelamin/page';
|
import colors from "@/con/colors";
|
||||||
import GrafikBerdasarkanResponden from './grafik_berdasarkan_pilihan_responden/page';
|
import { BarChart, PieChart } from '@mantine/charts';
|
||||||
import GrafikBerdasarakanUmur from './grafik_berdasarkan_umur_responden/page';
|
import { Box, Button, Center, Container, Flex, Group, Modal, Paper, Select, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from "@mantine/core";
|
||||||
import GrafikHasilKepuasan from './grafik_hasil_kepuasan_masyarakat/page';
|
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 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 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>
|
||||||
|
{/* 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 (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack p={"sm"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Container w={{ base: "100%", md: "80%" }} p={"xl"}>
|
||||||
<BackButton />
|
<Stack gap={"xs"}>
|
||||||
</Box>
|
<Text ta={"center"} fz={{ base: "2.4rem", md: "3.4rem" }}>Indeks Kepuasan Masyarakat</Text>
|
||||||
<Box>
|
<Group justify={"center"}>
|
||||||
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
<Button radius={"lg"} bg={colors["blue-button"]} onClick={open}>Ajukan Responden</Button>
|
||||||
Indeks Kepuasan Masyarakat (IKM)
|
</Group>
|
||||||
</Text>
|
</Stack>
|
||||||
<Text ta={"center"} fz={{ base: "h2", md: "2.5rem" }} c={colors["blue-button"]} fw={"bold"}>
|
</Container>
|
||||||
Desa Darmasaba
|
<Box px={"xl"}>
|
||||||
</Text>
|
<Paper p={"lg"} bg={colors.Bg}>
|
||||||
</Box>
|
<Paper p={"lg"}>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Stack gap={"xs"}>
|
||||||
<Paper bg={colors['white-1']} p={'xl'}>
|
<Flex justify={"space-between"} align={"center"}>
|
||||||
<GrafikHasilKepuasan />
|
<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>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
{/* Modal */}
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
<Modal opened={opened} onClose={close} title="Ajukan Responden" centered>
|
||||||
<GrafikBerdasarkanJenisKelamin/>
|
<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>
|
</Paper>
|
||||||
</Box>
|
</Modal>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
|
||||||
<GrafikBerdasarkanResponden/>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
|
||||||
<Paper p={"xl"} bg={colors['white-trans-1']}>
|
|
||||||
<GrafikBerdasarakanUmur/>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page;
|
export default Kepuasan;
|
||||||
|
|||||||
Reference in New Issue
Block a user