Sinkronisasi Sinkronisasi UI & API Admin - User Submenu Program Kemiskinan
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import ApiFetch from "@/lib/api-fetch";
|
import ApiFetch from "@/lib/api-fetch";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -11,8 +12,7 @@ const templateForm = z.object({
|
|||||||
statistik: z.object({
|
statistik: z.object({
|
||||||
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
|
tahun: z.string().min(1, "Tahun minimal 1 karakter"),
|
||||||
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
|
jumlah: z.string().min(1, "Jumlah minimal 1 karakter"),
|
||||||
})
|
}),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultForm = {
|
const defaultForm = {
|
||||||
@@ -21,8 +21,8 @@ const defaultForm = {
|
|||||||
ikonUrl: "",
|
ikonUrl: "",
|
||||||
statistik: {
|
statistik: {
|
||||||
tahun: "",
|
tahun: "",
|
||||||
jumlah: ""
|
jumlah: "",
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const programKemiskinanState = proxy({
|
const programKemiskinanState = proxy({
|
||||||
@@ -64,12 +64,35 @@ const programKemiskinanState = proxy({
|
|||||||
};
|
};
|
||||||
}>[],
|
}>[],
|
||||||
loading: false,
|
loading: false,
|
||||||
async load() {
|
page: 1,
|
||||||
const res = await ApiFetch.api.ekonomi.programkemiskinan[
|
totalPages: 1,
|
||||||
"find-many"
|
search: "",
|
||||||
].get();
|
load: async (page = 1, limit = 10, search = "") => {
|
||||||
if (res.status === 200) {
|
programKemiskinanState.findMany.loading = true; // ✅ Akses langsung via nama path
|
||||||
programKemiskinanState.findMany.data = res.data?.data ?? [];
|
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;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'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, 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react';
|
||||||
import HeaderSearch from '../../_com/header';
|
import HeaderSearch from '../../_com/header';
|
||||||
import JudulList from '../../_com/judulList';
|
import JudulList from '../../_com/judulList';
|
||||||
@@ -23,7 +23,7 @@ function ProgramKemiskinan() {
|
|||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.currentTarget.value)}
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<ListProgramKemiskinan search={search}/>
|
<ListProgramKemiskinan search={search} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -34,14 +34,22 @@ function ListProgramKemiskinan({ search }: { search: string }) {
|
|||||||
const [lineChart, setLineChart] = useState<any[]>([]);
|
const [lineChart, setLineChart] = useState<any[]>([]);
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
loading,
|
||||||
|
load,
|
||||||
|
} = programState.findMany;
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true)
|
||||||
programState.findMany.load()
|
load(page, 10, search)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (programState.findMany.data) {
|
if (data) {
|
||||||
const chartData = programState.findMany.data
|
const chartData = data
|
||||||
.filter(item => item.statistik)
|
.filter(item => item.statistik)
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
tahun: item.statistik?.tahun,
|
tahun: item.statistik?.tahun,
|
||||||
@@ -52,18 +60,11 @@ function ListProgramKemiskinan({ search }: { search: string }) {
|
|||||||
setLineChart(chartData);
|
setLineChart(chartData);
|
||||||
|
|
||||||
}
|
}
|
||||||
}, [programState.findMany.data])
|
}, [data])
|
||||||
|
|
||||||
const filteredData = (programState.findMany.data || []).filter(item => {
|
const filteredData = data || []
|
||||||
const keyword = search.toLowerCase();
|
|
||||||
return (
|
|
||||||
item.nama.toLowerCase().includes(keyword) ||
|
|
||||||
item.deskripsi.toLowerCase().includes(keyword) ||
|
|
||||||
item.statistik?.tahun.toString().includes(keyword)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!programState.findMany.data) {
|
if (loading || !data) {
|
||||||
return (
|
return (
|
||||||
<Stack py={10}>
|
<Stack py={10}>
|
||||||
<Skeleton h={500} />
|
<Skeleton h={500} />
|
||||||
@@ -112,7 +113,7 @@ function ListProgramKemiskinan({ search }: { search: string }) {
|
|||||||
<Box >
|
<Box >
|
||||||
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
|
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
|
||||||
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
|
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}>
|
||||||
<Box w={"100%"} style={{overflowX: 'auto'}}>
|
<Box w={"100%"} style={{ overflowX: 'auto' }}>
|
||||||
<LineChart
|
<LineChart
|
||||||
width={820}
|
width={820}
|
||||||
height={300}
|
height={300}
|
||||||
@@ -143,6 +144,14 @@ function ListProgramKemiskinan({ search }: { search: string }) {
|
|||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
<Center>
|
||||||
|
<Pagination
|
||||||
|
value={page}
|
||||||
|
onChange={(newPage) => load(newPage)}
|
||||||
|
total={totalPages}
|
||||||
|
my={"md"}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,53 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export default async function programKemiskinanFindMany() {
|
export default async function programKemiskinanFindMany(context: Context) {
|
||||||
const data = await prisma.programKemiskinan.findMany({
|
const page = Number(context.query.page) || 1;
|
||||||
include: {
|
const limit = Number(context.query.limit) || 10;
|
||||||
statistik: true, // ikut sertakan relasinya
|
const search = (context.query.search as string) || '';
|
||||||
},
|
const skip = (page - 1) * limit;
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
// Buat where clause
|
||||||
},
|
const where: any = { isActive: true };
|
||||||
});
|
|
||||||
|
// Tambahkan pencarian (jika ada)
|
||||||
return {
|
if (search) {
|
||||||
success: true,
|
where.OR = [
|
||||||
message: "Success get all program layanan",
|
{ nama: { contains: search, mode: 'insensitive' } },
|
||||||
data,
|
{ 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,14 +50,6 @@ function Page() {
|
|||||||
</Text>
|
</Text>
|
||||||
<Group py={20} align='center' justify='space-between'>
|
<Group py={20} align='center' justify='space-between'>
|
||||||
<Text fz={'h4'} fw={"bold"}>DATA PENGANGGURAN DESA</Text>
|
<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>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
|
|||||||
@@ -1,54 +1,50 @@
|
|||||||
|
'use client'
|
||||||
import colors from '@/con/colors';
|
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 React from 'react';
|
||||||
import BackButton from '../../desa/layanan/_com/BackButto';
|
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() {
|
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 (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -69,26 +65,58 @@ function Page() {
|
|||||||
md: 2
|
md: 2
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.map((v, k) => {
|
{state.findMany.data.map((v, k) => {
|
||||||
return (
|
return (
|
||||||
<Paper p={'xl'} key={k}>
|
<Paper p={'xl'} key={k}>
|
||||||
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.judul}</Text>
|
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
|
||||||
<Text fz={'lg'} c={'black'}>{v.deskripsi}</Text>
|
<Text fz={'lg'} c={'black'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<Paper p={'xl'}>
|
<Paper p={'xl'}>
|
||||||
<Text fz={'h4'} fw={'bold'} c={colors['blue-button']}>Statistik Kemiskinan Masyarakat</Text>
|
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
|
||||||
<LineChart
|
<Box style={{ width: '100%', height: 'auto' }}>
|
||||||
h={300}
|
{state.findMany.data.length > 0 && state.findMany.data[0]?.statistik ? (
|
||||||
data={dataStatistik}
|
<Box w="100%" style={{ overflowX: 'auto' }}>
|
||||||
dataKey="tahun"
|
<Center>
|
||||||
series={[
|
<RechartsLineChart
|
||||||
{ name: 'Kemiskinan', color: colors['blue-button'] },
|
width={800}
|
||||||
]}
|
height={300}
|
||||||
curveType="linear"
|
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>
|
</Paper>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user