QC Tampilan Admin & User, Api berfungsi

This commit is contained in:
2025-09-16 16:47:12 +08:00
parent 4ceea5203f
commit 39e1e7b575
48 changed files with 3250 additions and 1916 deletions

View File

@@ -1487,7 +1487,7 @@ model ProgramKemiskinan {
id String @id @default(uuid()) id String @id @default(uuid())
nama String nama String
deskripsi String deskripsi String
ikonUrl String? icon String
isActive Boolean @default(true) isActive Boolean @default(true)
statistikId String? @unique statistikId String? @unique
statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id]) statistik StatistikKemiskinan? @relation(fields: [statistikId], references: [id])

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import React from 'react'
import {
IconLeaf,
IconTrophy,
IconTent,
IconChartLine,
IconRecycle,
IconTruck,
IconScale,
IconClipboard,
IconTrash,
IconHomeEco,
IconChristmasTreeFilled,
IconTrendingUp,
IconShieldFilled,
IconHome,
IconTree,
IconDroplet,
IconCash,
IconSchool,
IconShoppingCart,
IconHospital,
} from '@tabler/icons-react'
export type IconKey =
| 'ekowisata'
| 'kompetisi'
| 'wisata'
| 'ekonomi'
| 'sampah'
| 'truck'
| 'scale'
| 'clipboard'
| 'trash'
| 'lingkunganSehat'
| 'sumberOksigen'
| 'ekonomiBerkelanjutan'
| 'mencegahBencana'
| 'rumah'
| 'pohon'
| 'air'
| 'bantuan'
| 'pelatihan'
| 'subsidi'
| 'layananKesehatan'
const iconMap: Record<IconKey, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital,
}
type Props = {
name: IconKey
size?: number
color?: string
}
export const IconMapper: React.FC<Props> = ({ name, size = 24, color }) => {
const IconComponent = iconMap[name]
if (!IconComponent) return null
return <IconComponent size={size} color={color} />
}

View File

@@ -3,16 +3,20 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
@@ -32,13 +36,17 @@ const iconMap = {
scale: { label: 'Scale', icon: IconScale }, scale: { label: 'Scale', icon: IconScale },
clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled }, clipboard: { label: 'Clipboard', icon: IconClipboardTextFilled },
trash: { label: 'Trash', icon: IconTrashFilled }, trash: { label: 'Trash', icon: IconTrashFilled },
lingkunganSehat: {label: 'Lingkungan Sehat', icon: IconHomeEco}, lingkunganSehat: { label: 'Lingkungan Sehat', icon: IconHomeEco },
sumberOksigen: {label: 'Sumber Oksigen', icon: IconChristmasTreeFilled}, sumberOksigen: { label: 'Sumber Oksigen', icon: IconChristmasTreeFilled },
ekonomiBerkelanjutan: {label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp}, ekonomiBerkelanjutan: { label: 'Ekonomi Berkelanjutan', icon: IconTrendingUp },
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: { label: 'Mencegah Bencana', icon: IconShieldFilled },
rumah: {label: 'Rumah', icon: IconHome}, rumah: { label: 'Rumah', icon: IconHome },
pohon: {label: 'Pohon', icon: IconTree}, pohon: { label: 'Pohon', icon: IconTree },
air: {label: 'Air', icon: IconDroplet} air: { label: 'Air', icon: IconDroplet },
bantuan: { label: 'Bantuan', icon: IconCash },
pelatihan: { label: 'Pelatihan', icon: IconSchool },
subsidi: { label: 'Subsidi', icon: IconShoppingCart },
layananKesehatan: { label: 'Layanan Kesehatan', icon: IconHospital },
}; };

View File

@@ -2,16 +2,20 @@
import { Box, rem, Select } from '@mantine/core'; import { Box, rem, Select } from '@mantine/core';
import { import {
IconCash,
IconChartLine, IconChartLine,
IconChristmasTreeFilled, IconChristmasTreeFilled,
IconClipboardTextFilled, IconClipboardTextFilled,
IconDroplet, IconDroplet,
IconHome, IconHome,
IconHomeEco, IconHomeEco,
IconHospital,
IconLeaf, IconLeaf,
IconRecycle, IconRecycle,
IconScale, IconScale,
IconSchool,
IconShieldFilled, IconShieldFilled,
IconShoppingCart,
IconTent, IconTent,
IconTrashFilled, IconTrashFilled,
IconTree, IconTree,
@@ -36,7 +40,11 @@ const iconMap = {
mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled}, mencegahBencana: {label: 'Mencegah Bencana', icon: IconShieldFilled},
rumah: {label: 'Rumah', icon: IconHome}, rumah: {label: 'Rumah', icon: IconHome},
pohon: {label: 'Pohon', icon: IconTree}, pohon: {label: 'Pohon', icon: IconTree},
air: {label: 'Air', icon: IconDroplet} air: {label: 'Air', icon: IconDroplet},
bantuan: {label: 'Bantuan', icon: IconCash},
pelatihan: {label: 'Pelatihan', icon: IconSchool},
subsidi: {label: 'Subsidi', icon: IconShoppingCart},
layananKesehatan: {label: 'Layanan Kesehatan', icon: IconHospital},
}; };
type IconKey = keyof typeof iconMap; type IconKey = keyof typeof iconMap;

View File

@@ -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";
@@ -71,12 +72,37 @@ const demografiPekerjaan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
async load() { page: 1,
const res = await ApiFetch.api.ekonomi.demografipekerjaan[ totalPages: 1,
"find-many" loading: false,
].get(); search: "",
if (res.status === 200) { load: async (page = 1, limit = 10, search = "") => {
demografiPekerjaan.findMany.data = res.data?.data ?? []; demografiPekerjaan.findMany.loading = true; // ✅ Akses langsung via nama path
demografiPekerjaan.findMany.page = page;
demografiPekerjaan.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.demografipekerjaan[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
demografiPekerjaan.findMany.data = res.data.data ?? [];
demografiPekerjaan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch demografi pekerjaan paginated:", err);
demografiPekerjaan.findMany.data = [];
demografiPekerjaan.findMany.totalPages = 1;
} finally {
demografiPekerjaan.findMany.loading = false;
} }
}, },
}, },
@@ -194,4 +220,4 @@ const demografiPekerjaan = proxy({
}, },
}, },
}); });
export default demografiPekerjaan export default demografiPekerjaan;

View File

