Sinkronisasi Sinkronisasi UI & API Admin - User Submenu Program Kemiskinan

This commit is contained in:
2025-08-22 00:26:58 +08:00
parent 20d4c90e60
commit 760ba4b6d2
5 changed files with 195 additions and 108 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
@@ -11,8 +12,7 @@ const templateForm = z.object({
statistik: z.object({
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
})
}),
});
const defaultForm = {
@@ -21,8 +21,8 @@ const defaultForm = {
ikonUrl: "",
statistik: {
tahun: "",
jumlah: ""
}
jumlah: "",
},
};
const programKemiskinanState = proxy({
@@ -64,12 +64,35 @@ const programKemiskinanState = proxy({
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get();
if (res.status === 200) {
programKemiskinanState.findMany.data = res.data?.data ?? [];
page: 1,
totalPages: 1,
search: "",
load: async (page = 1, limit = 10, search = "") => {
programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
programKemiskinanState.findMany.page = page;
programKemiskinanState.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.programkemiskinan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
programKemiskinanState.findMany.data = res.data.data ?? [];
programKemiskinanState.findMany.totalPages = res.data.totalPages ?? 1;
} else {
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch program kemiskinan paginated:", err);
programKemiskinanState.findMany.data = [];
programKemiskinanState.findMany.totalPages = 1;
} finally {
programKemiskinanState.findMany.loading = false;
}
},
},

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
@@ -23,7 +23,7 @@ function ProgramKemiskinan() {
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListProgramKemiskinan search={search}/>
<ListProgramKemiskinan search={search} />
</Box>
);
}
@@ -34,14 +34,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false);
const {
data,
page,
totalPages,
loading,
load,
} = programState.findMany;
useShallowEffect(() => {
setMounted(true)
programState.findMany.load()
load(page, 10, search)
}, [])
useEffect(() => {
if (programState.findMany.data) {
const chartData = programState.findMany.data
if (data) {
const chartData = data
.filter(item => item.statistik)
.map(item => ({
tahun: item.statistik?.tahun,
@@ -52,18 +60,11 @@ function ListProgramKemiskinan({ search }: { search: string }) {
setLineChart(chartData);
}
}, [programState.findMany.data])
}, [data])
const filteredData = (programState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.nama.toLowerCase().includes(keyword) ||
item.deskripsi.toLowerCase().includes(keyword) ||
item.statistik?.tahun.toString().includes(keyword)
);
});
const filteredData = data || []
if (!programState.findMany.data) {
if (loading || !data) {
return (
<Stack py={10}>
<Skeleton h={500} />
@@ -112,7 +113,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
<Box >
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
<Box w={"100%"} style={{overflowX: 'auto'}}>
<Box w={"100%"} style={{ overflowX: 'auto' }}>
<LineChart
width={820}
height={300}
@@ -143,6 +144,14 @@ function ListProgramKemiskinan({ search }: { search: string }) {
</Box>
</Stack>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box>
</Box>
);

View File

@@ -1,18 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import prisma from "@/lib/prisma";
import { Context } from "elysia";
export default async function programKemiskinanFindMany() {
const data = await prisma.programKemiskinan.findMany({
include: {
statistik: true, // ikut sertakan relasinya
},
orderBy: {
createdAt: "desc",
},
});
return {
success: true,
message: "Success get all program layanan",
data,
};
export default async function programKemiskinanFindMany(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 = [
{ nama: { contains: search, mode: 'insensitive' } },
{ deskripsi: { contains: search, mode: 'insensitive' } },
];
}
try {
// Ambil data dan total count secara paralel
const [data, total] = await Promise.all([
prisma.programKemiskinan.findMany({
where,
include: {
statistik: true,
},
skip,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.programKemiskinan.count({ where }),
]);
return {
success: true,
message: "Berhasil ambil program kemiskinan dengan pagination",
data,
page,
limit,
total,
totalPages: Math.ceil(total / limit),
};
} catch (e) {
console.error("Error di findMany paginated:", e);
return {
success: false,
message: "Gagal mengambil data program kemiskinan",
};
}
}

View File

@@ -50,14 +50,6 @@ function Page() {
</Text>
<Group py={20} align='center' justify='space-between'>
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
{/* <Flex gap={'xl'}>
<Select
value={selectedYear || state.findMany.data?.[0]?.year.toString()}
placeholder="Pilih Tahun"
data={Array.from(new Set(state.findMany.data?.map(item => item.year.toString()) || []))}
onChange={setSelectedYear}
/>
</Flex> */}
</Group>
</Box>
<Box px={{ base: "md", md: 100 }}>

View File

@@ -1,54 +1,50 @@
'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper } from '@mantine/core';
import { Stack, Box, Text, SimpleGrid, Paper, Skeleton, Center } from '@mantine/core';
import React from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
import { LineChart } from '@mantine/charts';
import { CartesianGrid, Legend, Line, LineChart as RechartsLineChart, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks';
interface StatistikData {
id: string;
tahun: number;
jumlah: number;
createdAt: Date;
updatedAt: Date;
}
interface ProgramKemiskinanData {
id: string;
nama: string;
deskripsi: string;
ikonUrl: string | null;
statistik: StatistikData | null;
isActive: boolean;
statistikId: string | null;
createdAt: Date;
updatedAt: Date;
}
const data = [
{
id: 1,
judul: 'Bantuan Tunai',
deskripsi: 'Bantuan keuangan langsung bagi keluarga kurang mampu'
},
{
id: 2,
judul: 'Pelatihan Kerja',
deskripsi: 'Program pelatihan keterampilan untuk meningkatkan peluang kerja'
},
{
id: 3,
judul: 'Subsidi Pangan',
deskripsi: 'Distribusi bahan pangan bersubsidi bagi masyarakat kurang mampu'
},
{
id: 4,
judul: 'Layanan Kesehatan Gratis',
deskripsi: 'Akses kesehatan gratis bagi masyarakat kurang mampu'
},
]
const dataStatistik = [
{
id: 1,
tahun: '2022',
Kemiskinan: 400000
},
{
id: 2,
tahun: '2023',
Kemiskinan: 450000
},
{
id: 3,
tahun: '2024',
Kemiskinan: 500000
},
{
id: 4,
tahun: '2025',
Kemiskinan: 400000
},
]
function Page() {
const state = useProxy(programKemiskinanState)
useShallowEffect(() => {
state.findMany.load()
}, [])
const data = programKemiskinanState.findMany.data
if (!data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
<Box px={{ base: 'md', md: 100 }}>
@@ -69,26 +65,58 @@ function Page() {
md: 2
}}
>
{data.map((v, k) => {
{state.findMany.data.map((v, k) => {
return (
<Paper p={'xl'} key={k}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
<Text fz={'lg'} c={'black'}>{v.deskripsi}</Text>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
<Text fz={'lg'} c={'black'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
</Paper>
)
})}
</SimpleGrid>
<Paper p={'xl'}>
<Text fz={'h4'} fw={'bold'} c={colors['blue-button']}>Statistik Kemiskinan Masyarakat</Text>
<LineChart
h={300}
data={dataStatistik}
dataKey="tahun"
series={[
{ name: 'Kemiskinan', color: colors['blue-button'] },
]}
curveType="linear"
/>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
<Box style={{ width: '100%', height: 'auto' }}>
{state.findMany.data.length > 0 && state.findMany.data[0]?.statistik ? (
<Box w="100%" style={{ overflowX: 'auto' }}>
<Center>
<RechartsLineChart
width={800}
height={300}
data={state.findMany.data
.filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } =>
item.statistik !== null
)
.map(item => ({
tahun: item.statistik.tahun,
jumlah: item.statistik.jumlah
}))
.sort((a, b) => a.tahun - b.tahun)
}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" />
<YAxis />
<Tooltip
formatter={(value: number, name: string) => [`${value} orang`, name]}
labelFormatter={(label: number) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah Masyarakat Miskin"
stroke={colors['blue-button']}
activeDot={{ r: 8 }}
/>
</RechartsLineChart>
</Center>
</Box>
) : (
<Text c="dimmed">Belum ada data statistik yang tersedia</Text>
)}
</Box>
</Paper>
</Stack>
</Box>