@@ -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";
@@ -69,16 +70,37 @@ const jumlahPendudukMiskin = proxy({
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { jumlahPendudukMiskin.findMany.loading = true; // ✅ Akses langsung via nama path
jumlahPendudukMiskin.findMany.data = res.data?.data ?? []; jumlahPendudukMiskin.findMany.page = page;
} jumlahPendudukMiskin.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
jumlahPendudukMiskin.findMany.data = res.data.data ?? [];
jumlahPendudukMiskin.findMany.totalPages = res.data.totalPages ?? 1;
} else {
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch jumlah penduduk miskin paginated:", err);
jumlahPendudukMiskin.findMany.data = [];
jumlahPendudukMiskin.findMany.totalPages = 1;
} finally {
jumlahPendudukMiskin.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{ data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true }; select: { id: true; year: true; totalPoorPopulation: true };

View File

@@ -8,7 +8,7 @@ import { z } from "zod";
const templateForm = z.object({ const templateForm = z.object({
nama: z.string().min(1, "Nama minimal 1 karakter"), nama: z.string().min(1, "Nama minimal 1 karakter"),
deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"), deskripsi: z.string().min(1, "Deskripsi minimal 1 karakter"),
ikonUrl: z.string().optional(), icon: z.string().min(1, "Icon minimal 1 karakter"),
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"),
@@ -18,7 +18,7 @@ const templateForm = z.object({
const defaultForm = { const defaultForm = {
nama: "", nama: "",
deskripsi: "", deskripsi: "",
ikonUrl: "", icon: "",
statistik: { statistik: {
tahun: "", tahun: "",
jumlah: "", jumlah: "",
@@ -148,7 +148,7 @@ const programKemiskinanState = proxy({
this.form = { this.form = {
nama: data.nama, nama: data.nama,
deskripsi: data.deskripsi, deskripsi: data.deskripsi,
ikonUrl: data.ikonUrl || "", icon: data.icon,
statistik: { statistik: {
tahun: data.statistik.tahun, tahun: data.statistik.tahun,
jumlah: data.statistik.jumlah, jumlah: data.statistik.jumlah,
@@ -189,7 +189,7 @@ const programKemiskinanState = proxy({
body: JSON.stringify({ body: JSON.stringify({
nama: this.form.nama, nama: this.form.nama,
deskripsi: this.form.deskripsi, deskripsi: this.form.deskripsi,
ikonUrl: this.form.ikonUrl, icon: this.form.icon,
statistik: { statistik: {
tahun: this.form.statistik.tahun, tahun: this.form.statistik.tahun,
jumlah: this.form.statistik.jumlah, jumlah: this.form.statistik.jumlah,

View File

@@ -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";
@@ -76,13 +77,37 @@ const grafikSektorUnggulan = proxy({
}; };
}>[] }>[]
| null, | null,
page: 1,
totalPages: 1,
loading: false, loading: false,
async load() { search: "",
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[ load: async (page = 1, limit = 10, search = "") => {
"find-many" grafikSektorUnggulan.findMany.loading = true; // ✅ Akses langsung via nama path
].get(); grafikSektorUnggulan.findMany.page = page;
if (res.status === 200) { grafikSektorUnggulan.findMany.search = search;
grafikSektorUnggulan.findMany.data = res.data?.data ?? [];
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.sektourunggulandesa[
"find-many"
].get({ query });
if (res.status === 200 && res.data?.success) {
grafikSektorUnggulan.findMany.data = res.data.data ?? [];
grafikSektorUnggulan.findMany.totalPages =
res.data.totalPages ?? 1;
} else {
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch sektor unggulan desa paginated:", err);
grafikSektorUnggulan.findMany.data = [];
grafikSektorUnggulan.findMany.totalPages = 1;
} finally {
grafikSektorUnggulan.findMany.loading = false;
} }
}, },
}, },

View File

@@ -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";
@@ -75,16 +76,37 @@ const grafikBerdasarkanUsiaKerjaNganggur = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data?.data ?? []; grafikBerdasarkanUsiaKerjaNganggur.findMany.page = page;
} grafikBerdasarkanUsiaKerjaNganggur.findMany.search = search;
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikusiakerjayangmenganggur["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = res.data.data ?? [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan usia kerja yang menganggur paginated:", err);
grafikBerdasarkanUsiaKerjaNganggur.findMany.data = [];
grafikBerdasarkanUsiaKerjaNganggur.findMany.totalPages = 1;
} finally {
grafikBerdasarkanUsiaKerjaNganggur.findMany.loading = false;
}
},
}, },
},
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanUsiaGetPayload<{
omit: { isActive: true }; omit: { isActive: true };
@@ -259,15 +281,36 @@ const grafikBerdasarkanPendidikan = proxy({
omit: { isActive: true }; omit: { isActive: true };
}>[] }>[]
| null, | null,
loading: false, page: 1,
async load() { totalPages: 1,
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan[ loading: false,
"find-many" search: "",
].get(); load: async (page = 1, limit = 10, search = "") => {
if (res.status === 200) { grafikBerdasarkanPendidikan.findMany.loading = true; // ✅ Akses langsung via nama path
grafikBerdasarkanPendidikan.findMany.data = res.data?.data ?? []; grafikBerdasarkanPendidikan.findMany.page = page;
} grafikBerdasarkanPendidikan.findMany.search = search;
},
try {
const query: any = { page, limit };
if (search) query.search = search;
const res = await ApiFetch.api.ekonomi.grafikmenganggurberdasarkanpendidikan["find-many"].get({ query });
if (res.status === 200 && res.data?.success) {
grafikBerdasarkanPendidikan.findMany.data = res.data.data ?? [];
grafikBerdasarkanPendidikan.findMany.totalPages = res.data.totalPages ?? 1;
} else {
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
}
} catch (err) {
console.error("Gagal fetch grafik berdasarkan pendidikan paginated:", err);
grafikBerdasarkanPendidikan.findMany.data = [];
grafikBerdasarkanPendidikan.findMany.totalPages = 1;
} finally {
grafikBerdasarkanPendidikan.findMany.loading = false;
}
},
}, },
findUnique: { findUnique: {
data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{ data: null as Prisma.GrafikMenganggurBerdasarkanPendidikanGetPayload<{

View File

@@ -1,8 +1,17 @@
/* 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, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
@@ -11,33 +20,33 @@ import { useProxy } from 'valtio/utils';
import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function EditDemografiPekerjaan() { function EditDemografiPekerjaan() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stateDemografi = useProxy(demografiPekerjaan) const stateDemografi = useProxy(demografiPekerjaan);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if (!id) return; if (!id) return;
stateDemografi.update.id = id; stateDemografi.update.id = id;
stateDemografi.findUnique.load(id) stateDemografi.findUnique
.load(id)
.then(() => { .then(() => {
const data = stateDemografi.findUnique.data; const data = stateDemografi.findUnique.data;
if (data) { if (data) {
stateDemografi.update.form = { stateDemografi.update.form = {
pekerjaan: String(data.pekerjaan || ''), pekerjaan: String(data.pekerjaan || ''),
lakiLaki: Number(data.lakiLaki || 0), lakiLaki: Number(data.lakiLaki || 0),
perempuan: Number(data.perempuan || 0) perempuan: Number(data.perempuan || 0),
}; };
} }
}) })
.catch(error => { .catch((error) => {
console.error('Error loading data:', error); console.error('Error loading data:', error);
toast.error('Gagal memuat data'); toast.error('Gagal memuat data');
}); });
}, [id]); }, [id]);
// Di handleSubmit, ubah menjadi:
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
stateDemografi.update.id = id; stateDemografi.update.id = id;
@@ -51,52 +60,88 @@ function EditDemografiPekerjaan() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> onClick={() => router.back()}
<Stack> p="xs"
<Title order={3}>Edit Demografi Pekerjaan</Title> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Demografi Pekerjaan
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Pekerjaan" label="Pekerjaan"
placeholder="masukkan pekerjaan" placeholder="Masukkan jenis pekerjaan"
value={stateDemografi.update.form.pekerjaan} value={stateDemografi.update.form.pekerjaan}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.pekerjaan = val.currentTarget.value; (stateDemografi.update.form.pekerjaan = e.currentTarget.value)
}} }
required
/> />
<TextInput <TextInput
label="Jumlah Pekerja Laki - Laki" label="Jumlah Pekerja Laki-laki"
type="number" type="number"
placeholder="masukkan jumlah pekerja laki - laki" placeholder="Masukkan jumlah pekerja laki-laki"
value={stateDemografi.update.form.lakiLaki} value={stateDemografi.update.form.lakiLaki}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.lakiLaki = Number(val.currentTarget.value); (stateDemografi.update.form.lakiLaki = Number(
}} e.currentTarget.value
))
}
required
/> />
<TextInput <TextInput
label="Jumlah Pekerja Perempuan" label="Jumlah Pekerja Perempuan"
type="number" type="number"
placeholder="masukkan jumlah pekerja perempuan" placeholder="Masukkan jumlah pekerja perempuan"
value={stateDemografi.update.form.perempuan} value={stateDemografi.update.form.perempuan}
onChange={(val) => { onChange={(e) =>
stateDemografi.update.form.perempuan = Number(val.currentTarget.value); (stateDemografi.update.form.perempuan = Number(
}} e.currentTarget.value
))
}
required
/> />
<Button
mt={10} <Group justify="flex-end">
bg={colors['blue-button']} <Button
onClick={handleSubmit} onClick={handleSubmit}
> radius="md"
Simpan Perubahan size="md"
</Button> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
) );
} }
export default EditDemografiPekerjaan; export default EditDemografiPekerjaan;

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* 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, Group, Paper, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -12,15 +22,15 @@ import demografiPekerjaan from '../../../_state/ekonomi/demografi-pekerjaan';
function CreateDemografiPekerjaan() { function CreateDemografiPekerjaan() {
const stateDemografi = useProxy(demografiPekerjaan); const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateDemografi.create.form = { stateDemografi.create.form = {
pekerjaan: "", pekerjaan: '',
lakiLaki: 0, lakiLaki: 0,
perempuan: 0, perempuan: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateDemografi.create.create(); const id = await stateDemografi.create.create();
@@ -32,58 +42,85 @@ function CreateDemografiPekerjaan() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/demografi-pekerjaan"); router.push('/admin/ekonomi/demografi-pekerjaan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Box> onClick={() => router.back()}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> p="xs"
<Title order={4}>Tambah Demografi Pekerjaan</Title> radius="md"
<Stack gap={"xs"}> >
<TextInput <IconArrowBack color={colors['blue-button']} size={24} />
label="Pekerjaan" </Button>
type="text" </Tooltip>
value={stateDemografi.create.form.pekerjaan} <Title order={4} ml="sm" c="dark">
placeholder="Masukkan pekerjaan" Tambah Demografi Pekerjaan
onChange={(val) => { </Title>
stateDemografi.create.form.pekerjaan = val.currentTarget.value; </Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Pekerjaan"
type="text"
value={stateDemografi.create.form.pekerjaan}
placeholder="Masukkan pekerjaan"
onChange={(val) => {
stateDemografi.create.form.pekerjaan = val.currentTarget.value;
}}
required
/>
<TextInput
label="Jumlah Pekerja Laki-Laki"
type="number"
value={stateDemografi.create.form.lakiLaki}
placeholder="Masukkan jumlah pekerja laki-laki"
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
required
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
/> >
<TextInput Simpan
label="Jumlah Pekerja Laki - Laki" </Button>
type="number" </Group>
value={stateDemografi.create.form.lakiLaki} </Stack>
placeholder="Masukkan jumlah pekerja laki - laki" </Paper>
onChange={(val) => {
stateDemografi.create.form.lakiLaki = Number(val.currentTarget.value);
}}
/>
<TextInput
label="Jumlah Pekerja Perempuan"
type="number"
value={stateDemografi.create.form.perempuan}
placeholder="Masukkan jumlah pekerja perempuan"
onChange={(val) => {
stateDemografi.create.form.perempuan = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,14 +1,32 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import { Box, Button, Paper, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
Box,
Button,
Center,
Group,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
Pagination,
Flex,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, 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';
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 { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan'; import demografiPekerjaan from '../../_state/ekonomi/demografi-pekerjaan';
@@ -18,7 +36,7 @@ function DemografiPekerjaan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Demografi Pekerjaan' title='Demografi Pekerjaan'
placeholder='pencarian' placeholder='Cari pekerjaan atau jumlah pekerja...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -34,131 +52,193 @@ function ListDemografiPekerjaan({ search }: { search: string }) {
pekerjaan: string; pekerjaan: string;
lakiLaki: number; lakiLaki: number;
perempuan: number; perempuan: number;
} };
const router = useRouter(); const router = useRouter();
const stateDemografi = useProxy(demografiPekerjaan) const stateDemografi = useProxy(demografiPekerjaan);
const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]); const [chartData, setChartData] = useState<DemografiPekerjaan[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
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,
} = stateDemografi.findMany;
const handleDelete = () => { const handleDelete = () => {
if (selectedId) { if (selectedId) {
stateDemografi.delete.byId(selectedId) stateDemografi.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stateDemografi.findMany.load()
} }
} };
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateDemografi.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true); if (data) {
if (stateDemografi.findMany.data) { setChartData(
setChartData(stateDemografi.findMany.data.map((item) => ({ data.map((item) => ({
id: item.id, id: item.id,
pekerjaan: item.pekerjaan, pekerjaan: item.pekerjaan,
lakiLaki: Number(item.lakiLaki), lakiLaki: Number(item.lakiLaki),
perempuan: Number(item.perempuan), perempuan: Number(item.perempuan),
}))); }))
);
} }
}, [stateDemografi.findMany.data]); }, [data]);
const filteredData = (stateDemografi.findMany.data || []).filter(item => { const filteredData = data || [];
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.pekerjaan.toLowerCase().includes(keyword) || <Stack py={10}>
item.lakiLaki.toString().toLowerCase().includes(keyword) || <Skeleton height={600} radius="md" />
item.perempuan.toString().toLowerCase().includes(keyword) </Stack>
); );
}); }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Demografi Pekerjaan' <Title order={4}>List Demografi Pekerjaan</Title>
href='/admin/ekonomi/demografi-pekerjaan/create' <Tooltip label="Tambah Data Pekerjaan" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
<TableThead> color="blue"
<TableTr> variant="light"
<TableTh>Pekerjaan</TableTh> onClick={() => router.push('/admin/ekonomi/demografi-pekerjaan/create')}
<TableTh>Jumlah Pekerja Laki - Laki</TableTh> >
<TableTh>Jumlah Pekerja Perempuan</TableTh> Tambah Baru
<TableTh>Edit</TableTh> </Button>
<TableTh>Delete</TableTh> </Tooltip>
</TableTr> </Group>
</TableThead>
<TableTbody> <Box style={{ overflowX: 'auto' }}>
{filteredData.map((item) => ( <Table highlightOnHover>
<TableTr key={item.id}> <TableThead>
<TableTd>{item.pekerjaan}</TableTd> <TableTr>
<TableTd>{item.lakiLaki}</TableTd> <TableTh>Pekerjaan</TableTh>
<TableTd>{item.perempuan}</TableTd> <TableTh>Laki - Laki</TableTh>
<TableTd> <TableTh>Perempuan</TableTh>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)}> <TableTh>Edit</TableTh>
<IconEdit size={20} /> <TableTh>Hapus</TableTh>
</Button>
</TableTd>
<TableTd>
<Button
color='red'
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.pekerjaan}</TableTd>
<TableTd>{item.lakiLaki}</TableTd>
<TableTd>{item.perempuan}</TableTd>
<TableTd>
<Button
variant="light"
color="green"
onClick={() =>
router.push(`/admin/ekonomi/demografi-pekerjaan/${item.id}`)
}
>
<IconEdit size={18} />
</Button>
</TableTd>
<TableTd>
<Button
variant="light"
color="red"
disabled={stateDemografi.delete.loading}
onClick={() => {
setSelectedId(item.id);
setModalHapus(true);
}}
>
<IconTrash size={18} />
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data demografi pekerjaan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10, search);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */} {/* Chart */}
{!mounted && !chartData ? ( <Box mt={30} style={{ width: '100%', minHeight: 400 }}>
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Paper bg={colors['white-1']} p="md" radius="md" withBorder>
<Paper bg={colors['white-1']} p={'md'}> <Stack gap={"xs"}>
<Title pb={10} order={3}>Data Kelahiran & Kematian</Title> <Title pb={10} order={4}>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> Grafik Demografi Pekerjaan
</Paper> </Title>
</Box> {mounted && chartData.length > 0 ? (
) : ( <BarChart
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> h={450}
<Paper bg={colors['white-1']} p={'md'}> data={chartData}
<Title pb={10} order={4}>Data Kelahiran & Kematian</Title> dataKey="pekerjaan"
{mounted && chartData.length > 0 && ( type="stacked"
<Box w={{ base: '100%', md: '30%' }}> series={[
<BarChart { name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
h={450} { name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
data={chartData} ]}
dataKey="pekerjaan" />
type="stacked" ) : (
series={[ <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
{ name: 'lakiLaki', color: '#5082EE', label: 'Laki - Laki' },
{ name: 'perempuan', color: '#6EDF9C', label: 'Perempuan' },
]}
/>
</Box>
)} )}
</Paper> <Box py={10}>
</Box> <Group justify='center'>
)} <Flex align="center" gap={10}>
<Box bg="#5082EE" w={20} h={20} />
<Text>Laki - Laki</Text>
</Flex>
<Flex align="center" gap={10}>
<Box bg="#6EDF9C" w={20} h={20} />
<Text>Perempuan</Text>
</Flex>
</Group>
</Box>
</Stack>
</Paper>
</Box>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus demografi pekerjaan ini?' text="Apakah anda yakin ingin menghapus demografi pekerjaan ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,78 +3,116 @@
import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-penduduk-miskin';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditJumlahPendudukMiskin() { function EditJumlahPendudukMiskin() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stateJPM = useProxy(jumlahPendudukMiskin) const stateJPM = useProxy(jumlahPendudukMiskin);
const id = params.id const id = params.id;
// Load data saat komponen mount
useEffect(() => { useEffect(() => {
if (id) { if (!id) return;
stateJPM.findUnique.load(id).then(() => {
const data = stateJPM.findUnique.data const loadData = async () => {
try {
await stateJPM.findUnique.load(id);
const data = stateJPM.findUnique.data;
if (data) { if (data) {
stateJPM.update.form = { stateJPM.update.form = {
year: data.year || 0, year: data.year || 0,
totalPoorPopulation: data.totalPoorPopulation || 0, totalPoorPopulation: data.totalPoorPopulation || 0,
} };
} }
}) } catch (error) {
} console.error('Gagal memuat data:', error);
}, [id]) toast.error('Gagal memuat data jumlah penduduk miskin');
}
};
loadData();
}, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
// Set the ID before submitting try {
stateJPM.update.id = id; stateJPM.update.id = id;
await stateJPM.update.submit(); await stateJPM.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-miskin') toast.success('Data jumlah penduduk miskin berhasil diperbarui!');
} router.push('/admin/ekonomi/jumlah-penduduk-miskin');
return ( } catch (error) {
<Box> console.error('Gagal menyimpan data:', error);
<Box mb={10}> toast.error('Terjadi kesalahan saat menyimpan data');
<Button variant="subtle" onClick={() => router.back()}> }
<IconArrowBack size={20} /> };
</Button>
</Box> return (
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack> <Group mb="md">
<Title order={3}>Edit Jumlah Penduduk Miskin</Title> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Jumlah Penduduk Miskin
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Tahun" label="Tahun"
placeholder="masukkan tahun" placeholder="Masukkan tahun"
type="number"
required
value={stateJPM.update.form.year} value={stateJPM.update.form.year}
onChange={(val) => { onChange={(val) => {
stateJPM.update.form.year = Number(val.currentTarget.value); stateJPM.update.form.year = Number(val.currentTarget.value);
}} }}
/> />
<TextInput <TextInput
label="Jumlah Penduduk Miskin" label="Jumlah Penduduk Miskin"
placeholder="Masukkan jumlah penduduk miskin"
type="number" type="number"
placeholder="masukkan jumlah penduduk miskin" required
value={stateJPM.update.form.totalPoorPopulation} value={stateJPM.update.form.totalPoorPopulation}
onChange={(val) => { onChange={(val) => {
stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value); stateJPM.update.form.totalPoorPopulation = Number(val.currentTarget.value);
}} }}
/> />
<Button
mt={10} <Group justify="right">
bg={colors['blue-button']} <Button
onClick={handleSubmit} onClick={handleSubmit}
> radius="md"
Simpan size="md"
</Button> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
) );
} }
export default EditJumlahPendudukMiskin; export default EditJumlahPendudukMiskin;

View File

@@ -1,26 +1,25 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client';
import grafikkepuasan from '@/app/admin/(dashboard)/_state/kesehatan/data_kesehatan_warga/grafikKepuasan'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import colors from '@/con/colors';
import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../../_state/ekonomi/jumlah-penduduk-miskin';
function CreateJumlahPendudukMiskin() { export default function CreateJumlahPendudukMiskin() {
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateJPM.create.form = { stateJPM.create.form = {
year: new Date().getFullYear(), // Default to current year year: new Date().getFullYear(),
totalPoorPopulation: 0, totalPoorPopulation: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateJPM.create.create(); const id = await stateJPM.create.create();
@@ -32,52 +31,72 @@ function CreateJumlahPendudukMiskin() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-miskin"); router.push('/admin/ekonomi/jumlah-penduduk-miskin');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Box> </Button>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> </Tooltip>
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> <Title order={4} ml="sm" c="dark">
<Stack gap={"xs"}> Tambah Jumlah Penduduk Miskin
<TextInput </Title>
label="Tahun" </Group>
type="number"
value={stateJPM.create.form.year || ''} {/* Form Paper */}
placeholder="Masukkan tahun" <Paper
onChange={(val) => { w={{ base: '100%', md: '50%' }}
const value = val.currentTarget.value; bg={colors['white-1']}
stateJPM.create.form.year = value ? Number(value) : 0; p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Tahun"
type="number"
value={stateJPM.create.form.year || ''}
placeholder="Masukkan tahun"
onChange={(e) => {
const value = e.currentTarget.value;
stateJPM.create.form.year = value ? Number(value) : 0;
}}
required
/>
<TextInput
label="Jumlah Penduduk Miskin"
type="number"
value={stateJPM.create.form.totalPoorPopulation}
placeholder="Masukkan jumlah penduduk miskin"
onChange={(e) => {
stateJPM.create.form.totalPoorPopulation = Number(e.currentTarget.value);
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
/> >
<TextInput Simpan
label="Jumlah Penduduk Miskin" </Button>
type="number" </Group>
value={stateJPM.create.form.totalPoorPopulation} </Stack>
placeholder="Masukkan jumlah penduduk miskin" </Paper>
onChange={(val) => {
stateJPM.create.form.totalPoorPopulation = Number(val.currentTarget.value);
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }
export default CreateJumlahPendudukMiskin;

View File

@@ -1,15 +1,14 @@
'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, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useShallowEffect, useMediaQuery } from '@mantine/hooks';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin'; import jumlahPendudukMiskin from '../../_state/ekonomi/jumlah-penduduk-miskin';
import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip } from 'recharts'; import { Bar, BarChart, Legend, XAxis, YAxis, Tooltip as RechartsTooltip } from 'recharts';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
function JumlahPendudukMiskin() { function JumlahPendudukMiskin() {
@@ -18,7 +17,7 @@ function JumlahPendudukMiskin() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Jumlah Penduduk Miskin' title='Jumlah Penduduk Miskin'
placeholder='pencarian' placeholder='Cari tahun atau jumlah penduduk miskin...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,138 +28,158 @@ function JumlahPendudukMiskin() {
} }
function ListJumlahPendudukMiskin({ search }: { search: string }) { function ListJumlahPendudukMiskin({ search }: { search: string }) {
type JPMGrafik = { type JPMGrafik = { id: string; year: number; totalPoorPopulation: number }
id: string;
year: number;
totalPoorPopulation: number;
}
const stateJPM = useProxy(jumlahPendudukMiskin); const stateJPM = useProxy(jumlahPendudukMiskin);
const [chartData, setChartData] = useState<JPMGrafik[]>([]); const [chartData, setChartData] = useState<JPMGrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready const [mounted, setMounted] = useState(false);
const isTablet = useMediaQuery('(max-width: 1024px)') const [modalHapus, setModalHapus] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)') const [selectedId, setSelectedId] = useState<string | null>(null);
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const handleDelete = () => { const isTablet = useMediaQuery('(max-width:1024px)');
if (selectedId) { const isMobile = useMediaQuery('(max-width:768px)');
stateJPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateJPM.findMany.load()
}
}
const {
data,
page,
loading,
load,
totalPages,
} = stateJPM.findMany;
// Load data
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateJPM.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true);
if (stateJPM.findMany.data) { if (stateJPM.findMany.data) {
setChartData(stateJPM.findMany.data.map((item) => ({ setChartData(stateJPM.findMany.data.map(item => ({
id: item.id, id: item.id,
year: Number(item.year), year: Number(item.year),
totalPoorPopulation: Number(item.totalPoorPopulation), totalPoorPopulation: Number(item.totalPoorPopulation)
}))); })));
} }
}, [stateJPM.findMany.data]); }, [stateJPM.findMany.data]);
const filteredData = (stateJPM.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
return (
item.year.toString().toLowerCase().includes(keyword) ||
item.totalPoorPopulation.toString().toLowerCase().includes(keyword)
);
});
if (!stateJPM.findMany.data) { const handleDelete = () => {
if (selectedId) {
stateJPM.delete.byId(selectedId);
setModalHapus(false);
setSelectedId(null);
stateJPM.findMany.load();
}
}
if (loading || !data) {
return ( return (
<Stack> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Stack gap={'xs'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={'md'}> <Group justify="space-between" mb="md">
<JudulList <Title order={4}>Daftar Jumlah Penduduk Miskin</Title>
title='List Jumlah Penduduk Miskin' <Tooltip label="Tambah Data" withArrow>
href='/admin/ekonomi/jumlah-penduduk-miskin/create' <Button
/> leftSection={<IconEdit size={18} />}
<Table striped withTableBorder withRowBorders> color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-miskin/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Tahun</TableTh> <TableTh style={{ width: '25%' }}>Tahun</TableTh>
<TableTh>Jumlah Penduduk Miskin</TableTh> <TableTh style={{ width: '35%' }}>Jumlah Penduduk Miskin</TableTh>
<TableTh>Edit</TableTh> <TableTh style={{ width: '20%' }}>Edit</TableTh>
<TableTh>Delete</TableTh> <TableTh style={{ width: '20%' }}>Delete</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? filteredData.map(item => (
<TableTr key={item.id}> <TableTr key={item.id}>
<TableTd>{item.year}</TableTd> <TableTd>{item.year}</TableTd>
<TableTd>{item.totalPoorPopulation}</TableTd> <TableTd>{item.totalPoorPopulation}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}> <Button variant='light' color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-miskin/${item.id}`)}>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Button variant='light' color="red" disabled={stateJPM.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true) }}>
color='red'
disabled={stateJPM.delete.loading}
onClick={() => {
setSelectedId(item.id)
setModalHapus(true)
}}>
<IconTrash size={20} /> <IconTrash size={20} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
{/* Chart */} <Center>
{!mounted && !chartData ? ( <Pagination
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> value={page}
<Paper bg={colors['white-1']} p={'md'}> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Jumlah Penduduk Miskin</Title> load(newPage, 10);
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> window.scrollTo({ top: 0, behavior: 'smooth' });
</Paper> }}
</Box> total={totalPages}
) : ( mt="md"
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> mb="md"
<Paper bg={colors['white-1']} p={'md'}> color="blue"
<Title pb={10} order={4}>Grafik Jumlah Penduduk Miskin</Title> radius="md"
{mounted && chartData.length > 0 && ( />
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} > </Center>
<XAxis dataKey="year" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Chart */}
<Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
<Stack>
<Box mt="lg" style={{ width: '100%', minHeight: 350 }}>
<Title order={4} mb="sm">Grafik Jumlah Penduduk Miskin</Title>
{mounted && chartData.length > 0 ? (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData}>
<XAxis dataKey="year" />
<YAxis />
<RechartsTooltip />
<Legend />
<Bar dataKey="totalPoorPopulation" fill={colors['blue-button']} name="Jumlah Penduduk Miskin" />
</BarChart>
) : (
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box>
</Stack>
</Paper>
{/* Modal Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik jumlah penduduk miskin ini?' text='Apakah anda yakin ingin menghapus data ini?'
/> />
</Box> </Box>
); );

View File

@@ -1,62 +1,124 @@
/* 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 { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core'; import {
Stack,
Tabs,
TabsList,
TabsPanel,
TabsTab,
Title,
Tooltip,
ScrollArea,
} from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { IconUsers, IconSchool } from '@tabler/icons-react';
function LayoutTabs({ children }: { children: React.ReactNode }) { function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter() const router = useRouter();
const pathname = usePathname() const pathname = usePathname();
const tabs = [
{
label: "Pengangguran Berdasarkan Usia",
value: "pengangguranberdasarkanusia",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia"
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan"
},
];
const curentTab = tabs.find(tab => tab.href === pathname)
const [activeTab, setActiveTab] = useState<string | null>(curentTab?.value || tabs[0].value);
const handleTabChange = (value: string | null) => { const tabs = [
const tab = tabs.find(t => t.value === value) {
if (tab) { label: "Pengangguran Berdasarkan Usia",
router.push(tab.href) value: "pengangguranberdasarkanusia",
} href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia",
setActiveTab(value) icon: <IconUsers size={18} stroke={1.8} />,
} tooltip: "Data pengangguran menurut kelompok usia",
},
{
label: "Pengangguran Berdasarkan Pendidikan",
value: "pengangguranberdasarkanpendidikan",
href: "/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan",
icon: <IconSchool size={18} stroke={1.8} />,
tooltip: "Data pengangguran menurut tingkat pendidikan",
},
];
useEffect(() => { const currentTab = tabs.find(tab => tab.href === pathname);
const match = tabs.find(tab => tab.href === pathname) const [activeTab, setActiveTab] = useState<string | null>(currentTab?.value || tabs[0].value);
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return ( const handleTabChange = (value: string | null) => {
<Stack> const tab = tabs.find(t => t.value === value);
<Title order={3}>Jumlah Penduduk Usia Kerja yang Menganggur</Title> if (tab) router.push(tab.href);
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}> setActiveTab(value);
<TabsList p={"xs"} bg={"#BBC8E7FF"}> };
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab> useEffect(() => {
))} const match = tabs.find(tab => tab.href === pathname);
</TabsList> if (match) setActiveTab(match.value);
{tabs.map((e, i) => ( }, [pathname]);
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */} return (
<></> <Stack gap="lg">
</TabsPanel> <Title order={2} fw={700} style={{ color: "#1A1B1E" }}>
))} Jumlah Penduduk Usia Kerja yang Menganggur
</Tabs> </Title>
<Tabs
color={colors['blue-button']}
variant="pills"
value={activeTab}
onChange={handleTabChange}
radius="lg"
keepMounted={false}
>
<ScrollArea type="auto" offsetScrollbars>
<TabsList
p="sm"
style={{
background: "linear-gradient(135deg, #e7ebf7, #f9faff)",
borderRadius: "1rem",
boxShadow: "inset 0 0 10px rgba(0,0,0,0.05)",
display: "flex",
flexWrap: "nowrap",
gap: "0.5rem",
paddingInline: "0.5rem",
}}
>
{tabs.map((tab, i) => (
<Tooltip
key={i}
label={tab.tooltip}
position="bottom"
withArrow
transitionProps={{ transition: "pop", duration: 200 }}
>
<TabsTab
value={tab.value}
leftSection={tab.icon}
style={{
fontWeight: 600,
fontSize: "0.9rem",
transition: "all 0.2s ease",
flexShrink: 0,
}}
>
{tab.label}
</TabsTab>
</Tooltip>
))}
</TabsList>
</ScrollArea>
{tabs.map((tab, i) => (
<TabsPanel
key={i}
value={tab.value}
style={{
padding: "1.5rem",
background: "linear-gradient(180deg, #ffffff, #f5f6fa)",
borderRadius: "1rem",
boxShadow: "0 4px 16px rgba(0,0,0,0.05)",
}}
>
{children} {children}
</Stack> </TabsPanel>
); ))}
</Tabs>
</Stack>
);
} }
export default LayoutTabs; export default LayoutTabs;

View File

@@ -1,108 +1,117 @@
'use client' 'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, TextInput, Title, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditGrafikBerdasarkanPendidikan() { function EditGrafikBerdasarkanPendidikan() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if(id){ if (id) {
stategrafik.findUnique.load(id).then(() => { stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data const data = stategrafik.findUnique.data;
if(data){ if (data) {
stategrafik.update.form = { stategrafik.update.form = {
SD: data.SD || '', SD: data.SD || '',
SMP: data.SMP || '', SMP: data.SMP || '',
SMA: data.SMA || '', SMA: data.SMA || '',
D3: data.D3 || '', D3: data.D3 || '',
S1: data.S1 || '', S1: data.S1 || '',
} };
} }
}) });
} }
}, [id]) }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
stategrafik.update.id = id; stategrafik.update.id = id;
await stategrafik.update.submit(); await stategrafik.update.submit();
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan') router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Pendidikan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="SD"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => (stategrafik.update.form.SD = val.currentTarget.value)}
/>
<TextInput
label="SMP"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => (stategrafik.update.form.SMP = val.currentTarget.value)}
/>
<TextInput
label="SMA"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => (stategrafik.update.form.SMA = val.currentTarget.value)}
/>
<TextInput
label="D3"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => (stategrafik.update.form.D3 = val.currentTarget.value)}
/>
<TextInput
label="S1"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => (stategrafik.update.form.S1 = val.currentTarget.value)}
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Pendidikan</Title>
<TextInput
label="SD"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.SD}
onChange={(val) => {
stategrafik.update.form.SD = val.currentTarget.value;
}}
/>
<TextInput
label="SMP"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMP}
onChange={(val) => {
stategrafik.update.form.SMP = val.currentTarget.value;
}}
/>
<TextInput
label="SMA"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.SMA}
onChange={(val) => {
stategrafik.update.form.SMA = val.currentTarget.value;
}}
/>
<TextInput
label="D3"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.D3}
onChange={(val) => {
stategrafik.update.form.D3 = val.currentTarget.value;
}}
/>
<TextInput
label="S1"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.S1}
onChange={(val) => {
stategrafik.update.form.S1 = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,32 +1,30 @@
'use client' 'use client';
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'; import React from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin'; import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useState } from 'react'; import { useState } from 'react';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core'; import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
function CreateGrafikBerdasarkanPendidikan() { function CreateGrafikBerdasarkanPendidikan() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
...stategrafik.create.form, ...stategrafik.create.form,
SD: "", SD: '',
SMP: "", SMP: '',
SMA: "", SMA: '',
D3: "", D3: '',
S1: "", S1: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
@@ -38,73 +36,91 @@ function CreateGrafikBerdasarkanPendidikan() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan") router.push(
} '/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan'
);
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Box> </Button>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> </Tooltip>
<Stack> <Title order={4} ml="sm" c="dark">
<Title order={3}>Create Grafik Pengangguran Berdasarkan Pendidikan</Title> Tambah Data Pengangguran Berdasarkan Pendidikan
<TextInput </Title>
label="SD" </Group>
type='number'
placeholder="masukkan jumlah" <Paper
value={stategrafik.create.form.SD} w={{ base: '100%', md: '50%' }}
onChange={(val) => { bg={colors['white-1']}
stategrafik.create.form.SD = val.currentTarget.value; p="lg"
}} radius="md"
/> shadow="sm"
<TextInput style={{ border: '1px solid #e0e0e0' }}
label="SMP" >
type="number" <Stack gap="md">
placeholder="masukkan jumlah" <TextInput
value={stategrafik.create.form.SMP} label="SD"
onChange={(val) => { type="number"
stategrafik.create.form.SMP = val.currentTarget.value; placeholder="Masukkan jumlah"
}} value={stategrafik.create.form.SD}
/> onChange={(val) => (stategrafik.create.form.SD = val.currentTarget.value)}
<TextInput required
label="SMA" />
type="number" <TextInput
placeholder="masukkan jumlah" label="SMP"
value={stategrafik.create.form.SMA} type="number"
onChange={(val) => { placeholder="Masukkan jumlah"
stategrafik.create.form.SMA = val.currentTarget.value; value={stategrafik.create.form.SMP}
}} onChange={(val) => (stategrafik.create.form.SMP = val.currentTarget.value)}
/> required
<TextInput />
label="D3" <TextInput
type="number" label="SMA"
placeholder="masukkan jumlah" type="number"
value={stategrafik.create.form.D3} placeholder="Masukkan jumlah"
onChange={(val) => { value={stategrafik.create.form.SMA}
stategrafik.create.form.D3 = val.currentTarget.value; onChange={(val) => (stategrafik.create.form.SMA = val.currentTarget.value)}
}} required
/> />
<TextInput <TextInput
label="S1" label="D3"
type="number" type="number"
placeholder="masukkan jumlah" placeholder="Masukkan jumlah"
value={stategrafik.create.form.S1} value={stategrafik.create.form.D3}
onChange={(val) => { onChange={(val) => (stategrafik.create.form.D3 = val.currentTarget.value)}
stategrafik.create.form.S1 = val.currentTarget.value; required
}} />
/> <TextInput
<Button label="S1"
mt={10} type="number"
bg={colors['blue-button']} placeholder="Masukkan jumlah"
onClick={handleSubmit} value={stategrafik.create.form.S1}
> onChange={(val) => (stategrafik.create.form.S1 = val.currentTarget.value)}
Submit required
</Button> />
</Stack>
</Paper> <Group justify="right" mt="md">
</Box> <Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,59 +1,62 @@
/* 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, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, 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';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanPendidikan() { function GrafikBerdasarkanPendidikan() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran Berdasarkan Pendidikan' title='Detail Data Pengangguran Berdasarkan Pendidikan'
placeholder='pencarian' placeholder='Cari data pendidikan...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikBerdasarkanPendidikan search={search}/> <ListGrafikBerdasarkanPendidikan search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanPendidikan({search}: {search: string}) { function ListGrafikBerdasarkanPendidikan({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanPendidikan);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
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 router = useRouter(); const router = useRouter();
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
await stategrafik.delete.byId(selectedId) await stategrafik.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stategrafik.findMany.load();
stategrafik.findMany.load()
} }
} };
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
stategrafik.findMany.load() load(page, 10, search);
}, []); }, [page, search]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -70,36 +73,37 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
{ name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' }, { name: 'S1', value: S1, color: '#1018A8FF', key: 'S1' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.SD.toString().toLowerCase().includes(keyword) || <Stack py={10}>
item.SMP.toString().toLowerCase().includes(keyword) || <Skeleton height={500} radius="md" />
item.SMA.toString().toLowerCase().includes(keyword) || </Stack>
item.D3.toString().toLowerCase().includes(keyword) ||
item.S1.toString().toLowerCase().includes(keyword)
); );
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={"md"}> {/* Header */}
<JudulList <Flex justify="space-between" align="center" mb="md">
title='List Grafik Pengangguran Berdasarkan Pendidikan' <Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/create' <Tooltip label="Tambah Data" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Flex>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>SD</TableTh> <TableTh>SD</TableTh>
@@ -114,8 +118,10 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTbody> <TableTbody>
{filteredData.length === 0 ? ( {filteredData.length === 0 ? (
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTd colSpan={7}>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text> <Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
) : ( ) : (
@@ -127,82 +133,87 @@ function ListGrafikBerdasarkanPendidikan({search}: {search: string}) {
<TableTd>{item.D3}</TableTd> <TableTd>{item.D3}</TableTd>
<TableTd>{item.S1}</TableTd> <TableTd>{item.S1}</TableTd>
<TableTd> <TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}> <Tooltip label="Edit Data" withArrow>
<IconEdit size={20} /> <Button color="green" variant="light" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_pendidikan/${item.id}`)}>
</Button> <IconEdit size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Button <Tooltip label="Hapus Data" withArrow>
color='red' <Button
disabled={stategrafik.delete.loading} color="red"
onClick={() => { variant="light"
setSelectedId(item.id) disabled={stategrafik.delete.loading}
setModalHapus(true) onClick={() => {
}}> setSelectedId(item.id);
<IconTrash size={20} /> setModalHapus(true);
</Button> }}>
<IconTrash size={18} />
</Button>
</Tooltip>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) ))
)} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
{/* Chart */} <Center>
<Box> <Pagination
<Paper bg={colors['white-1']} p={'md'}> value={page}
<Stack> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Pendidikan</Title> load(newPage, 10);
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}> window.scrollTo({ top: 0, behavior: 'smooth' });
<PieChart }}
width={800} height={300} total={totalPages}
data={donutData} mt="md"
> mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */}
<Box mt="md">
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Stack>
<Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Pendidikan</Title>
{mounted && donutData.length > 0 ? (
<Box style={{ width: '100%', minHeight: 250 }}>
<PieChart width={800} height={300} data={donutData}>
<Pie <Pie
dataKey="value" dataKey="value"
nameKey="name" nameKey="name"
data={donutData} data={donutData}
cx={500} cx={400}
cy={150} cy={150}
innerRadius={60} innerRadius={60}
outerRadius={115} outerRadius={115}
label={true} label
> >
{donutData.map((entry, index) => ( {donutData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} /> <Cell key={`cell-${index}`} fill={entry.color} />
))} ))}
</Pie> </Pie>
</PieChart> </PieChart>
<Flex gap={"md"} align={"center"}> <Stack gap="xs" mt="sm">
<Box bg={colors['blue-button']} w={20} h={20} /> {donutData.map((entry) => (
<Text>SD : {donutData.find((entry) => entry.name === 'SD')?.value}</Text> <Flex key={entry.key} gap="sm" align="center">
</Flex> <Box w={20} h={20} bg={entry.color} />
<Flex gap={"md"} align={"center"}> <Text>{entry.name} : {entry.value}</Text>
<Box bg={'#10A85AFF'} w={20} h={20} /> </Flex>
<Text>SMP : {donutData.find((entry) => entry.name === 'SMP')?.value}</Text> ))}
</Flex> </Stack>
<Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} />
<Text>SMA : {donutData.find((entry) => entry.name === 'SMA')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} />
<Text>D3 : {donutData.find((entry) => entry.name === 'D3')?.value}</Text>
</Flex>
<Flex gap={"md"} align={"center"}>
<Box bg={'#A83610FF'} w={20} h={20} />
<Text>S1 : {donutData.find((entry) => entry.name === 'S1')?.value}</Text>
</Flex>
</Box> </Box>
) : ( ) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Text color="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
)} )}
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus

View File

@@ -1,98 +1,130 @@
'use client'
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import { Box, Button, Paper, Stack, TextInput, Title, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() { function EditGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const id = params.id const id = params.id;
useEffect(() => { useEffect(() => {
if(id){ if (id) {
stategrafik.findUnique.load(id).then(() => { stategrafik.findUnique.load(id).then(() => {
const data = stategrafik.findUnique.data const data = stategrafik.findUnique.data;
if(data){ if (data) {
stategrafik.update.form = { stategrafik.update.form = {
usia18_25: data.usia18_25 || '', usia18_25: data.usia18_25 || '',
usia26_35: data.usia26_35 || '', usia26_35: data.usia26_35 || '',
usia36_45: data.usia36_45 || '', usia36_45: data.usia36_45 || '',
usia46_keatas: data.usia46_keatas || '', usia46_keatas: data.usia46_keatas || '',
} };
} }
}) });
} }
}, [id]) }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
stategrafik.update.id = id; try {
await stategrafik.update.submit(); stategrafik.update.id = id;
router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia') await stategrafik.update.submit();
} toast.success('Data grafik berhasil diperbarui!');
router.push(
'/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia'
);
} catch (error) {
console.error(error);
toast.error('Terjadi kesalahan saat memperbarui data grafik');
}
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button variant="subtle" onClick={() => router.back()}> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<IconArrowBack size={20} /> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Button> <IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Grafik Pengangguran Berdasarkan Usia Kerja
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Usia 18 - 25"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
required
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="Masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
required
/>
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box> </Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Grafik Pengangguran Berdasarkan Usia Kerja</Title>
<TextInput
label="Usia 18 - 25"
type='number'
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia18_25}
onChange={(val) => {
stategrafik.update.form.usia18_25 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 26 - 35"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia26_35}
onChange={(val) => {
stategrafik.update.form.usia26_35 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 36 - 45"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia36_45}
onChange={(val) => {
stategrafik.update.form.usia36_45 = val.currentTarget.value;
}}
/>
<TextInput
label="Usia 46 +"
type="number"
placeholder="masukkan jumlah"
value={stategrafik.update.form.usia46_keatas}
onChange={(val) => {
stategrafik.update.form.usia46_keatas = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Submit
</Button>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,31 +1,29 @@
'use client'
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import React from 'react'; 'use client';
import { useRouter } from 'next/navigation';
import grafikBerdasarkanJenisKelamin from '@/app/admin/(dashboard)/_state/ppid/indeks_kepuasan_masyarakat/grafikBerdasarkanJenisKelamin';
import { useProxy } from 'valtio/utils';
import { useState } from 'react';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import grafikNganggur from '@/app/admin/(dashboard)/_state/ekonomi/usia-kerja-nganggur';
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Title, TextInput, Group, Tooltip } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() { function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
const router = useRouter(); const router = useRouter();
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const resetForm = () => { const resetForm = () => {
stategrafik.create.form = { stategrafik.create.form = {
...stategrafik.create.form, ...stategrafik.create.form,
usia18_25: "", usia18_25: '',
usia26_35: "", usia26_35: '',
usia36_45: "", usia36_45: '',
usia46_keatas: "", usia46_keatas: '',
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stategrafik.create.create(); const id = await stategrafik.create.create();
@@ -37,64 +35,84 @@ function CreateGrafikBerdasarkanUsiaKerjaYangMenganggur() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia") router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
</Box> <IconArrowBack color={colors['blue-button']} size={24} />
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> </Button>
<Stack> </Tooltip>
<Title order={3}>Create Grafik Pengangguran Berdasarkan Usia Kerja</Title> <Title order={4} ml="sm" c="dark">
<TextInput Tambah Data Pengangguran Berdasarkan Usia
label="Usia 18 - 25" </Title>
type='number' </Group>
placeholder="masukkan jumlah"
value={stategrafik.create.form.usia18_25} {/* Form Paper */}
onChange={(val) => { <Paper
stategrafik.create.form.usia18_25 = val.currentTarget.value; w={{ base: '100%', md: '50%' }}
}} bg={colors['white-1']}
/> p="lg"
<TextInput radius="md"
label="Usia 26 - 35" shadow="sm"
type="number" style={{ border: '1px solid #e0e0e0' }}
placeholder="masukkan jumlah" >
value={stategrafik.create.form.usia26_35} <Stack gap="md">
onChange={(val) => { <TextInput
stategrafik.create.form.usia26_35 = val.currentTarget.value; label="Usia 18 - 25"
}} type="number"
/> placeholder="Masukkan jumlah"
<TextInput value={stategrafik.create.form.usia18_25}
label="Usia 36 - 45" onChange={(val) => (stategrafik.create.form.usia18_25 = val.currentTarget.value)}
type="number" required
placeholder="masukkan jumlah" />
value={stategrafik.create.form.usia36_45} <TextInput
onChange={(val) => { label="Usia 26 - 35"
stategrafik.create.form.usia36_45 = val.currentTarget.value; type="number"
}} placeholder="Masukkan jumlah"
/> value={stategrafik.create.form.usia26_35}
<TextInput onChange={(val) => (stategrafik.create.form.usia26_35 = val.currentTarget.value)}
label="Usia 46 +" required
type="number" />
placeholder="masukkan jumlah" <TextInput
value={stategrafik.create.form.usia46_keatas} label="Usia 36 - 45"
onChange={(val) => { type="number"
stategrafik.create.form.usia46_keatas = val.currentTarget.value; placeholder="Masukkan jumlah"
}} value={stategrafik.create.form.usia36_45}
/> onChange={(val) => (stategrafik.create.form.usia36_45 = val.currentTarget.value)}
<Button required
mt={10} />
bg={colors['blue-button']} <TextInput
onClick={handleSubmit} label="Usia 46 +"
> type="number"
Submit placeholder="Masukkan jumlah"
</Button> value={stategrafik.create.form.usia46_keatas}
</Stack> onChange={(val) => (stategrafik.create.form.usia46_keatas = val.currentTarget.value)}
</Paper> required
</Box> />
{/* Submit Button */}
<Group justify="right">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack>
</Paper>
</Box>
); );
} }

View File

@@ -1,60 +1,62 @@
/* 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, Flex, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Flex, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react'; import { IconEdit, IconPlus, 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';
import { Cell, Pie, PieChart } from 'recharts'; import { Cell, Pie, PieChart } from 'recharts';
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 { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur'; import grafikNganggur from '../../../_state/ekonomi/usia-kerja-nganggur';
function GrafikBerdasarkanUsiaKerjaYangMenganggur() { function GrafikBerdasarkanUsiaKerjaYangMenganggur() {
const [search, setSearch] = useState("") const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='pencarian' placeholder='Cari usia...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} /> <ListGrafikBerdasarkanUsiaKerjaYangMenganggur search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}) { function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({ search }: { search: string }) {
const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur) const stategrafik = useProxy(grafikNganggur.grafikBerdasarkanUsiaKerjaNganggur);
const [donutData, setDonutData] = useState<any[]>([]); const [donutData, setDonutData] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
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 router = useRouter(); const router = useRouter();
const handleDelete = async () => { const handleDelete = async () => {
if (selectedId) { if (selectedId) {
await stategrafik.delete.byId(selectedId) await stategrafik.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
stategrafik.findMany.load();
stategrafik.findMany.load()
} }
} };
const {
data,
page,
totalPages,
loading,
load,
} = stategrafik.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true); setMounted(true);
stategrafik.findMany.load() load(page, 10, search)
}, []); }, [page, search]);
useEffect(() => { useEffect(() => {
if (stategrafik.findMany.data) { if (stategrafik.findMany.data) {
@@ -69,139 +71,149 @@ function ListGrafikBerdasarkanUsiaKerjaYangMenganggur({search}: {search: string}
{ name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' }, { name: 'usia46_keatas', value: totalUsia46_keatas, color: '#1094A8FF', key: 'usia46_keatas' },
]); ]);
} }
}, [stategrafik.findMany.data]) }, [stategrafik.findMany.data]);
const filteredData = (stategrafik.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
if (loading || !data) {
return ( return (
item.usia18_25.toString().toLowerCase().includes(keyword) || <Stack py={10}>
item.usia26_35.toString().toLowerCase().includes(keyword) || <Skeleton height={500} radius="md" />
item.usia36_45.toString().toLowerCase().includes(keyword) || </Stack>
item.usia46_keatas.toString().toLowerCase().includes(keyword)
); );
});
if (!stategrafik.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Box py={10}>
<Stack gap={"xs"}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Paper bg={colors['white-1']} p={"md"}> <Stack>
<JudulList {/* Header */}
title='List Pengangguran Berdasarkan Usia Kerja' <Flex justify="space-between" align="center" mb="md">
href='/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create' <Title order={4}>List Pengangguran Berdasarkan Usia Kerja</Title>
/> <Tooltip label="Tambah Data" withArrow>
<Table striped withTableBorder withRowBorders> <Button
<TableThead> leftSection={<IconPlus size={18} />}
<TableTr> color="blue"
<TableTh>Usia 18-25</TableTh> variant="light"
<TableTh>Usia 26-35</TableTh> onClick={() => router.push('/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/create')}
<TableTh>Usia 36-45</TableTh> >
<TableTh>Usia 46 +</TableTh> Tambah Baru
<TableTh>Edit</TableTh> </Button>
<TableTh>Delete</TableTh> </Tooltip>
</TableTr> </Flex>
</TableThead>
<TableTbody> {/* Table */}
{filteredData.length === 0 ? ( <Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover>
<TableThead>
<TableTr> <TableTr>
<TableTd colSpan={6}> <TableTh>Usia 18-25</TableTh>
<Text ta='center' c='dimmed'>Belum ada data grafik responden</Text> <TableTh>Usia 26-35</TableTh>
</TableTd> <TableTh>Usia 36-45</TableTh>
<TableTh>Usia 46 +</TableTh>
<TableTh>Edit</TableTh>
<TableTh>Delete</TableTh>
</TableTr> </TableTr>
) : ( </TableThead>
filteredData.map((item) => ( <TableTbody>
<TableTr key={item.id}> {filteredData.length > 0 ? (
<TableTd>{item.usia18_25}</TableTd> filteredData.map(item => (
<TableTd>{item.usia26_35}</TableTd> <TableTr key={item.id}>
<TableTd>{item.usia36_45}</TableTd> <TableTd>{item.usia18_25}</TableTd>
<TableTd>{item.usia46_keatas}</TableTd> <TableTd>{item.usia26_35}</TableTd>
<TableTd> <TableTd>{item.usia36_45}</TableTd>
<Button color='green' onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}> <TableTd>{item.usia46_keatas}</TableTd>
<IconEdit size={20} /> <TableTd>
</Button> <Button color="green" onClick={() => router.push(`/admin/ekonomi/jumlah-penduduk-usia-kerja-yang-menganggur/pengangguran_berdasarkan_usia/${item.id}`)}>
</TableTd> <IconEdit size={20} />
<TableTd> </Button>
<Button </TableTd>
color='red' <TableTd>
disabled={stategrafik.delete.loading} <Button color="red" disabled={stategrafik.delete.loading} onClick={() => { setSelectedId(item.id); setModalHapus(true); }}>
onClick={() => { <IconTrash size={20} />
setSelectedId(item.id) </Button>
setModalHapus(true) </TableTd>
}}> </TableTr>
<IconTrash size={20} /> ))
</Button> ) : (
<TableTr>
<TableTd colSpan={6}>
<Center py={20}>
<Text color="dimmed">Belum ada data grafik responden</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
)) )}
)} </TableTbody>
</TableTbody> </Table>
</Box>
</Table> </Stack>
</Paper> </Paper>
{/* Chart */} <Center>
<Box> <Pagination
<Paper bg={colors['white-1']} p={'md'}> value={page}
<Stack> onChange={(newPage) => {
<Title pb={10} order={3}>Grafik Pengangguran Berdasarkan Usia Kerja</Title> load(newPage, 10);
{mounted && donutData.length > 0 ? (<Box style={{ width: '100%', height: 'auto', minHeight: 200 }}> window.scrollTo({ top: 0, behavior: 'smooth' });
<PieChart }}
width={800} height={300} total={totalPages}
data={donutData} mt="md"
> mb="md"
<Pie color="blue"
dataKey="value" radius="md"
nameKey="name" />
data={donutData} </Center>
cx={500}
cy={150} {/* Chart */}
innerRadius={60} <Paper bg={colors['white-1']} p="md" mt="lg" withBorder radius="md">
outerRadius={115} <Stack>
label={true} <Title order={3} pb={10}>Grafik Pengangguran Berdasarkan Usia Kerja</Title>
> {mounted && donutData.length > 0 ? (
{donutData.map((entry, index) => ( <Box style={{ width: '100%', height: 'auto', minHeight: 200 }}>
<Cell key={`cell-${index}`} fill={entry.color} /> <PieChart width={800} height={300} data={donutData}>
))} <Pie dataKey="value" nameKey="name" data={donutData} cx={400} cy={150} innerRadius={60} outerRadius={115} label>
</Pie> {donutData.map((entry, index) => (
</PieChart> <Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
</PieChart>
<Stack mt="sm" gap="xs">
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={colors['blue-button']} w={20} h={20} /> <Box bg={colors['blue-button']} w={20} h={20} />
<Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text> <Text>Usia 18-25 : {donutData.find((entry) => entry.name === 'usia18_25')?.value}</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#10A85AFF'} w={20} h={20} /> <Box bg={'#10A85AFF'} w={20} h={20} />
<Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}</Text> <Text>Usia 26-35 : {donutData.find((entry) => entry.name === 'usia26_35')?.value}
</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#C07B13FF'} w={20} h={20} /> <Box bg={'#C07B13FF'} w={20} h={20} />
<Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}</Text> <Text>Usia 36-45 : {donutData.find((entry) => entry.name === 'usia36_45')?.value}
</Text>
</Flex> </Flex>
<Flex gap={"md"} align={"center"}> <Flex gap={"md"} align={"center"}>
<Box bg={'#1094A8FF'} w={20} h={20} /> <Box bg={'#1094A8FF'} w={20} h={20} />
<Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}</Text> <Text>Usia 46 + : {donutData.find((entry) => entry.name === 'usia46_keatas')?.value}
</Text>
</Flex> </Flex>
</Box> </Stack>
) : ( </Box>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> ) : (
)} <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
</Stack> )}
</Paper> </Stack>
</Box> </Paper>
</Stack>
{/* Modal Konfirmasi Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleDelete} onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?' text="Apakah anda yakin ingin menghapus grafik pengangguran berdasarkan usia kerja ini?"
/> />
</Box> </Box>
); );

View File

@@ -1,5 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
'use client' 'use client';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core'; import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core';
@@ -10,19 +10,12 @@ import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function EditDetailDataPengangguran() { function EditDetailDataPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
const [formData, setFormData] = useState<{ const [formData, setFormData] = useState({
month: string; month: '',
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
totalUnemployment: number;
percentageChange: number | null;
}>({
month: "",
year: new Date().getFullYear(), year: new Date().getFullYear(),
educatedUnemployment: 0, educatedUnemployment: 0,
uneducatedUnemployment: 0, uneducatedUnemployment: 0,
@@ -30,24 +23,12 @@ function EditDetailDataPengangguran() {
percentageChange: 0, percentageChange: 0,
}); });
// Update form data and recalculate totals // Hitung total & perubahan otomatis
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
setFormData({
...newData,
totalUnemployment: total,
percentageChange,
});
};
const calculateTotalAndChange = async () => { const calculateTotalAndChange = async () => {
const total = formData.educatedUnemployment + formData.uneducatedUnemployment; const total = formData.educatedUnemployment + formData.uneducatedUnemployment;
// Calculate percentage change based on previous month's data
let percentageChange = 0; let percentageChange = 0;
const monthOrder = ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]; const monthOrder = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'];
const currentMonthIndex = monthOrder.indexOf(formData.month); const currentMonthIndex = monthOrder.indexOf(formData.month);
if (currentMonthIndex !== -1) { if (currentMonthIndex !== -1) {
@@ -60,12 +41,7 @@ function EditDetailDataPengangguran() {
} }
const prevMonth = monthOrder[prevMonthIndex]; const prevMonth = monthOrder[prevMonthIndex];
const prevData = await stateDetail.findByMonthYear.load({ month: prevMonth, year: prevYear });
// Get previous month's data
const prevData = await stateDetail.findByMonthYear.load({
month: prevMonth,
year: prevYear,
});
if (prevData && prevData.totalUnemployment > 0) { if (prevData && prevData.totalUnemployment > 0) {
const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100; const change = ((total - prevData.totalUnemployment) / prevData.totalUnemployment) * 100;
@@ -76,6 +52,12 @@ function EditDetailDataPengangguran() {
return { total, percentageChange }; return { total, percentageChange };
}; };
const updateFormData = async (updates: Partial<typeof formData>) => {
const newData = { ...formData, ...updates };
const { total, percentageChange } = await calculateTotalAndChange();
setFormData({ ...newData, totalUnemployment: total, percentageChange });
};
useEffect(() => { useEffect(() => {
const loadDetail = async () => { const loadDetail = async () => {
const id = params?.id as string; const id = params?.id as string;
@@ -124,79 +106,69 @@ function EditDetailDataPengangguran() {
const handleSubmit = async () => { const handleSubmit = async () => {
const { total, percentageChange } = await calculateTotalAndChange(); const { total, percentageChange } = await calculateTotalAndChange();
try { try {
stateDetail.update.form = { stateDetail.update.form = { ...formData, totalUnemployment: total, percentageChange };
...formData,
totalUnemployment: total,
percentageChange,
};
const success = await stateDetail.update.submit(); const success = await stateDetail.update.submit();
if (success) { if (success) {
toast.success("Detail data pengangguran berhasil diperbarui!"); toast.success('Detail data pengangguran berhasil diperbarui!');
router.push("/admin/ekonomi/jumlah-pengangguran"); router.push('/admin/ekonomi/jumlah-pengangguran');
} }
} catch (error) { } catch (error) {
console.error("Error updating:", error); console.error('Error updating:', error);
toast.error("Terjadi kesalahan saat memperbarui data"); toast.error('Terjadi kesalahan saat memperbarui data');
} }
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> <Group mb="md">
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <IconArrowBack color={colors['blue-button']} size={24} />
</Button> </Button>
</Box> <Title order={4} ml="sm">
Edit Detail Data Pengangguran
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="lg" radius="md" shadow="sm" style={{ border: '1px solid #e0e0e0' }}>
<Title order={4}>Edit Detail Data Pengangguran</Title> <Stack gap="md">
<Stack gap="xs">
<Select <Select
label="Bulan" label="Bulan"
data={["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"]} data={['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']}
value={formData.month} value={formData.month}
onChange={async (val) => { onChange={(val) => updateFormData({ month: val || '' })}
await updateFormData({ month: val || "" });
}}
/> />
<NumberInput <NumberInput
label="Tahun" label="Tahun"
value={formData.year} value={formData.year}
onChange={async (val) => { onChange={(val) => updateFormData({ year: Number(val) })}
await updateFormData({ year: Number(val) });
}}
/> />
<TextInput <TextInput
label="Pengangguran Terdidik" label="Pengangguran Terdidik"
type="number" type="number"
value={formData.educatedUnemployment} value={formData.educatedUnemployment}
onChange={async (val) => { onChange={(val) => updateFormData({ educatedUnemployment: Number(val.currentTarget.value) || 0 })}
const value = Number(val.currentTarget.value) || 0;
await updateFormData({ educatedUnemployment: value });
}}
/> />
<TextInput <TextInput
label="Pengangguran Tidak Terdidik" label="Pengangguran Tidak Terdidik"
type="number" type="number"
value={formData.uneducatedUnemployment} value={formData.uneducatedUnemployment}
onChange={async (val) => { onChange={(val) => updateFormData({ uneducatedUnemployment: Number(val.currentTarget.value) || 0 })}
const value = Number(val.currentTarget.value) || 0;
await updateFormData({ uneducatedUnemployment: value });
}}
/> />
<Text fz="sm" fw={500}> <Text fz="sm" fw={500}>Total Otomatis: {formData.totalUnemployment}</Text>
Total Otomatis: {formData.totalUnemployment} <Text fz="sm" fw={500}>Perubahan Otomatis: {formData.percentageChange !== null ? `${formData.percentageChange}%` : '-'}</Text>
</Text>
<Text fz="sm" fw={500}> <Group justify="right">
Perubahan Otomatis:{" "} <Button
{formData.percentageChange !== null onClick={handleSubmit}
? `${formData.percentageChange}%` radius="md"
: '-'} size="md"
</Text> style={{
<Group> background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
<Button bg={colors['blue-button']} mt={10} onClick={handleSubmit}> color: '#fff',
Submit boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button> </Button>
</Group> </Group>
</Stack> </Stack>
@@ -206,3 +178,4 @@ function EditDetailDataPengangguran() {
} }
export default EditDetailDataPengangguran; export default EditDetailDataPengangguran;

View File

@@ -2,7 +2,7 @@
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text, Tooltip } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
@@ -11,91 +11,125 @@ import { useProxy } from 'valtio/utils';
function DetailJumlahPengangguran() { function DetailJumlahPengangguran() {
const router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
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 stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
useShallowEffect(() => { useShallowEffect(() => {
stateDetail.findUnique.load(params?.id as string) stateDetail.findUnique.load(params?.id as string);
}, [params?.id]) }, [params?.id]);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateDetail.delete.byId(selectedId) stateDetail.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/jumlah-pengangguran") router.push("/admin/ekonomi/jumlah-pengangguran");
} }
} };
if (!stateDetail.findUnique.data) { if (!stateDetail.findUnique.data) {
return ( return (
<Box> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Box> </Stack>
) );
} }
const data = stateDetail.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Data Pengangguran</Text> Kembali
<Paper bg={colors['BG-trans']} p={'md'}> </Button>
<Stack gap={"xs"}>
{/* Paper Detail */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Data Pengangguran
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"}>Pengangguran Terdidik</Text> <Text fw="bold">Pengangguran Terdidik</Text>
<Text>{stateDetail.findUnique.data?.educatedUnemployment}</Text> <Text c="dimmed">{data.educatedUnemployment || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Pengangguran Tidak Terdidik</Text> <Text fw="bold">Pengangguran Tidak Terdidik</Text>
<Text>{stateDetail.findUnique.data?.uneducatedUnemployment}</Text> <Text c="dimmed">{data.uneducatedUnemployment || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Perubahan</Text> <Text fw="bold">Perubahan</Text>
<Text> <Text c="dimmed">
{stateDetail.findUnique.data?.percentageChange !== null && {data.percentageChange !== null && data.percentageChange !== undefined
stateDetail.findUnique.data?.percentageChange !== undefined ? `${data.percentageChange}%`
? `${stateDetail.findUnique.data.percentageChange}%`
: 'Tidak ada data perubahan'} : 'Tidak ada data perubahan'}
</Text> </Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Tahun</Text> <Text fw="bold">Tahun</Text>
<Text>{stateDetail.findUnique.data?.year || ''}</Text> <Text c="dimmed">{data.year || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Bulan</Text> <Text fw="bold">Bulan</Text>
<Text>{stateDetail.findUnique.data?.month}</Text> <Text c="dimmed">{data.month || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Total Pengangguran</Text> <Text fw="bold">Total Pengangguran</Text>
<Text>{stateDetail.findUnique.data?.totalUnemployment}</Text> <Text c="dimmed">{data.totalUnemployment || '-'}</Text>
</Box> </Box>
<Box>
<Flex gap={"xs"}> {/* Tombol Edit & Hapus */}
<Flex gap="sm">
<Tooltip label="Hapus Data Pengangguran" withArrow position="top">
<Button <Button
onClick={() => { onClick={() => {
if (stateDetail.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateDetail.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={stateDetail.delete.loading || !stateDetail.findUnique.data} color="red"
color={"red"}> variant="light"
radius="md"
size="md"
>
<IconX size={20} /> <IconX size={20} />
</Button> </Button>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${stateDetail.findUnique.data?.id}/edit`)} color="green"> </Tooltip>
<Tooltip label="Edit Data Pengangguran" withArrow position="top">
<Button
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${data.id}/edit`)}
color="green"
variant="light"
radius="md"
size="md"
>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Flex>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
@@ -106,7 +140,7 @@ function DetailJumlahPengangguran() {
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus data ini?" text="Apakah Anda yakin ingin menghapus data ini?"
/> />
</Box> </Box>
); );

View File

@@ -3,14 +3,25 @@
'use client' 'use client'
import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '@/app/admin/(dashboard)/_state/ekonomi/jumlah-pengangguran';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title, Select, NumberInput } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
NumberInput,
Title,
Select,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
function CreateJumlahPengangguran() { function CreateJumlahPengangguran() {
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran) const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter(); const router = useRouter();
@@ -21,14 +32,14 @@ function CreateJumlahPengangguran() {
const resetForm = () => { const resetForm = () => {
stateDetail.create.form = { stateDetail.create.form = {
month: monthOptions[new Date().getMonth()], // Default to current month month: monthOptions[new Date().getMonth()], // default bulan sekarang
year: new Date().getFullYear(), // Default to current year year: new Date().getFullYear(), // default tahun sekarang
totalUnemployment: 0, totalUnemployment: 0,
educatedUnemployment: 0, educatedUnemployment: 0,
uneducatedUnemployment: 0, uneducatedUnemployment: 0,
percentageChange: 0, percentageChange: 0,
} };
} };
const calculateTotalAndChange = async () => { const calculateTotalAndChange = async () => {
const total = const total =
@@ -37,11 +48,8 @@ function CreateJumlahPengangguran() {
stateDetail.create.form.totalUnemployment = total; stateDetail.create.form.totalUnemployment = total;
// Ambil data bulan sebelumnya // hitung perubahan dibanding bulan sebelumnya
const monthOrder = [ const monthOrder = monthOptions;
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
];
const currentIndex = monthOrder.findIndex( const currentIndex = monthOrder.findIndex(
(m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase() (m) => m.toLowerCase() === stateDetail.create.form.month.toLowerCase()
); );
@@ -78,15 +86,34 @@ function CreateJumlahPengangguran() {
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> onClick={() => router.back()}
<Title order={4}>Tambah Data Pengangguran</Title> p="xs"
<Stack gap="xs" mt="md"> radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Data Pengangguran
</Title>
</Group>
{/* Form Card */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<Select <Select
label="Bulan" label="Bulan"
placeholder="Pilih bulan" placeholder="Pilih bulan"
@@ -98,6 +125,7 @@ function CreateJumlahPengangguran() {
}} }}
required required
/> />
<NumberInput <NumberInput
label="Tahun" label="Tahun"
value={stateDetail.create.form.year} value={stateDetail.create.form.year}
@@ -109,6 +137,7 @@ function CreateJumlahPengangguran() {
max={2100} max={2100}
required required
/> />
<NumberInput <NumberInput
label="Pengangguran Terdidik" label="Pengangguran Terdidik"
value={stateDetail.create.form.educatedUnemployment} value={stateDetail.create.form.educatedUnemployment}
@@ -119,6 +148,7 @@ function CreateJumlahPengangguran() {
min={0} min={0}
required required
/> />
<NumberInput <NumberInput
label="Pengangguran Tidak Terdidik" label="Pengangguran Tidak Terdidik"
value={stateDetail.create.form.uneducatedUnemployment} value={stateDetail.create.form.uneducatedUnemployment}
@@ -129,15 +159,36 @@ function CreateJumlahPengangguran() {
min={0} min={0}
required required
/> />
<Text fz="sm" fw={500}>
Total Otomatis: {stateDetail.create.form.totalUnemployment.toLocaleString()} <Box>
</Text> <Text fz="sm" fw={500} mb={4}>
<Text fz="sm" fw={500}> Total Otomatis:
Perubahan Otomatis: {stateDetail.create.form.percentageChange.toFixed(1)}% </Text>
</Text> <Text fz="sm" c="dimmed">
<Group mt="md"> {stateDetail.create.form.totalUnemployment.toLocaleString()}
</Text>
</Box>
<Box>
<Text fz="sm" fw={500} mb={4}>
Perubahan Otomatis:
</Text>
<Text fz="sm" c="dimmed">
{stateDetail.create.form.percentageChange.toFixed(1)}%
</Text>
</Box>
{/* Action Button */}
<Group justify="right" mt="md">
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
disabled={!stateDetail.create.form.month || !stateDetail.create.form.year} disabled={!stateDetail.create.form.month || !stateDetail.create.form.year}
> >
Simpan Simpan

View File

@@ -1,150 +1,181 @@
/* 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, Group, Pagination, Paper, Skeleton, Stack,
Table, TableTbody, TableTd, TableTh, TableThead, TableTr,
Text, Title, Tooltip
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { BarChart } from '@mantine/charts'; import { BarChart } from '@mantine/charts';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran'; import jumlahPengangguranState from '../../_state/ekonomi/jumlah-pengangguran';
function DetailDataPengangguran() { function DetailDataPengangguran() {
const [search, setSearch] = useState("") const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<Stack gap={"xs"}>
<HeaderSearch <HeaderSearch
title='Detail Data Pengangguran' title='Detail Data Pengangguran'
placeholder='pencarian' placeholder='Cari bulan atau tahun...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListDetailDataPengangguran search={search} /> <ListDetailDataPengangguran search={search} />
</Stack>
</Box> </Box>
); );
} }
function ListDetailDataPengangguran({search}: {search: string}) { function ListDetailDataPengangguran({ search }: { search: string }) {
const [chartData, setChartData] = useState<any[]>([]);
type DetailDataPengangguran = { const [mounted, setMounted] = useState(false);
id: string; const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran);
month: string;
year: number;
educatedUnemployment: number;
uneducatedUnemployment: number;
percentageChange: number;
totalUnemployment: number;
}
const [chartData, setChartData] = useState<DetailDataPengangguran[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const stateDetail = useProxy(jumlahPengangguranState.jumlahPengangguran)
const router = useRouter(); const router = useRouter();
const {
data,
page,
totalPages,
loading,
load,
} = stateDetail.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
stateDetail.findMany.load() load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
setMounted(true); if (data) {
if (stateDetail.findMany.data) { setChartData(
setChartData(stateDetail.findMany.data.map((item) => ({ data.map((item) => ({
id: item.id, id: item.id,
month: item.month, month: item.month,
year: item.year && typeof item.year === 'object' && 'getFullYear' in item.year ? (item.year as Date).getFullYear() : Number(item.year), year: Number(item.year),
educatedUnemployment: Number(item.educatedUnemployment), educatedUnemployment: Number(item.educatedUnemployment),
uneducatedUnemployment: Number(item.uneducatedUnemployment), uneducatedUnemployment: Number(item.uneducatedUnemployment),
percentageChange: Number(item.percentageChange), percentageChange: Number(item.percentageChange),
totalUnemployment: Number(item.totalUnemployment), totalUnemployment: Number(item.totalUnemployment),
}))); }))
);
} }
}, [stateDetail.findMany.data]); }, [data]);
const filteredData = (stateDetail.findMany.data || []).filter(item => { const filteredData = data || []
const keyword = search.toLowerCase();
// Loading state
if (loading || !data) {
return ( return (
item.month.toLowerCase().includes(keyword) || <Stack py="md">
item.year.toString().toLowerCase().includes(keyword) <Skeleton h={500} radius="md" />
</Stack>
); );
});
if (!stateDetail.findMany.data) {
return (
<Box>
<Skeleton h={500} />
</Box>
)
} }
return ( return (
<Box> <Stack py="md" gap="lg">
<Stack gap={"md"}> {/* Table Section */}
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Detail Data Pengangguran' <Title order={4}>Daftar Detail Data Pengangguran</Title>
href='/admin/ekonomi/jumlah-pengangguran/create' <Tooltip label="Tambah Data Baru" withArrow>
/> <Button
<Table striped withTableBorder withRowBorders> leftSection={<IconPlus size={18} />}
color="blue"
variant="light"
onClick={() => router.push('/admin/ekonomi/jumlah-pengangguran/create')}
>
Tambah Baru
</Button>
</Tooltip>
</Group>
<Box style={{ overflowX: 'auto' }}>
<Table highlightOnHover striped withTableBorder withRowBorders>
<TableThead> <TableThead>
<TableTr> <TableTr>
<TableTh>Bulan</TableTh> <TableTh style={{ width: '25%' }}>Bulan</TableTh>
<TableTh>Terdidik</TableTh> <TableTh style={{ width: '20%' }}>Terdidik</TableTh>
<TableTh>Tidak Terdidik</TableTh> <TableTh style={{ width: '20%' }}>Tidak Terdidik</TableTh>
<TableTh>Detail</TableTh> <TableTh style={{ width: '15%' }}>Aksi</TableTh>
</TableTr> </TableTr>
</TableThead> </TableThead>
<TableTbody> <TableTbody>
{filteredData.map((item) => ( {filteredData.length > 0 ? (
<TableTr key={item.id}> filteredData.map((item) => (
<TableTd>{item.month}</TableTd> <TableTr key={item.id}>
<TableTd>{item.educatedUnemployment}</TableTd> <TableTd>{item.month} {item.year}</TableTd>
<TableTd>{item.uneducatedUnemployment}</TableTd> <TableTd>{item.educatedUnemployment}</TableTd>
<TableTd> <TableTd>{item.uneducatedUnemployment}</TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}> <TableTd>
<IconDeviceImac size={20} /> <Button
</Button> variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/jumlah-pengangguran/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text c="dimmed">Tidak ada data yang cocok</Text>
</Center>
</TableTd> </TableTd>
</TableTr> </TableTr>
))} )}
</TableTbody> </TableTbody>
</Table> </Table>
</Paper> </Box>
</Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */} {/* Chart Section */}
{!mounted && !chartData ? ( <Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Title order={4} mb="md">
<Paper bg={colors['white-1']} p={'md'}> Data Pengangguran Terdidik & Tidak Terdidik
<Title pb={10} order={3}>Data Pengangguran Terdidik dan Tidak Terdidik</Title> </Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> {mounted && chartData.length > 0 ? (
</Paper> <Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box> </Box>
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 550, minHeight: 300 }}> <Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Data Pengangguran Terdidik dan Tidak Terdidik</Title>
{mounted && chartData.length > 0 && (
<Box w={{ base: '100%', md: '70%' }}>
<BarChart
h={450}
data={chartData}
dataKey="month"
series={[
{ name: 'educatedUnemployment', color: 'red.6', label: 'Terdidik' },
{ name: 'uneducatedUnemployment', color: 'orange.6', label: 'Tidak Terdidik' },
]}
/>
</Box>
)}
</Paper>
</Box>
)} )}
</Stack> </Paper>
</Box> </Stack>
); );
} }

View File

@@ -2,6 +2,8 @@
'use client' 'use client'
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor'; import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
import SelectIconProgramEdit from '@/app/admin/(dashboard)/_com/selectIconEdit';
import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { import {
@@ -13,11 +15,13 @@ import {
Text, Text,
TextInput, TextInput,
Title, Title,
Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter, useParams } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
function EditProgramKemiskinan() { function EditProgramKemiskinan() {
const router = useRouter(); const router = useRouter();
@@ -33,46 +37,74 @@ function EditProgramKemiskinan() {
stateProgram.update.form = { stateProgram.update.form = {
nama: data.nama || '', nama: data.nama || '',
deskripsi: data.deskripsi || '', deskripsi: data.deskripsi || '',
ikonUrl: data.ikonUrl || '', icon: data.icon || '',
statistik: { statistik: {
tahun: data.statistik?.tahun?.toString() || '', tahun: data.statistik?.tahun?.toString() || '',
jumlah: data.statistik?.jumlah?.toString() || '', jumlah: data.statistik?.jumlah?.toString() || '',
}, },
}; };
} }
}).catch((err) => {
console.error("Error load data:", err);
toast.error("Gagal mengambil data program");
}); });
} }
}, [id]); }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
stateProgram.update.id = id; try {
await stateProgram.update.update(); stateProgram.update.id = id;
router.push('/admin/ekonomi/program-kemiskinan'); await stateProgram.update.update();
toast.success("Program berhasil diperbarui!");
router.push('/admin/ekonomi/program-kemiskinan');
} catch (error) {
console.error("Error update program:", error);
toast.error("Terjadi kesalahan saat memperbarui program");
}
}; };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol kembali */}
<Button onClick={() => router.back()} variant="subtle" color="blue"> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p="md"> p="xs"
<Stack gap="xs"> radius="md"
<Title order={4}>Edit Program Kemiskinan</Title> >
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Program Kemiskinan
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
value={stateProgram.update.form.nama} value={stateProgram.update.form.nama}
onChange={(e) => { onChange={(e) => {
stateProgram.update.form.nama = e.target.value; stateProgram.update.form.nama = e.target.value;
}} }}
label={<Text fw="bold" fz="md">Judul Program</Text>} label={<Text fw="bold" fz="sm">Judul Program</Text>}
placeholder="Masukkan judul program" placeholder="Masukkan judul program"
required
/> />
<Box> <Box>
<Text fw="bold" fz="md">Deskripsi</Text> <Text fw="bold" fz="sm" mb={6}>
Deskripsi
</Text>
<EditEditor <EditEditor
value={stateProgram.update.form.deskripsi} value={stateProgram.update.form.deskripsi}
onChange={(val) => { onChange={(val) => {
@@ -81,40 +113,58 @@ function EditProgramKemiskinan() {
/> />
</Box> </Box>
<TextInput <Box>
value={stateProgram.update.form.ikonUrl} <Text fw="bold" fz="sm" mb={6}>
onChange={(e) => { Ikon Program Kreatif Desa
stateProgram.update.form.ikonUrl = e.target.value; </Text>
}} <SelectIconProgramEdit
label={<Text fw="bold" fz="md">Ikon URL</Text>} value={stateProgram.update.form.icon as IconKey}
placeholder="Masukkan ikon url" onChange={(value) => {
/> stateProgram.update.form.icon = value;
}}
/>
</Box>
<Text fw="bold" fz="md">Statistik Jumlah Masyarakat Miskin</Text> <Box>
<Text fw="bold" fz="sm" mb={6}>
Statistik Jumlah Masyarakat Miskin
</Text>
<TextInput
type="number"
value={stateProgram.update.form.statistik.jumlah}
onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value;
}}
label="Jumlah Masyarakat Miskin"
placeholder="Masukkan jumlah masyarakat miskin"
required
/>
<TextInput <TextInput
type="number" type="number"
value={stateProgram.update.form.statistik.jumlah} value={stateProgram.update.form.statistik.tahun}
onChange={(e) => { onChange={(e) => {
stateProgram.update.form.statistik.jumlah = e.target.value; stateProgram.update.form.statistik.tahun = e.target.value;
}} }}
label={<Text fw="bold" fz="md">Jumlah Masyarakat Miskin</Text>} label="Tahun"
placeholder="Masukkan jumlah masyarakat miskin" placeholder="Masukkan tahun"
/> required
mt="sm"
/>
</Box>
<TextInput <Group justify="right" mt="md">
type="number" <Button
value={stateProgram.update.form.statistik.tahun} onClick={handleSubmit}
onChange={(e) => { radius="md"
stateProgram.update.form.statistik.tahun = e.target.value; size="md"
}} style={{
label={<Text fw="bold" fz="md">Tahun</Text>} background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
placeholder="Masukkan tahun" color: '#fff',
/> boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
<Group> >
<Button bg={colors['blue-button']} onClick={handleSubmit}> Simpan
Submit
</Button> </Button>
</Group> </Group>
</Stack> </Stack>

View File

@@ -1,115 +1,169 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import { IconKey, IconMapper } from '../../../_com/iconMap';
function DetailProgramKemiskinan() { function DetailProgramKemiskinan() {
const programState = useProxy(programKemiskinanState) const programState = useProxy(programKemiskinanState);
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 router = useRouter(); const router = useRouter();
const params = useParams() const params = useParams();
useShallowEffect(() => { useShallowEffect(() => {
programState.findUnique.load(params?.id as string) programState.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
programState.delete.delete(selectedId) programState.delete.delete(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/program-kemiskinan") router.push("/admin/ekonomi/program-kemiskinan");
} }
} };
if (!programState.findUnique.data) { if (!programState.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={40} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = programState.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol Kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Program Kemiskinan</Text> Kembali
<Paper bg={colors['BG-trans']} p={'md'}> </Button>
<Stack gap={"xs"}>
{/* Card utama */}
<Paper
withBorder
w={{ base: "100%", md: "60%" }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Program Kemiskinan
</Text>
{/* Detail Content */}
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fw={"bold"}>Judul Program</Text> <Text fz="lg" fw="bold">Judul Program</Text>
<Text fz={"md"}>{programState.findUnique.data?.nama}</Text> <Text fz="md" c="dimmed">{data.nama || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Deskripsi Singkat</Text> <Text fz="lg" fw="bold">Deskripsi Singkat</Text>
<Text fz={"md"} dangerouslySetInnerHTML={{ __html: programState.findUnique.data?.deskripsi }}></Text> <Text
fz="md"
c="dimmed"
dangerouslySetInnerHTML={{ __html: data.deskripsi || '-' }}
/>
</Box> </Box>
<Box> <Box>
<Text fw={"bold"}>Ikon URL</Text> <Text fz="lg" fw="bold">Ikon Program</Text>
<Text fz={"md"}>{programState.findUnique.data?.ikonUrl}</Text> {data.icon ? (
<IconMapper
name={data.icon as IconKey}
size={32}
color={colors['blue-button']}
/>
) : (
<Text fz="sm" c="dimmed">Tidak ada ikon</Text>
)}
</Box> </Box>
<Text fw={"bold"}>Statistik Jumlah Masyarakat Miskin</Text>
<Box> <Box>
<Text fw={"bold"}>Jumlah Masyarakat Miskin</Text> <Text fz="lg" fw="bold">Statistik Jumlah Masyarakat Miskin</Text>
<Text fz={"md"}>{programState.findUnique.data?.statistik?.jumlah}</Text> <Stack gap="xs" pl="sm">
<Box>
<Text fw="bold">Jumlah</Text>
<Text fz="md" c="dimmed">{data.statistik?.jumlah || '-'}</Text>
</Box>
<Box>
<Text fw="bold">Tahun</Text>
<Text fz="md" c="dimmed">{data.statistik?.tahun || '-'}</Text>
</Box>
</Stack>
</Box> </Box>
<Box>
<Text fw={"bold"}>Tahun</Text> {/* Action Buttons */}
<Text fz={"md"}>{programState.findUnique.data?.statistik?.tahun}</Text> <Group gap="sm" mt="sm">
</Box> <Tooltip label="Hapus Program" withArrow position="top">
<Flex gap={"xs"} mt={10}> <Button
<Button color="red"
onClick={() => { onClick={() => {
if (programState.findUnique.data) { setSelectedId(data.id);
setSelectedId(programState.findUnique.data.id);
setModalHapus(true); setModalHapus(true);
} }}
}} variant="light"
disabled={programState.delete.loading || !programState.findUnique.data} radius="md"
color={"red"} size="md"
> disabled={programState.delete.loading}
<IconX size={20} /> >
</Button> <IconTrash size={20} />
<Button </Button>
onClick={() => { </Tooltip>
if (programState.findUnique.data) {
router.push(`/admin/ekonomi/program-kemiskinan/${programState.findUnique.data.id}/edit`); <Tooltip label="Edit Program" withArrow position="top">
} <Button
}} color="green"
disabled={!programState.findUnique.data} onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${data.id}/edit`)}
color={"green"} variant="light"
> radius="md"
<IconEdit size={20} /> size="md"
</Button> >
</Flex> <IconEdit size={20} />
</Button>
</Tooltip>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}
onConfirm={handleHapus} onConfirm={handleHapus}
text="Apakah anda yakin ingin menghapus program kemiskinan ini?" text="Apakah Anda yakin ingin menghapus program kemiskinan ini?"
/> />
</Box> </Box>
); );
} }
export default DetailProgramKemiskinan; export default DetailProgramKemiskinan;

View File

@@ -1,34 +1,50 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* 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, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan'; import programKemiskinanState from '../../../_state/ekonomi/program-kemiskinan';
import CreateEditor from '../../../_com/createEditor'; import CreateEditor from '../../../_com/createEditor';
import SelectIconProgram from '../../../_com/selectIcon';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-toastify';
function CreateProgramKemiskinan() { function CreateProgramKemiskinan() {
const programState = useProxy(programKemiskinanState) const programState = useProxy(programKemiskinanState);
const router = useRouter(); const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const resetForm = () => { const resetForm = () => {
programState.create.form = { programState.create.form = {
nama: "", nama: '',
deskripsi: "", deskripsi: '',
ikonUrl: "", icon: '',
statistik: { statistik: {
tahun: "", tahun: '',
jumlah: "", jumlah: '',
} },
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!programState.create.form.nama || !programState.create.form.deskripsi) {
return toast.warn('Judul dan deskripsi wajib diisi');
}
const id = await programState.create.create(); const id = await programState.create.create();
if (id) { if (id) {
const idStr = String(id); const idStr = String(id);
@@ -36,68 +52,116 @@ function CreateProgramKemiskinan() {
if (programState.findUnique.data) { if (programState.findUnique.data) {
setLineChart([programState.findUnique.data]); setLineChart([programState.findUnique.data]);
} }
toast.success('Program berhasil ditambahkan');
} else {
toast.error('Gagal menambahkan program, coba lagi');
} }
resetForm()
router.push("/admin/ekonomi/program-kemiskinan") resetForm();
} router.push('/admin/ekonomi/program-kemiskinan');
};
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan tombol back */}
<Button onClick={() => router.back()} variant='subtle' color={'blue'}> <Group mb="md">
<IconArrowBack color={colors['blue-button']} size={25} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={24} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Tambah Program Kemiskinan
</Title>
</Group>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> <Paper
<Stack gap={"xs"}> w={{ base: '100%', md: '50%' }}
<Title order={4}>Create Program Kemiskinan</Title> bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
{/* Judul Program */}
<TextInput <TextInput
label="Judul Program"
placeholder="Masukkan judul program"
value={programState.create.form.nama} value={programState.create.form.nama}
onChange={(val) => { onChange={(val) => (programState.create.form.nama = val.target.value)}
programState.create.form.nama = val.target.value; required
}}
label={<Text fw={"bold"} fz={"md"}>Judul Program</Text>}
placeholder='Masukkan judul program'
/> />
<Box>
<Text fw={"bold"} fz={"md"}>Deskripsi</Text> {/* Ikon Program */}
<CreateEditor <Box>
value={programState.create.form.deskripsi} <Text fw="bold" fz="sm" mb={6}>
onChange={(val) => { Ikon Program Kreatif Desa
programState.create.form.deskripsi = val; </Text>
}} <SelectIconProgram
/> onChange={(value) => (programState.create.form.icon = value)}
</Box> />
<TextInput </Box>
value={programState.create.form.ikonUrl}
onChange={(val) => { {/* Deskripsi */}
programState.create.form.ikonUrl = val.target.value; <Box>
}} <Text fw="bold" fz="sm" mb={6}>
label={<Text fw={"bold"} fz={"md"}>Ikon URL</Text>} Deskripsi
placeholder='Masukkan ikon url' </Text>
/> <CreateEditor
<Text fw={"bold"} fz={"md"}>Statistik Jumlah Masyarakat Miskin</Text> value={programState.create.form.deskripsi}
<TextInput onChange={(val) => {
type='number' programState.create.form.deskripsi = val;
value={programState.create.form.statistik.jumlah} }}
onChange={(val) => { />
programState.create.form.statistik.jumlah = val.target.value; </Box>
}}
label={<Text fw={"bold"} fz={"md"}>Jumlah Masyarakat Miskin</Text>} {/* Statistik */}
placeholder='Masukkan jumlah masyarakat miskin' <Text fw="bold" fz="sm">
/> Statistik Jumlah Masyarakat Miskin
<TextInput </Text>
type='number' <Group grow>
value={programState.create.form.statistik.tahun} <TextInput
onChange={(val) => { type="number"
programState.create.form.statistik.tahun = val.target.value; value={programState.create.form.statistik.jumlah}
}} onChange={(val) =>
label={<Text fw={"bold"} fz={"md"}>Tahun</Text>} (programState.create.form.statistik.jumlah = val.target.value)
placeholder='Masukkan tahun' }
/> label="Jumlah"
<Group> placeholder="Masukkan jumlah masyarakat miskin"
<Button bg={colors['blue-button']} onClick={handleSubmit}>Submit</Button> required
/>
<TextInput
type="number"
value={programState.create.form.statistik.tahun}
onChange={(val) =>
(programState.create.form.statistik.tahun = val.target.value)
}
label="Tahun"
placeholder="Masukkan tahun"
required
/>
</Group>
{/* Tombol Submit */}
<Group justify="right" mt="sm">
<Button
onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper> </Paper>

View File

@@ -1,16 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
/* eslint-disable @typescript-eslint/no-explicit-any */
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import { Box, Button, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title, Tooltip } from '@mantine/core';
import { IconDeviceImac, IconSearch } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation';
import { useProxy } from 'valtio/utils';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CartesianGrid, Legend, Line, LineChart, Tooltip, XAxis, YAxis } from 'recharts'; import { CartesianGrid, Legend, Line, LineChart, Tooltip as RechartTooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header';
import programKemiskinanState from '../../_state/ekonomi/program-kemiskinan';
function ProgramKemiskinan() { function ProgramKemiskinan() {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@@ -18,7 +17,7 @@ function ProgramKemiskinan() {
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Program Kemiskinan' title='Program Kemiskinan'
placeholder='pencarian' placeholder='Cari judul program atau deskripsi...'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -29,23 +28,17 @@ function ProgramKemiskinan() {
} }
function ListProgramKemiskinan({ search }: { search: string }) { function ListProgramKemiskinan({ search }: { search: string }) {
const programState = useProxy(programKemiskinanState) const programState = useProxy(programKemiskinanState);
const router = useRouter(); const router = useRouter();
const [lineChart, setLineChart] = useState<any[]>([]); const [lineChart, setLineChart] = useState<any[]>([]);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const { const { data, page, totalPages, loading, load } = programState.findMany;
data,
page,
totalPages,
loading,
load,
} = programState.findMany;
useShallowEffect(() => { useShallowEffect(() => {
setMounted(true) setMounted(true);
load(page, 10, search) load(page, 10, search);
}, []) }, [page, search]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -55,103 +48,118 @@ function ListProgramKemiskinan({ search }: { search: string }) {
tahun: item.statistik?.tahun, tahun: item.statistik?.tahun,
jumlah: Number(item.statistik?.jumlah) jumlah: Number(item.statistik?.jumlah)
})) }))
.sort((a, b) => (a.tahun || 0) - (b.tahun || 0)); // opsional, urutkan tahun .sort((a, b) => (a.tahun || 0) - (b.tahun || 0));
setLineChart(chartData); setLineChart(chartData);
} }
}, [data]) }, [data]);
const filteredData = data || [] const filteredData = data || [];
if (loading || !data) { if (loading || !data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={600} radius="md" />
</Stack> </Stack>
) );
} }
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'}> <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<JudulList <Group justify="space-between" mb="md">
title='List Program Kemiskinan' <Title order={4}>Daftar Program Kemiskinan</Title>
href='/admin/ekonomi/program-kemiskinan/create' <Tooltip label="Tambah Program Kemiskinan" withArrow>
/> <Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/program-kemiskinan/create')}>
<Table striped withTableBorder withRowBorders> Tambah Baru
<TableThead> </Button>
<TableTr> </Tooltip>
<TableTh>Judul Program</TableTh> </Group>
<TableTh>Deskripsi Singkat</TableTh> <Box style={{ overflowX: 'auto' }}>
<TableTh>Jumlah Masyarakat Miskin</TableTh> <Table highlightOnHover>
<TableTh>Detail</TableTh> <TableThead>
</TableTr> <TableTr>
</TableThead> <TableTh style={{ width: '30%' }}>Judul Program</TableTh>
<TableTbody> <TableTh style={{ width: '40%' }}>Deskripsi Singkat</TableTh>
{filteredData.map((item) => ( <TableTh style={{ width: '20%' }}>Jumlah Masyarakat Miskin</TableTh>
<TableTr key={item.id}> <TableTh style={{ width: '10%' }}>Aksi</TableTh>
<TableTd>{item.nama}</TableTd>
<TableTd>
<Text fz={'sm'} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah}</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr> </TableTr>
))} </TableThead>
</TableTbody> <TableTbody>
</Table> {filteredData.length > 0 ? (
filteredData.map(item => (
<TableTr key={item.id}>
<TableTd>
<Text fw={500} truncate lineClamp={1}>{item.nama}</Text>
</TableTd>
<TableTd>
<Text fz="sm" truncate lineClamp={2} dangerouslySetInnerHTML={{ __html: item.deskripsi }} />
</TableTd>
<TableTd>{item.statistik?.jumlah || '-'}</TableTd>
<TableTd>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/program-kemiskinan/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={5}>Detail</Text>
</Button>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={4}>
<Center py={20}>
<Text color="dimmed">Tidak ada data program kemiskinan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box>
</Paper> </Paper>
{/* Pagination */}
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */} {/* Chart */}
<Box> <Box py={10}>
<Paper bg={colors['white-1']} p={'md'} > <Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
<Stack> <Title pb={10} order={3}>Grafik Berdasarkan Responden</Title>
<Box > {mounted && lineChart.length > 0 ? (
<Title pb={10} order={3}>Grafik Berdasarkan Responden</Title> <Box style={{ width: '100%', overflowX: 'auto' }}>
{mounted && lineChart.length > 0 ? (<Box style={{ width: '100%', height: 'auto', }}> <LineChart width={820} height={300} data={lineChart}>
<Box w={"100%"} style={{ overflowX: 'auto' }}> <CartesianGrid strokeDasharray="3 3" />
<LineChart <XAxis dataKey="tahun" />
width={820} <YAxis />
height={300} <RechartTooltip
data={lineChart} formatter={(value: any, name: string) => [`${value} orang`, name]}
> labelFormatter={(label: any) => `Tahun: ${label}`}
<CartesianGrid strokeDasharray="3 3" /> />
<XAxis dataKey="tahun" /> <Legend />
<YAxis /> <Line type="monotone" dataKey="jumlah" name="Jumlah per Tahun" stroke={colors['blue-button']} />
<Tooltip </LineChart>
formatter={(value: any, name: string) => [`${value} orang`, name]}
labelFormatter={(label: any) => `Tahun: ${label}`}
/>
<Legend />
<Line
type="monotone"
dataKey="jumlah"
name="Jumlah per Tahun"
stroke={colors['blue-button']}
/>
</LineChart>
</Box>
</Box>
) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Box> </Box>
</Stack> ) : (
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
)}
</Paper> </Paper>
<Center>
<Pagination
value={page}
onChange={(newPage) => load(newPage)}
total={totalPages}
my={"md"}
/>
</Center>
</Box> </Box>
</Box> </Box>
); );

View File

@@ -3,87 +3,138 @@
import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa'; import grafikSektorUnggulan from '@/app/admin/(dashboard)/_state/ekonomi/sektor-unggulan-desa';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { toast } from 'react-toastify';
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
function EditSektorUnggulanDesa() { function EditSektorUnggulanDesa() {
const router = useRouter() const router = useRouter();
const params = useParams() as { id: string } const params = useParams() as { id: string };
const stateGrafik = useProxy(grafikSektorUnggulan) const stateGrafik = useProxy(grafikSektorUnggulan);
const id = params.id const id = params.id;
// Load data saat komponen mount // Load data saat komponen mount
useEffect(() => { useEffect(() => {
if (id) { if (id) {
stateGrafik.findUnique.load(id).then(() => { stateGrafik.findUnique.load(id).then(() => {
const data = stateGrafik.findUnique.data const data = stateGrafik.findUnique.data;
if (data) { if (data) {
stateGrafik.update.form = { stateGrafik.update.form = {
name: data.name || '', name: data.name || '',
description: data.description || '', description: data.description || '',
value: data.value || 0, value: data.value || 0,
} };
} }
}) }).catch((err) => {
console.error('Error load sektor unggulan:', err);
toast.error('Gagal mengambil data sektor unggulan');
});
} }
}, [id]) }, [id]);
const handleSubmit = async () => { const handleSubmit = async () => {
// Set the ID before submitting try {
stateGrafik.update.id = id; stateGrafik.update.id = id;
await stateGrafik.update.submit(); await stateGrafik.update.submit();
router.push('/admin/ekonomi/sektor-unggulan-desa') toast.success('Sektor unggulan berhasil diperbarui!');
} router.push('/admin/ekonomi/sektor-unggulan-desa');
return ( } catch (error) {
<Box> console.error('Error update sektor unggulan:', error);
<Box mb={10}> toast.error('Terjadi kesalahan saat memperbarui sektor unggulan');
<Button variant="subtle" onClick={() => router.back()}> }
<IconArrowBack size={20} /> };
</Button>
</Box> return (
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Stack> <Group mb="md">
<Title order={3}>Edit Sektor Unggulan Desa</Title> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
<Button
variant="subtle"
onClick={() => router.back()}
p="xs"
radius="md"
>
<IconArrowBack color={colors['blue-button']} size={22} />
</Button>
</Tooltip>
<Title order={4} ml="sm" c="dark">
Edit Sektor Unggulan Desa
</Title>
</Group>
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput <TextInput
label="Nama Sektor Unggulan" label="Nama Sektor Unggulan"
placeholder="masukkan nama sektor unggulan" placeholder="Masukkan nama sektor unggulan"
value={stateGrafik.update.form.name} value={stateGrafik.update.form.name}
onChange={(val) => { onChange={(val) => {
stateGrafik.update.form.name = val.currentTarget.value; stateGrafik.update.form.name = val.currentTarget.value;
}} }}
required
/> />
<TextInput <Box>
label="Deskripsi Sektor Unggulan" <Text fz="sm" fw="bold">
placeholder="masukkan deskripsi sektor unggulan" Konten
value={stateGrafik.update.form.description} </Text>
onChange={(val) => { <EditEditor
stateGrafik.update.form.description = val.currentTarget.value; value={stateGrafik.update.form.description}
}} onChange={(htmlContent) => {
/> stateGrafik.update.form.description = htmlContent;
}}
/>
</Box>
<TextInput <TextInput
label="Jumlah" label="Jumlah"
type="number" type="number"
placeholder="masukkan jumlah" placeholder="Masukkan jumlah"
value={stateGrafik.update.form.value} value={stateGrafik.update.form.value}
onChange={(val) => { onChange={(val) => {
stateGrafik.update.form.value = Number(val.currentTarget.value); stateGrafik.update.form.value = Number(val.currentTarget.value);
}} }}
required
/> />
<Button
mt={10} <Group justify="right">
bg={colors['blue-button']} <Button
onClick={handleSubmit} onClick={handleSubmit}
> radius="md"
Simpan size="md"
</Button> style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}}
>
Simpan
</Button>
</Group>
</Stack> </Stack>
</Paper> </Paper>
</Box> </Box>
) );
} }
export default EditSektorUnggulanDesa; export default EditSektorUnggulanDesa;

View File

@@ -1,97 +1,134 @@
'use client' 'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Skeleton,
Stack,
Text,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa'; import grafikSektorUnggulan from '../../../_state/ekonomi/sektor-unggulan-desa';
function DetailSektorUnggulanDesa() { function DetailSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan) const stateGrafik = useProxy(grafikSektorUnggulan);
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 params = useParams() const params = useParams();
const router = useRouter(); const router = useRouter();
useShallowEffect(() => { useShallowEffect(() => {
stateGrafik.findUnique.load(params?.id as string) stateGrafik.findUnique.load(params?.id as string);
}, []) }, []);
const handleHapus = () => { const handleHapus = () => {
if (selectedId) { if (selectedId) {
stateGrafik.delete.byId(selectedId) stateGrafik.delete.byId(selectedId);
setModalHapus(false) setModalHapus(false);
setSelectedId(null) setSelectedId(null);
router.push("/admin/ekonomi/sektor-unggulan-desa") router.push('/admin/ekonomi/sektor-unggulan-desa');
} }
} };
if (!stateGrafik.findUnique.data) { if (!stateGrafik.findUnique.data) {
return ( return (
<Stack py={10}> <Stack py={10}>
<Skeleton h={500} /> <Skeleton height={500} radius="md" />
</Stack> </Stack>
) );
} }
const data = stateGrafik.findUnique.data;
return ( return (
<Box> <Box py={10}>
<Box mb={10}> {/* Tombol kembali */}
<Button variant="subtle" onClick={() => router.back()}> <Button
<IconArrowBack color={colors['blue-button']} size={25} /> variant="subtle"
</Button> onClick={() => router.back()}
</Box> leftSection={<IconArrowBack size={24} color={colors['blue-button']} />}
<Paper w={{ base: "100%", md: "50%" }} bg={colors['white-1']} p={'md'}> mb={15}
<Stack> >
<Text fz={"xl"} fw={"bold"}>Detail Sektor Unggulan Desa</Text> Kembali
<Paper bg={colors['BG-trans']} p={'md'}> </Button>
<Stack gap={"xs"}>
<Paper
withBorder
w={{ base: '100%', md: '60%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
>
<Stack gap="md">
<Text fz="2xl" fw="bold" c={colors['blue-button']}>
Detail Sektor Unggulan Desa
</Text>
<Paper bg="#ECEEF8" p="md" radius="md" shadow="xs">
<Stack gap="sm">
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Nama Sektor Unggulan</Text> <Text fz="lg" fw="bold">Nama Sektor Unggulan</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.name}</Text> <Text fz="md" c="dimmed">{data.name || '-'}</Text>
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Deskripsi Sektor Unggulan</Text> <Text fz="lg" fw="bold">Deskripsi</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.description}</Text> <Text ta={"justify"} fz="md" c="dimmed" dangerouslySetInnerHTML={{ __html: data.description || '-' }} />
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Jumlah Sektor Unggulan</Text> <Text fz="lg" fw="bold">Jumlah</Text>
<Text fz={"lg"}>{stateGrafik.findUnique.data?.value}</Text> <Text fz="md" c="dimmed">{data.value ?? '-'}</Text>
</Box> </Box>
<Box>
<Flex gap={"xs"}> {/* Tombol Aksi */}
<Group gap="sm">
<Tooltip label="Hapus Sektor Unggulan" withArrow position="top">
<Button <Button
color="red"
onClick={() => { onClick={() => {
if (stateGrafik.findUnique.data) { setSelectedId(data.id);
setSelectedId(stateGrafik.findUnique.data.id); setModalHapus(true);
setModalHapus(true);
}
}} }}
disabled={!stateGrafik.findUnique.data} variant="light"
color="red"> radius="md"
<IconX size={20} /> size="md"
>
<IconTrash size={20} />
</Button> </Button>
</Tooltip>
<Tooltip label="Edit Sektor Unggulan" withArrow position="top">
<Button <Button
onClick={() => { color="green"
if (stateGrafik.findUnique.data) { onClick={() =>
router.push(`/admin/ekonomi/sektor-unggulan-desa/${stateGrafik.findUnique.data.id}/edit`); router.push(
} `/admin/ekonomi/sektor-unggulan-desa/${data.id}/edit`
}} )
disabled={!stateGrafik.findUnique.data} }
color="green"> variant="light"
radius="md"
size="md"
>
<IconEdit size={20} /> <IconEdit size={20} />
</Button> </Button>
</Flex> </Tooltip>
</Box> </Group>
</Stack> </Stack>
</Paper> </Paper>
</Stack> </Stack>
</Paper> </Paper>
{/* Modal Hapus */} {/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus <ModalKonfirmasiHapus
opened={modalHapus} opened={modalHapus}
onClose={() => setModalHapus(false)} onClose={() => setModalHapus(false)}

View File

@@ -1,8 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* 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, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core'; import {
Box,
Button,
Group,
Paper,
Stack,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react'; import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
@@ -13,15 +23,15 @@ import CreateEditor from '../../../_com/createEditor';
function CreateSektorUnggulanDesa() { function CreateSektorUnggulanDesa() {
const stateGrafik = useProxy(grafikSektorUnggulan); const stateGrafik = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<any[]>([]); const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter() const router = useRouter();
const resetForm = () => { const resetForm = () => {
stateGrafik.create.form = { stateGrafik.create.form = {
name: "", name: '',
description: "", description: '',
value: 0, value: 0,
} };
} };
const handleSubmit = async () => { const handleSubmit = async () => {
const id = await stateGrafik.create.create(); const id = await stateGrafik.create.create();
@@ -33,58 +43,87 @@ function CreateSektorUnggulanDesa() {
} }
} }
resetForm(); resetForm();
router.push("/admin/ekonomi/sektor-unggulan-desa"); router.push('/admin/ekonomi/sektor-unggulan-desa');
} };
return ( return (
<Box> <Box px={{ base: 'sm', md: 'lg' }} py="md">
<Box mb={10}> {/* Header dengan back button */}
<Button variant="subtle" onClick={() => router.back()}> <Group mb="md">
<IconArrowBack size={20} /> <Tooltip label="Kembali ke halaman sebelumnya" withArrow>
</Button> <Button
</Box> variant="subtle"
<Box> onClick={() => router.back()}
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}> p="xs"
<Title order={4}>Tambah Grafik Hasil Kepuasan Masyarakat</Title> radius="md"
<Stack gap={"xs"}> >
<TextInput <IconArrowBack color={colors['blue-button']} size={24} />
label="Nama Sektor Unggulan" </Button>
type="text" </Tooltip>
value={stateGrafik.create.form.name} <Title order={4} ml="sm" c="dark">
placeholder="Masukkan nama sektor unggulan" Tambah Sektor Unggulan Desa
</Title>
</Group>
{/* Form */}
<Paper
w={{ base: '100%', md: '50%' }}
bg={colors['white-1']}
p="lg"
radius="md"
shadow="sm"
style={{ border: '1px solid #e0e0e0' }}
>
<Stack gap="md">
<TextInput
label="Nama Sektor Unggulan"
placeholder="Masukkan nama sektor unggulan"
value={stateGrafik.create.form.name}
onChange={(e) => {
stateGrafik.create.form.name = e.currentTarget.value;
}}
required
/>
<Box>
<Text fw="bold" fz="sm" mb={6}>
Deskripsi Sektor Unggulan
</Text>
<CreateEditor
value={stateGrafik.create.form.description}
onChange={(val) => { onChange={(val) => {
stateGrafik.create.form.name = val.currentTarget.value; stateGrafik.create.form.description = val;
}} }}
/> />
<Box> </Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Sektor Ungggulan</Text>
<CreateEditor <TextInput
value={stateGrafik.create.form.description} label="Jumlah"
onChange={(val) => { type="number"
stateGrafik.create.form.description = val; placeholder="Masukkan jumlah"
}} value={stateGrafik.create.form.value}
/> onChange={(e) => {
</Box> stateGrafik.create.form.value = Number(e.currentTarget.value);
<TextInput }}
label="Jumlah" required
type="number" />
value={stateGrafik.create.form.value}
placeholder="Masukkan jumlah" <Group justify="right">
onChange={(val) => { <Button
stateGrafik.create.form.value = Number(val.currentTarget.value); onClick={handleSubmit}
radius="md"
size="md"
style={{
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
color: '#fff',
boxShadow: '0 4px 15px rgba(79, 172, 254, 0.4)',
}} }}
/> >
<Group> Simpan
<Button </Button>
bg={colors['blue-button']} </Group>
mt={10} </Stack>
onClick={handleSubmit} </Paper>
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box> </Box>
); );
} }

View File

@@ -1,23 +1,40 @@
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core'; import {
import { IconDeviceImac, IconSearch } from '@tabler/icons-react'; Box,
import HeaderSearch from '../../_com/header'; Button,
import JudulList from '../../_com/judulList'; Center,
Group,
Pagination,
Paper,
Skeleton,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconDeviceImac, IconPlus, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa'; import { Bar, BarChart, Legend, ResponsiveContainer, Tooltip as ReTooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import HeaderSearch from '../../_com/header';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts'; import grafikSektorUnggulan from '../../_state/ekonomi/sektor-unggulan-desa';
function SektorUnggulanDesa() { function SektorUnggulanDesa() {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Sektor Unggulan Desa' title="Sektor Unggulan Desa"
placeholder='pencarian' placeholder="Cari nama sektor atau nilai..."
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search} value={search}
onChange={(e) => setSearch(e.currentTarget.value)} onChange={(e) => setSearch(e.currentTarget.value)}
@@ -30,96 +47,155 @@ function SektorUnggulanDesa() {
function ListSektorUnggulanDesa({ search }: { search: string }) { function ListSektorUnggulanDesa({ search }: { search: string }) {
const router = useRouter(); const router = useRouter();
const state = useProxy(grafikSektorUnggulan); const state = useProxy(grafikSektorUnggulan);
const [chartData, setChartData] = useState<{ id: string; name: string; description: string | null; value: number | null }[]>([]); const [chartData, setChartData] = useState<
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready { id: string; name: string; description: string | null; value: number | null }[]
const isTablet = useMediaQuery('(max-width: 1024px)') >([]);
const isMobile = useMediaQuery('(max-width: 768px)')
const filteredData = (state.findMany.data || []).filter(item => { const {
const keyword = search.toLowerCase(); data,
return ( page,
item.name.toLowerCase().includes(keyword) || totalPages,
(item.value?.toString() || '').toLowerCase().includes(keyword) loading,
); load,
}); } = state.findMany;
useShallowEffect(() => {
setMounted(true)
state.findMany.load()
}, [])
useEffect(() => { useEffect(() => {
setMounted(true);
if (state.findMany.data) { if (state.findMany.data) {
setChartData(state.findMany.data.map((item) => ({ setChartData(
id: item.id, state.findMany.data.map((item) => ({
name: item.name, id: item.id,
description: item.description, name: item.name,
value: Number(item.value), description: item.description,
}))); value: Number(item.value),
}))
);
} }
}, [state.findMany.data]); }, [state.findMany.data]);
return ( useShallowEffect(() => {
<Box> load(page, 10, search)
<Stack gap={"xs"}> }, [page, search])
<Paper bg={colors['white-1']} p={'md'}>
<JudulList
title='List Sektor Unggulan Desa'
href='/admin/ekonomi/sektor-unggulan-desa/create'
/>
<Table striped withTableBorder withRowBorders>
<TableThead>
<TableTr>
<TableTh>Nama Sektor Unggulan</TableTh>
<TableTh>Deskripsi Sektor Unggulan</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item) => (
<TableTr key={item.id}>
<TableTd>{item.name}</TableTd>
<TableTd>
<Text truncate={"end"} fz={'sm'} lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '' }}></Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}>
<IconDeviceImac size={20} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Paper>
{/* Chart */} const filteredData = data || []
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> if (loading || !data) {
<Paper bg={colors['white-1']} p={'md'}> return (
<Title pb={10} order={3}>Grafik Hasil Kepuasan Masyarakat</Title> <Stack py={10}>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text> <Skeleton height={600} radius="md" />
</Paper> </Stack>
</Box> );
}
return (
<Stack gap="md" py="md">
{/* List Table */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Group justify="space-between" mb="md">
<Title order={4}>List Sektor Unggulan Desa</Title>
<Tooltip label="Tambah Sektor Unggulan Desa" withArrow>
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/ekonomi/sektor-unggulan-desa/create')}>
Tambah Baru
</Button>
</Tooltip>
</Group>
{loading ? (
<Skeleton height={300} radius="md" />
) : ( ) : (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}> <Box style={{ overflowX: 'auto' }}>
<Paper bg={colors['white-1']} p={'md'}> <Table highlightOnHover>
<Title pb={10} order={4}>Grafik Sektor Unggulan Desa</Title> <TableThead>
{mounted && chartData.length > 0 && ( <TableTr>
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} > <TableTh style={{ width: '30%' }}>Nama Sektor</TableTh>
<XAxis dataKey="name" /> <TableTh style={{ width: '40%' }}>Deskripsi</TableTh>
<YAxis /> <TableTh style={{ width: '15%' }}>Detail</TableTh>
<Tooltip /> </TableTr>
<Legend /> </TableThead>
<Bar dataKey="value" fill={colors['blue-button']} name="Jumlah" /> <TableTbody>
</BarChart> {filteredData.length > 0 ? (
)} filteredData.map((item) => (
</Paper> <TableTr key={item.id}>
<TableTd>
<Box w={200}>
<Text fw={500} truncate="end" lineClamp={1}>
{item.name}
</Text>
</Box>
</TableTd>
<TableTd>
<Box w={200}>
<Text truncate="end" fz="sm" c="dimmed" lineClamp={1} dangerouslySetInnerHTML={{ __html: item.description || '-' }} />
</Box>
</TableTd>
<TableTd>
<Tooltip label="Lihat detail sektor" withArrow>
<Button
variant="light"
color="blue"
onClick={() => router.push(`/admin/ekonomi/sektor-unggulan-desa/${item.id}`)}
>
<IconDeviceImac size={20} />
<Text ml={6}>Detail</Text>
</Button>
</Tooltip>
</TableTd>
</TableTr>
))
) : (
<TableTr>
<TableTd colSpan={3}>
<Center py={20}>
<Text color="dimmed">Tidak ada data sektor unggulan yang cocok</Text>
</Center>
</TableTd>
</TableTr>
)}
</TableTbody>
</Table>
</Box> </Box>
)} )}
</Stack> </Paper>
</Box>
<Center>
<Pagination
value={page}
onChange={(newPage) => {
load(newPage, 10);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
total={totalPages}
mt="md"
mb="md"
color="blue"
radius="md"
/>
</Center>
{/* Chart */}
<Paper withBorder bg={colors['white-1']} p="lg" shadow="md" radius="md">
<Title order={4} pb="sm">
Grafik Sektor Unggulan Desa
</Title>
{loading ? (
<Skeleton height={350} radius="md" />
) : chartData.length > 0 ? (
<Box style={{ width: '100%', height: 400 }}>
<ResponsiveContainer>
<BarChart data={chartData}>
<XAxis dataKey="name" />
<YAxis />
<ReTooltip />
<Legend />
<Bar dataKey="value" fill={colors['blue-button']} name="Jumlah" />
</BarChart>
</ResponsiveContainer>
</Box>
) : (
<Center py={50}>
<Text c="dimmed">Belum ada data untuk ditampilkan dalam grafik</Text>
</Center>
)}
</Paper>
</Stack>
); );
} }

View File

@@ -10,6 +10,7 @@ import { useEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import SelectIconProgramEdit from '../../../../_com/selectIconEdit'; import SelectIconProgramEdit from '../../../../_com/selectIconEdit';
import { IconKey } from '@/app/admin/(dashboard)/_com/iconMap';
interface FormProgramKreatif { interface FormProgramKreatif {
name: string; name: string;
@@ -18,8 +19,6 @@ interface FormProgramKreatif {
icon: string; icon: string;
} }
type IconKey = 'ekowisata' | 'kompetisi' | 'wisata' | 'ekonomi' | 'sampah';
function EditProgramKreatifDesa() { function EditProgramKreatifDesa() {
const stateProgramKreatif = useProxy(programKreatifState) const stateProgramKreatif = useProxy(programKreatifState)

View File

@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client' 'use client'
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core'; import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks'; import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconChartLine, IconChristmasTreeFilled, IconClipboard, IconEdit, IconHomeEco, IconLeaf, IconRecycle, IconScale, IconShieldFilled, IconTent, IconTrash, IconTrendingUp, IconTrophy, IconTruck, IconX } from '@tabler/icons-react'; import { IconArrowBack, IconEdit, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import React, { useState } from 'react'; import { useState } from 'react';
import { useProxy } from 'valtio/utils'; import { useProxy } from 'valtio/utils';
import programKreatifState from '../../../_state/inovasi/program-kreatif'; import { IconKey, IconMapper } from '../../../_com/iconMap';
import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
import programKreatifState from '../../../_state/inovasi/program-kreatif';
// import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus'; // import { ModalKonfirmasiHapus } from '../../../_com/modalKonfirmasiHapus';
@@ -19,22 +19,6 @@ function DetailProgramKreatifDesa() {
const params = useParams() const params = useParams()
const [selectedId, setSelectedId] = useState<string | null>(null) const [selectedId, setSelectedId] = useState<string | null>(null)
const iconMap: Record<string, React.FC<any>> = {
ekowisata: IconLeaf,
kompetisi: IconTrophy,
wisata: IconTent,
ekonomi: IconChartLine,
sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
};
useShallowEffect(() => { useShallowEffect(() => {
stateProgramKreatif.findUnique.load(params?.id as string) stateProgramKreatif.findUnique.load(params?.id as string)
}, [params?.id]) }, [params?.id])
@@ -75,10 +59,12 @@ function DetailProgramKreatifDesa() {
</Box> </Box>
<Box> <Box>
<Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text> <Text fz={"lg"} fw={"bold"}>Ikon Program Kreatif Desa</Text>
{iconMap[stateProgramKreatif.findUnique.data?.icon] && ( {stateProgramKreatif.findUnique.data?.icon && (
<Box title={stateProgramKreatif.findUnique.data?.icon}> <IconMapper
{React.createElement(iconMap[stateProgramKreatif.findUnique.data?.icon], { size: 24 })} name={stateProgramKreatif.findUnique.data?.icon as IconKey}
</Box> size={32}
color={colors['blue-button']}
/>
)} )}
</Box> </Box>
<Box> <Box>

View File

@@ -4,7 +4,7 @@
import React from 'react'; import React from 'react';
import colors from '@/con/colors'; import colors from '@/con/colors';
import { Box, Button, Center, Pagination, 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 { IconDeviceImac, IconSearch } from '@tabler/icons-react'; import { IconCash, IconChristmasTreeFilled, IconClipboard, IconDeviceImac, IconDroplet, IconHome, IconHomeEco, IconHospital, IconScale, IconSchool, IconSearch, IconShieldFilled, IconShoppingCart, IconTrash, IconTree, IconTrendingUp, IconTruck } from '@tabler/icons-react';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@@ -60,6 +60,21 @@ function ListProgramKreatifDesa({ search }: { search: string }) {
wisata: IconTent, wisata: IconTent,
ekonomi: IconChartLine, ekonomi: IconChartLine,
sampah: IconRecycle, sampah: IconRecycle,
truck: IconTruck,
scale: IconScale,
clipboard: IconClipboard,
trash: IconTrash,
lingkunganSehat: IconHomeEco,
sumberOksigen: IconChristmasTreeFilled,
ekonomiBerkelanjutan: IconTrendingUp,
mencegahBencana: IconShieldFilled,
rumah: IconHome,
pohon: IconTree,
air: IconDroplet,
bantuan: IconCash,
pelatihan: IconSchool,
subsidi: IconShoppingCart,
layananKesehatan: IconHospital
}; };
if (loading || !data) { if (loading || !data) {

View File

@@ -1,8 +1,49 @@
/* 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 demografiPekerjaanFindMany(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;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ pekerjaan: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.dataDemografiPekerjaan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.dataDemografiPekerjaan.count({
where,
}),
]);
export default async function demografiPekerjaanFindMany() {
const res = await prisma.dataDemografiPekerjaan.findMany();
return { return {
data: res success: true,
} message: "Success fetch demografi pekerjaan with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch demografi pekerjaan with pagination",
data: null,
};
}
} }

View File

@@ -1,8 +1,54 @@
/* 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 grafikJumlahPendudukMiskinFindMany(
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;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
const yearSearch = Number(search);
where.OR = [
// kalau search bisa diparse jadi number → cocokin year
...(isNaN(yearSearch) ? [] : [{ year: yearSearch }]),
];
}
try {
const [data, total] = await Promise.all([
prisma.grafikJumlahPendudukMiskin.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.grafikJumlahPendudukMiskin.count({
where,
}),
]);
export default async function grafikJumlahPendudukMiskinFindMany() {
const res = await prisma.grafikJumlahPendudukMiskin.findMany();
return { return {
data: res, success: true,
message: "Success fetch grafik jumlah penduduk miskin with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
}; };
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch grafik jumlah penduduk miskin with pagination",
data: null,
};
}
} }

View File

@@ -12,27 +12,33 @@ export default async function detailDataPengangguranFindMany(context: Context) {
// Tambahkan pencarian (jika ada) // Tambahkan pencarian (jika ada)
if (search) { if (search) {
const yearSearch = Number(search);
where.OR = [ where.OR = [
{ year: { contains: search, mode: "insensitive" } }, // kalau search bisa diparse jadi number → cocokin year
...(isNaN(yearSearch) ? [] : [{ year: yearSearch }]),
// selalu cocokin month pakai contains
{ month: { contains: search, mode: "insensitive" } }, { month: { contains: search, mode: "insensitive" } },
]; ];
} }
try { try {
const monthOrder = ["Jan","Feb","Mar","Apr","Mei","Jun","Jul","Agu","Sep","Okt","Nov","Des"];
const [data, total] = await Promise.all([ const [data, total] = await Promise.all([
prisma.detailDataPengangguran.findMany({ prisma.detailDataPengangguran.findMany({
where, where,
skip, skip,
take: limit, take: limit,
orderBy: [{ year: "desc" }, { month: "asc" }], orderBy: { year: "asc" }, // urut tahun dulu
}), }),
prisma.detailDataPengangguran.count({ prisma.detailDataPengangguran.count({
where, where,
}), }),
]); ]);
data.sort((a, b) => monthOrder.indexOf(a.month) - monthOrder.indexOf(b.month));
return { return {
success: true, success: true,
message: "Success fetch apb desa with pagination", message: "Success fetch detail data pengangguran with pagination",
data, data,
page, page,
totalPages: Math.ceil(total / limit), totalPages: Math.ceil(total / limit),

View File

@@ -4,7 +4,7 @@ import { Context } from "elysia";
type FormCreate = { type FormCreate = {
nama: string; nama: string;
deskripsi: string; deskripsi: string;
ikonUrl?: string; // optional karena boleh null icon: string;
statistik?: { statistik?: {
tahun: number; tahun: number;
jumlah: number; jumlah: number;
@@ -18,7 +18,7 @@ export default async function programKemiskinanCreate(context: Context) {
data: { data: {
nama: body.nama, nama: body.nama,
deskripsi: body.deskripsi, deskripsi: body.deskripsi,
ikonUrl: body.ikonUrl, icon: body.icon,
statistik: body.statistik statistik: body.statistik
? { ? {
create: { create: {

View File

@@ -12,7 +12,7 @@ const ProgramKemiskinan = new Elysia({
body: t.Object({ body: t.Object({
nama: t.String(), nama: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
ikonUrl: t.String(), icon: t.String(),
statistik: t.Object({ statistik: t.Object({
tahun: t.String(), tahun: t.String(),
jumlah: t.String(), jumlah: t.String(),
@@ -32,7 +32,7 @@ const ProgramKemiskinan = new Elysia({
body: t.Object({ body: t.Object({
nama: t.String(), nama: t.String(),
deskripsi: t.String(), deskripsi: t.String(),
ikonUrl: t.String(), icon: t.String(),
statistik: t.Object({ statistik: t.Object({
tahun: t.String(), tahun: t.String(),
jumlah: t.String(), jumlah: t.String(),

View File

@@ -5,6 +5,7 @@ type FormUpdate = {
nama: string; nama: string;
deskripsi: string; deskripsi: string;
ikonUrl?: string; ikonUrl?: string;
icon: string;
statistik?: { statistik?: {
tahun: number; tahun: number;
jumlah: number; jumlah: number;
@@ -51,7 +52,7 @@ export default async function programKemiskinanUpdate(context: Context) {
data: { data: {
nama: body.nama, nama: body.nama,
deskripsi: body.deskripsi, deskripsi: body.deskripsi,
ikonUrl: body.ikonUrl, icon: body.icon,
statistik: { statistik: {
connect: { id: statistikUpdate.id }, // konek ke statistik baru atau yang diperbarui connect: { id: statistikUpdate.id }, // konek ke statistik baru atau yang diperbarui
}, },

View File

@@ -1,8 +1,50 @@
/* 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 sektorUnggulanDesaFindMany(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;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ name: { contains: search, mode: "insensitive" } },
{ description: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.sektorUnggulanDesa.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.sektorUnggulanDesa.count({
where,
}),
]);
export default async function sektorUnggulanDesaFindMany() {
const res = await prisma.sektorUnggulanDesa.findMany();
return { return {
data: res, success: true,
message: "Success fetch sektor unggulan desa with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
}; };
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch sektor unggulan desa with pagination",
data: null,
};
}
} }

View File

@@ -1,8 +1,56 @@
/* 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 grafikMenganggurBerdasarkanUsiaFindMany(
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;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ usia18_25: { contains: search, mode: "insensitive" } },
{ usia26_35: { contains: search, mode: "insensitive" } },
{ usia36_45: { contains: search, mode: "insensitive" } },
{ usia46_keatas: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.grafikMenganggurBerdasarkanUsia.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "desc" },
}),
prisma.grafikMenganggurBerdasarkanUsia.count({
where,
}),
]);
export default async function grafikMenganggurBerdasarkanUsiaFindMany() {
const res = await prisma.grafikMenganggurBerdasarkanUsia.findMany();
return { return {
data: res success: true,
} message:
"Success fetch grafik menganggur berdasarkan usia with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message:
"Failed to fetch grafik menganggur berdasarkan usia with pagination",
data: null,
};
}
} }

View File

@@ -1,8 +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 grafikPengangguranBerdasarkanPendidikanFindMany(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;
const where: any = { isActive: true };
// Tambahkan pencarian (jika ada)
if (search) {
where.OR = [
{ SD: { contains: search, mode: "insensitive" } },
{ SMP: { contains: search, mode: "insensitive" } },
{ SMA: { contains: search, mode: "insensitive" } },
{ D3: { contains: search, mode: "insensitive" } },
{ S1: { contains: search, mode: "insensitive" } },
];
}
try {
const [data, total] = await Promise.all([
prisma.grafikMenganggurBerdasarkanPendidikan.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: "asc" },
}),
prisma.grafikMenganggurBerdasarkanPendidikan.count({
where,
}),
]);
export default async function grafikMenganggurBerdasarkanPendidikanFindMany() {
const res = await prisma.grafikMenganggurBerdasarkanPendidikan.findMany();
return { return {
data: res success: true,
} message: "Success fetch grafik menganggur berdasarkan pendidikan with pagination",
data,
page,
totalPages: Math.ceil(total / limit),
total,
};
} catch (e) {
console.error(e);
return {
success: false,
message: "Failed to fetch grafik menganggur berdasarkan pendidikan with pagination",
data: null,
};
}
} }

View File

@@ -41,37 +41,39 @@ function Page() {
<Box px={{ base: "md", md: 100 }}> <Box px={{ base: "md", md: 100 }}>
<Stack gap={'lg'} justify='center'> <Stack gap={'lg'} justify='center'>
<Paper p={'xl'}> <Paper p={'xl'}>
<Text fw={'bold'} fz={'h4'}>Statistik Demografi Pekerjaan Di Desa Darmasaba</Text> <Box style={{overflowX: 'scroll'}}>
<BarChart <Text pb={5} fw={'bold'} fz={'h4'}>Statistik Demografi Pekerjaan Di Desa Darmasaba</Text>
type='stacked' <BarChart
p={10} type='stacked'
mb={50} p={10}
h={400} mb={50}
w={150} h={400}
data={data.map((item) => ({ w={150}
id: item.id, data={data.map((item) => ({
Pekerjaan: item.pekerjaan, id: item.id,
laki: item.lakiLaki, Pekerjaan: item.pekerjaan,
perempuan: item.perempuan, laki: item.lakiLaki,
}))} perempuan: item.perempuan,
dataKey="Pekerjaan" }))}
series={[ dataKey="Pekerjaan"
{ name: 'laki', color: '#5082EE' }, series={[
{ name: 'perempuan', color: '#6EDF9C' }, { name: 'laki', color: '#5082EE' },
]} { name: 'perempuan', color: '#6EDF9C' },
tickLine="y" ]}
xAxisProps={{ tickLine="y"
angle: -45, // Rotate labels by -45 degrees xAxisProps={{
textAnchor: 'end', // Anchor text to the end for better alignment angle: -45, // Rotate labels by -45 degrees
height: 100, // Increase height for rotated labels textAnchor: 'end', // Anchor text to the end for better alignment
interval: 0, // Show all labels height: 100, // Increase height for rotated labels
style: { interval: 0, // Show all labels
fontSize: '12px', // Adjust font size if needed style: {
overflow: 'visible', fontSize: '12px', // Adjust font size if needed
whiteSpace: 'nowrap' overflow: 'visible',
} whiteSpace: 'nowrap'
}} }
/> }}
/>
</Box>
<Flex pb={30} justify={'center'} gap={'xl'} align={'center'}> <Flex pb={30} justify={'center'} gap={'xl'} align={'center'}>
<Box> <Box>
<Flex gap={{ base: 0, md: 5 }} align={'center'}> <Flex gap={{ base: 0, md: 5 }} align={'center'}>

View File

@@ -1,18 +1,19 @@
'use client' 'use client'
import colors from '@/con/colors';
import { Stack, Box, Text, SimpleGrid, Paper, Skeleton, Center, Pagination, Grid, GridCol, TextInput } from '@mantine/core';
import React, { useState } from 'react';
import BackButton from '../../desa/layanan/_com/BackButto';
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 programKemiskinanState from '@/app/admin/(dashboard)/_state/ekonomi/program-kemiskinan';
import colors from '@/con/colors';
import { Box, Center, Grid, GridCol, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core';
import { useDebouncedValue, useShallowEffect } from '@mantine/hooks'; import { useDebouncedValue, useShallowEffect } from '@mantine/hooks';
import { IconSearch } from '@tabler/icons-react'; import { IconSearch } from '@tabler/icons-react';
import { useState } from 'react';
import { CartesianGrid, Line, LineChart as RechartsLineChart, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import BackButton from '../../desa/layanan/_com/BackButto';
interface StatistikData { interface StatistikData {
id: string; id: string;
tahun: number; tahun: number;
jumlah: number; jumlah: number;
icon: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
@@ -21,7 +22,7 @@ interface ProgramKemiskinanData {
id: string; id: string;
nama: string; nama: string;
deskripsi: string; deskripsi: string;
ikonUrl: string | null; icon: string;
statistik: StatistikData | null; statistik: StatistikData | null;
isActive: boolean; isActive: boolean;
statistikId: string | null; statistikId: string | null;
@@ -34,6 +35,20 @@ function Page() {
const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay const [debouncedSearch] = useDebouncedValue(search, 500); // 500ms delay
const state = useProxy(programKemiskinanState) const state = useProxy(programKemiskinanState)
// 🔧 Get valid statistics data with proper type checking
const statistikData = state.findMany.data
.filter((item): item is ProgramKemiskinanData & { statistik: StatistikData } => {
return !!item?.statistik &&
item.statistik.tahun !== undefined &&
item.statistik.jumlah !== undefined;
})
.map(item => ({
tahun: Number(item.statistik.tahun) || 0, // Ensure tahun is a number
jumlah: Number(item.statistik.jumlah) || 0, // Ensure jumlah is a number
}))
.sort((a, b) => a.tahun - b.tahun)
.filter(item => !isNaN(item.tahun) && !isNaN(item.jumlah)); // Remove any invalid entries
const { const {
data, data,
page, page,
@@ -88,9 +103,9 @@ function Page() {
md: 2 md: 2
}} }}
> >
{state.findMany.data.map((v, k) => { {state.findMany.data.map(v => {
return ( return (
<Paper p={'xl'} key={k}> <Paper p={'xl'} key={v.id}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text> <Text fz={'h3'} fw={'bold'} c={colors['blue-button']}>{v.nama}</Text>
<Text fz={'lg'} c={'black'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text> <Text fz={'lg'} c={'black'} dangerouslySetInnerHTML={{ __html: v.deskripsi }}></Text>
</Paper> </Paper>
@@ -100,7 +115,10 @@ function Page() {
<Center my={10}> <Center my={10}>
<Pagination <Pagination
value={page} value={page}
onChange={(newPage) => load(newPage)} onChange={(newPage) => {
load(newPage)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
total={totalPages} total={totalPages}
my={"md"} my={"md"}
/> />
@@ -108,44 +126,48 @@ function Page() {
<Paper p={'xl'}> <Paper p={'xl'}>
<Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text> <Text fz={'h3'} fw={'bold'} c={colors['blue-button']} mb="md">Statistik Kemiskinan Masyarakat</Text>
<Box style={{ width: '100%', height: 'auto' }}> <Box style={{ width: '100%', height: 'auto' }}>
{data.length > 0 && data[0]?.statistik ? ( {statistikData.length > 0 ? (
<Box w="100%" style={{ overflowX: 'auto' }}> <Box w="100%" style={{ overflowX: 'auto' }}>
<Center> <Center>
<RechartsLineChart <RechartsLineChart
width={800} width={Math.min(800, window.innerWidth - 100)}
height={300} height={400}
data={state.findMany.data data={statistikData}
.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 }} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
> >
<CartesianGrid strokeDasharray="3 3" /> <CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="tahun" /> <XAxis
<YAxis /> dataKey="tahun"
<Tooltip label={{ value: 'Tahun', position: 'insideBottomRight', offset: -5 }}
formatter={(value: number, name: string) => [`${value} orang`, name]} />
labelFormatter={(label: number) => `Tahun: ${label}`} <YAxis
label={{ value: 'Jumlah', angle: -90, position: 'insideLeft' }}
domain={[0, 'auto']}
/>
<Tooltip
formatter={(value) => [`${value} orang`, 'Jumlah']}
labelFormatter={(label) => `Tahun ${label}`}
/> />
<Legend />
<Line <Line
type="monotone" type="monotone"
dataKey="jumlah" dataKey="jumlah"
name="Jumlah Masyarakat Miskin" name="Jumlah"
stroke={colors['blue-button']} stroke="#228be6"
activeDot={{ r: 8 }} strokeWidth={2}
dot={{ r: 4 }}
activeDot={{ r: 6 }}
/> />
</RechartsLineChart> </RechartsLineChart>
</Center> </Center>
</Box> </Box>
) : ( ) : (
<Text c="dimmed">Belum ada data statistik yang tersedia</Text> <Box p="md" ta="center" bg="gray.0" style={{ borderRadius: '8px' }}>
<Text c="dimmed">
{state.findMany.loading
? 'Memuat data statistik...'
: 'Belum ada data statistik yang tersedia atau data tidak valid'}
</Text>
</Box>
)} )}
</Box> </Box>
</Paper> </Paper>