UI & API Menu Pendidikan, Submenu Data Pendidikan

This commit is contained in:
2025-07-29 11:05:01 +08:00
parent e2e1672c80
commit 50e8999205
14 changed files with 941 additions and 244 deletions

View File

@@ -1842,7 +1842,7 @@ model Pengajar {
// ========================================= PROGRAM PENDIDIKAN ANAK ========================================= // // ========================================= PROGRAM PENDIDIKAN ANAK ========================================= //
model TujuanProgram { model TujuanProgram {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1852,7 +1852,7 @@ model TujuanProgram {
model ProgramUnggulan { model ProgramUnggulan {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1863,7 +1863,7 @@ model ProgramUnggulan {
// ========================================= BIMBINGAN BELAJAR DESA ========================================= // // ========================================= BIMBINGAN BELAJAR DESA ========================================= //
model TujuanBimbinganBelajarDesa { model TujuanBimbinganBelajarDesa {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1873,7 +1873,7 @@ model TujuanBimbinganBelajarDesa {
model LokasiJadwalBimbinganBelajarDesa { model LokasiJadwalBimbinganBelajarDesa {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1883,7 +1883,7 @@ model LokasiJadwalBimbinganBelajarDesa {
model FasilitasBimbinganBelajarDesa { model FasilitasBimbinganBelajarDesa {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1894,7 +1894,7 @@ model FasilitasBimbinganBelajarDesa {
// ========================================= PENDIDIKAN NON FORMAL ========================================= // // ========================================= PENDIDIKAN NON FORMAL ========================================= //
model TujuanPendidikanNonFormal { model TujuanPendidikanNonFormal {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1904,7 +1904,7 @@ model TujuanPendidikanNonFormal {
model TempatKegiatan { model TempatKegiatan {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -1914,10 +1914,21 @@ model TempatKegiatan {
model JenisProgramYangDiselenggarakan { model JenisProgramYangDiselenggarakan {
id String @id @default(cuid()) id String @id @default(cuid())
judul String judul String
deskripsi String @db.Text deskripsi String @db.Text
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime @default(now()) deletedAt DateTime @default(now())
isActive Boolean @default(true) isActive Boolean @default(true)
} }
// ========================================= DATA PENDIDIKAN ========================================= //
model DataPendidikan {
id String @id @default(cuid())
name String
jumlah String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime @default(now())
isActive Boolean @default(true)
}

View File

@@ -5,176 +5,190 @@ import { proxy } from "valtio";
import { z } from "zod"; import { z } from "zod";
const templateJumlahPendudukMiskin = z.object({ const templateJumlahPendudukMiskin = z.object({
year: z.number().min(1, "Data tahun harus diisi"), year: z.number().min(1, "Data tahun harus diisi"),
totalPoorPopulation: z.number().min(1, "Data total penduduk miskin harus diisi"), totalPoorPopulation: z
.number()
.min(1, "Data total penduduk miskin harus diisi"),
}); });
type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{ type JumlahPendudukMiskin = Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { select: {
id: true; id: true;
year: true; year: true;
totalPoorPopulation: true; totalPoorPopulation: true;
}; };
}>; }>;
const defaultForm: Omit<JumlahPendudukMiskin, 'id'> & { id?: string } = { const defaultForm: Omit<JumlahPendudukMiskin, "id"> & { id?: string } = {
year: 0, year: 0,
totalPoorPopulation: 0, totalPoorPopulation: 0,
}; };
const jumlahPendudukMiskin = proxy({ const jumlahPendudukMiskin = proxy({
create: { create: {
form: defaultForm, form: defaultForm,
loading: false, loading: false,
async create() { async create() {
const cek = templateJumlahPendudukMiskin.safeParse( const cek = templateJumlahPendudukMiskin.safeParse(
jumlahPendudukMiskin.create.form jumlahPendudukMiskin.create.form
); );
if (!cek.success) { if (!cek.success) {
const err = `[${cek.error.issues const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`) .map((v) => `${v.path.join(".")}`)
.join("\n")}] required`; .join("\n")}] required`;
return toast.error(err); return toast.error(err);
}
try {
jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true; };
}> | null,
async load(id: string) {
try {
const res = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`
);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: {...defaultForm},
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues.map((v) => (v.path as string[]).join(".")).join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data grafik jumlah penduduk miskin:", error);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(`/api/ekonomi/jumlahpendudukmiskin/del/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Grafik jumlah penduduk miskin berhasil dihapus");
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus grafik jumlah penduduk miskin");
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error("Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin");
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
} }
}) try {
export default jumlahPendudukMiskin jumlahPendudukMiskin.create.loading = true;
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"create"
].post(jumlahPendudukMiskin.create.form);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
jumlahPendudukMiskin.create.form = {
year: 0,
totalPoorPopulation: 0,
};
jumlahPendudukMiskin.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
jumlahPendudukMiskin.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.ekonomi.jumlahpendudukmiskin[
"find-many"
].get();
if (res.status === 200) {
jumlahPendudukMiskin.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.GrafikJumlahPendudukMiskinGetPayload<{
select: { id: true; year: true; totalPoorPopulation: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/ekonomi/jumlahpendudukmiskin/${id}`);
if (res.ok) {
const data = await res.json();
jumlahPendudukMiskin.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
jumlahPendudukMiskin.findUnique.data = null;
}
} catch (error) {
console.error("Error loading grafik jumlah penduduk miskin:", error);
jumlahPendudukMiskin.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateJumlahPendudukMiskin.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/${id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
}
);
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await jumlahPendudukMiskin.findMany.load();
return result.data;
} catch (error) {
console.error(
"Error update data grafik jumlah penduduk miskin:",
error
);
toast.error("Gagal update data grafik jumlah penduduk miskin");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
jumlahPendudukMiskin.delete.loading = true;
const response = await fetch(
`/api/ekonomi/jumlahpendudukmiskin/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(
result.message || "Grafik jumlah penduduk miskin berhasil dihapus"
);
await jumlahPendudukMiskin.findMany.load(); // refresh list
} else {
toast.error(
result?.message || "Gagal menghapus grafik jumlah penduduk miskin"
);
}
} catch (error) {
console.error("Gagal delete grafik jumlah penduduk miskin:", error);
toast.error(
"Terjadi kesalahan saat menghapus grafik jumlah penduduk miskin"
);
} finally {
jumlahPendudukMiskin.delete.loading = false;
}
},
},
});
export default jumlahPendudukMiskin;

View File

@@ -0,0 +1,178 @@
import ApiFetch from "@/lib/api-fetch";
import { Prisma } from "@prisma/client";
import { toast } from "react-toastify";
import { proxy } from "valtio";
import { z } from "zod";
const templateDataPendidikan = z.object({
name: z.string().min(1, "Data nama harus diisi"),
jumlah: z.string().min(1, "Data jumlah harus diisi"),
});
type DataPendidikan = Prisma.DataPendidikanGetPayload<{
select: {
id: true;
name: true;
jumlah: true;
};
}>;
const defaultForm: Omit<DataPendidikan, "id"> & { id?: string } = {
name: "",
jumlah: "",
};
const dataPendidikan = proxy({
create: {
form: defaultForm,
loading: false,
async create() {
const cek = templateDataPendidikan.safeParse(dataPendidikan.create.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
dataPendidikan.create.loading = true;
const res = await ApiFetch.api.pendidikan.datapendidikan["create"].post(
dataPendidikan.create.form
);
if (res.status === 200) {
const id = res.data?.data?.id;
if (id) {
toast.success("Success create");
dataPendidikan.create.form = {
name: "",
jumlah: "",
};
dataPendidikan.findMany.load();
return id;
}
}
return toast.error("failed create");
} catch (error) {
console.log((error as Error).message);
} finally {
dataPendidikan.create.loading = false;
}
},
},
findMany: {
data: null as
| Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}>[]
| null,
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.datapendidikan[
"findMany"
].get();
if (res.status === 200) {
dataPendidikan.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.DataPendidikanGetPayload<{
select: { id: true; name: true; jumlah: true };
}> | null,
async load(id: string) {
try {
const res = await fetch(`/api/pendidikan/datapendidikan/${id}`);
if (res.ok) {
const data = await res.json();
dataPendidikan.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
dataPendidikan.findUnique.data = null;
}
} catch (error) {
console.error("Error loading data pendidikan:", error);
dataPendidikan.findUnique.data = null;
}
},
},
update: {
id: "",
form: { ...defaultForm },
loading: false,
async byId() {
// Method implementation if needed
},
async submit() {
const id = this.id;
if (!id) {
toast.warn("ID tidak valid");
return null;
}
const cek = templateDataPendidikan.safeParse(this.form);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => (v.path as string[]).join("."))
.join("\n")}] required`;
toast.error(err);
return null;
}
this.loading = true;
try {
const response = await fetch(`/api/pendidikan/datapendidikan/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(this.form),
});
const result = await response.json();
if (!response.ok || !result?.success) {
throw new Error(result?.message || "Gagal update data");
}
toast.success("Berhasil update data!");
await dataPendidikan.findMany.load();
return result.data;
} catch (error) {
console.error("Error update data data pendidikan:", error);
toast.error("Gagal update data data pendidikan");
} finally {
this.loading = false;
}
},
},
delete: {
loading: false,
async byId(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
dataPendidikan.delete.loading = true;
const response = await fetch(
`/api/pendidikan/datapendidikan/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Data berhasil dihapus");
await dataPendidikan.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus data");
}
} catch (error) {
console.error("Gagal delete data pendidikan:", error);
toast.error("Terjadi kesalahan saat menghapus data pendidikan");
} finally {
dataPendidikan.delete.loading = false;
}
},
},
});
export default dataPendidikan;

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect } from 'react';
import { useProxy } from 'valtio/utils';
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
function EditDataPendidikan() {
const router = useRouter()
const params = useParams() as { id: string }
const stateDPM = useProxy(dataPendidikan)
const id = params.id
// Load data saat komponen mount
useEffect(() => {
if (id) {
stateDPM.findUnique.load(id).then(() => {
const data = stateDPM.findUnique.data
if (data) {
stateDPM.update.form = {
name: data.name || "",
jumlah: data.jumlah || "",
}
}
})
}
}, [id])
const handleSubmit = async () => {
// Set the ID before submitting
stateDPM.update.id = id;
await stateDPM.update.submit();
router.push('/admin/pendidikan/data-pendidikan')
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: '100%', md: '50%' }} p={'md'}>
<Stack>
<Title order={3}>Edit Data Pendidikan</Title>
<TextInput
label="Nama"
placeholder="masukkan nama"
value={stateDPM.update.form.name}
onChange={(val) => {
stateDPM.update.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
placeholder="masukkan jumlah"
value={stateDPM.update.form.jumlah}
onChange={(val) => {
stateDPM.update.form.jumlah = val.currentTarget.value;
}}
/>
<Button
mt={10}
bg={colors['blue-button']}
onClick={handleSubmit}
>
Simpan
</Button>
</Stack>
</Paper>
</Box>
)
}
export default EditDataPendidikan;

View File

@@ -0,0 +1,81 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import colors from '@/con/colors';
import { Box, Button, Group, Paper, Stack, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
import dataPendidikan from '../../../_state/pendidikan/data-pendidikan';
function CreateDataPendidikan() {
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<any[]>([]);
const router = useRouter()
const resetForm = () => {
stateDPM.create.form = {
name: "",
jumlah: "",
}
}
const handleSubmit = async () => {
const id = await stateDPM.create.create();
if (id) {
const idStr = String(id);
await stateDPM.findUnique.load(idStr);
if (stateDPM.findUnique.data) {
setChartData([stateDPM.findUnique.data]);
}
}
resetForm();
router.push("/admin/pendidikan/data-pendidikan");
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack size={20} />
</Button>
</Box>
<Box>
<Paper w={{ base: '100%', md: '50%' }} bg={colors['white-1']} p={'md'}>
<Title order={4}>Tambah Data Pendidikan</Title>
<Stack gap={"xs"}>
<TextInput
label="Nama"
type="text"
value={stateDPM.create.form.name}
placeholder="Masukkan nama"
onChange={(val) => {
stateDPM.create.form.name = val.currentTarget.value;
}}
/>
<TextInput
label="Jumlah"
type="number"
value={stateDPM.create.form.jumlah}
placeholder="Masukkan jumlah"
onChange={(val) => {
stateDPM.create.form.jumlah = val.currentTarget.value;
}}
/>
<Group>
<Button
bg={colors['blue-button']}
mt={10}
onClick={handleSubmit}
>
Submit
</Button>
</Group>
</Stack>
</Paper>
</Box>
</Box>
);
}
export default CreateDataPendidikan;

View File

@@ -1,74 +1,169 @@
'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 } from '@mantine/core'; import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react'; import { useMediaQuery, useShallowEffect } from '@mantine/hooks';
import { IconEdit, IconSearch, IconTrash } from '@tabler/icons-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { Bar, BarChart, Legend, Tooltip, XAxis, YAxis } from 'recharts';
import { useProxy } from 'valtio/utils';
import HeaderSearch from '../../_com/header'; import HeaderSearch from '../../_com/header';
import JudulList from '../../_com/judulList'; import JudulList from '../../_com/judulList';
import { ModalKonfirmasiHapus } from '../../_com/modalKonfirmasiHapus';
import dataPendidikan from '../../_state/pendidikan/data-pendidikan';
function DataPendidikan() { function DataPendidikan() {
const [search, setSearch] = useState("");
return ( return (
<Box> <Box>
<HeaderSearch <HeaderSearch
title='Data Pendidikan' title='Data Pendidikan'
placeholder='pencarian' placeholder='pencarian'
searchIcon={<IconSearch size={20} />} searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/> />
<ListDataPendidikan/> <ListDataPendidikan search={search} />
</Box> </Box>
); );
} }
function ListDataPendidikan() { function ListDataPendidikan({ search }: { search: string }) {
type DPMrafik = {
id: string;
name: string;
jumlah: number;
}
const stateDPM = useProxy(dataPendidikan);
const [chartData, setChartData] = useState<DPMrafik[]>([]);
const [mounted, setMounted] = useState(false); // untuk memastikan DOM sudah ready
const isTablet = useMediaQuery('(max-width: 1024px)')
const isMobile = useMediaQuery('(max-width: 768px)')
const [modalHapus, setModalHapus] = useState(false)
const [selectedId, setSelectedId] = useState<string | null>(null)
const router = useRouter(); const router = useRouter();
const handleDelete = () => {
if (selectedId) {
stateDPM.delete.byId(selectedId)
setModalHapus(false)
setSelectedId(null)
stateDPM.findMany.load()
}
}
useShallowEffect(() => {
setMounted(true)
stateDPM.findMany.load()
}, [])
useEffect(() => {
setMounted(true);
if (stateDPM.findMany.data) {
setChartData(stateDPM.findMany.data.map((item) => ({
id: item.id,
name: item.name,
jumlah: Number(item.jumlah)
})));
}
}, [stateDPM.findMany.data]);
const filteredData = (stateDPM.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.name.toLowerCase().includes(keyword) ||
item.jumlah.toLowerCase().includes(keyword)
);
});
if (!stateDPM.findMany.data) {
return (
<Stack>
<Skeleton h={500} />
</Stack>
)
}
return ( return (
<Box py={10}> <Box py={10}>
<Paper bg={colors['white-1']} p="md"> <Stack gap={'xs'}>
<Stack> <Paper bg={colors['white-1']} p={'md'}>
<JudulList <JudulList
title='List Data Pendidikan' title='List Data Pendidikan'
href='/admin/pendidikan/data-pendidikan/create' href='/admin/pendidikan/data-pendidikan/create'
/> />
<Box style={{overflowX: 'auto'}}> <Table striped withTableBorder withRowBorders>
<Table striped withRowBorders withTableBorder style={{minWidth: '700px'}}> <TableThead>
<TableThead> <TableTr>
<TableTr> <TableTh>Nama</TableTh>
<TableTh>Nomor</TableTh> <TableTh>Jumlah</TableTh>
<TableTh>Nama Lengkap</TableTh> <TableTh>Edit</TableTh>
<TableTh>Nomor Telepon</TableTh> <TableTh>Delete</TableTh>
<TableTh>Email</TableTh> </TableTr>
<TableTh>Detail</TableTh> </TableThead>
</TableTr> <TableTbody>
</TableThead> {filteredData.map((item) => (
<TableTbody> <TableTr key={item.id}>
<TableTr> <TableTd>{item.name}</TableTd>
<TableTd>{item.jumlah}</TableTd>
<TableTd> <TableTd>
<Box w={100}> <Button color='green' onClick={() => router.push(`/admin/pendidikan/data-pendidikan/${item.id}`)}>
<Text truncate="end" fz={"sm"}>1</Text> <IconEdit size={20} />
</Box> </Button>
</TableTd> </TableTd>
<TableTd> <TableTd>
<Text truncate="end" fz={"sm"}>Nama Lengkap</Text> <Button
</TableTd> color='red'
<TableTd> disabled={stateDPM.delete.loading}
<Text truncate="end" fz={"sm"}>Nomor Telepon</Text> onClick={() => {
</TableTd> setSelectedId(item.id)
<TableTd> setModalHapus(true)
<Text truncate="end" fz={"sm"}>Email</Text> }}>
</TableTd> <IconTrash size={20} />
<TableTd>
<Button onClick={() => router.push('/admin/pendidikan/data-pendidikan/detail')}>
<IconDeviceImacCog size={25} />
</Button> </Button>
</TableTd> </TableTd>
</TableTr> </TableTr>
</TableTbody> ))}
</Table> </TableTbody>
</Table>
</Paper>
{/* Chart */}
{!mounted && !chartData ? (
<Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={3}>Grafik Data Pendidikan</Title>
<Text c='dimmed'>Belum ada data untuk ditampilkan dalam grafik</Text>
</Paper>
</Box> </Box>
</Stack> ) : (
</Paper> <Box style={{ width: '100%', minWidth: 300, height: 400, minHeight: 300 }}>
<Paper bg={colors['white-1']} p={'md'}>
<Title pb={10} order={4}>Grafik Data Pendidikan</Title>
{mounted && chartData.length > 0 && (
<BarChart width={isMobile ? 450 : isTablet ? 500 : 550} height={350} data={chartData} >
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="jumlah" fill={colors['blue-button']} name="Data Pendidikan" />
</BarChart>
)}
</Paper>
</Box>
)}
</Stack>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleDelete}
text='Apakah anda yakin ingin menghapus grafik data pendidikan ini?'
/>
</Box> </Box>
) );
} }
export default DataPendidikan; export default DataPendidikan;

View File

@@ -6,33 +6,35 @@ import grafikJumlahPendudukMiskinCreate from "./create";
import grafikJumlahPendudukMiskinDelete from "./del"; import grafikJumlahPendudukMiskinDelete from "./del";
const JumlahPendudukMiskin = new Elysia({ const JumlahPendudukMiskin = new Elysia({
prefix: "/jumlahpendudukmiskin", prefix: "/jumlahpendudukmiskin",
tags: ["Ekonomi/Jumlah Penduduk Miskin"], tags: ["Ekonomi/Jumlah Penduduk Miskin"],
}) })
.get("/find-many", grafikJumlahPendudukMiskinFindMany) .get("/find-many", grafikJumlahPendudukMiskinFindMany)
.get("/:id", async (context) => { .get("/:id", async (context) => {
const response = await grafikJumlahPendudukMiskinFindById(new Request(context.request)) const response = await grafikJumlahPendudukMiskinFindById(
return response new Request(context.request)
}) );
.put("/:id", grafikJumlahPendudukMiskinUpdate, { return response;
params: t.Object({ })
id: t.String(), .put("/:id", grafikJumlahPendudukMiskinUpdate, {
}), params: t.Object({
body: t.Object({ id: t.String(),
year: t.Number(), }),
totalPoorPopulation: t.Number(), body: t.Object({
}), year: t.Number(),
}) totalPoorPopulation: t.Number(),
.post("/create", grafikJumlahPendudukMiskinCreate, { }),
body: t.Object({ })
year: t.Number(), .post("/create", grafikJumlahPendudukMiskinCreate, {
totalPoorPopulation: t.Number(), body: t.Object({
}), year: t.Number(),
}) totalPoorPopulation: t.Number(),
.delete("/del/:id", grafikJumlahPendudukMiskinDelete, { }),
params: t.Object({ })
id: t.String(), .delete("/del/:id", grafikJumlahPendudukMiskinDelete, {
}), params: t.Object({
}); id: t.String(),
}),
});
export default JumlahPendudukMiskin; export default JumlahPendudukMiskin;

View File

@@ -0,0 +1,31 @@
import prisma from "@/lib/prisma";
import { Prisma } from "@prisma/client";
import { Context } from "elysia";
type FormCreate = Prisma.DataPendidikanGetPayload<{
select: {
name: true;
jumlah: true;
};
}>;
export default async function dataPendidikanCreate(context: Context) {
const body = context.body as FormCreate;
const created = await prisma.dataPendidikan.create({
data: {
name: body.name,
jumlah: body.jumlah,
},
select: {
id: true,
name: true,
jumlah: true,
},
});
return {
success: true,
message: "Data pendidikan berhasil dibuat",
data: created,
};
}

View File

@@ -0,0 +1,43 @@
import prisma from "@/lib/prisma"
import { Context } from "elysia"
export default async function dataPendidikanDelete(context: Context) {
const {id} = context.params as {id: string}
try {
const existingData = await prisma.dataPendidikan.findUnique({
where: {
id: id,
}
})
if (!existingData) {
return {
success: false,
message: "Data tidak ditemukan",
data: null,
}
}
await prisma.dataPendidikan.delete({
where: {
id: id
}
})
return {
success: true,
message: "Data berhasil dihapus",
data: {
id: id,
deleted: true,
},
}
} catch (error) {
return {
success: false,
message: error instanceof Error ? error.message : "Unknown error",
data: null,
}
}
}

View File

@@ -0,0 +1,8 @@
import prisma from "@/lib/prisma";
export default async function dataPendidikanFindMany() {
const res = await prisma.dataPendidikan.findMany();
return {
data: res,
};
}

View File

@@ -0,0 +1,65 @@
import prisma from "@/lib/prisma";
export default async function dataPendidikanFindUnique(
request: Request
) {
const url = new URL(request.url);
const pathSegments = url.pathname.split("/");
const id = pathSegments[pathSegments.length - 1];
if (!id) {
return Response.json(
{
success: false,
message: "ID tidak boleh kosong",
},
{ status: 400 }
);
}
try {
if (typeof id !== "string") {
return Response.json(
{
success: false,
message: "ID tidak valid",
},
{ status: 400 }
);
}
const data = await prisma.dataPendidikan.findUnique({
where: { id },
});
if (!data) {
return Response.json(
{
success: false,
message: "Data tidak ditemukan",
},
{ status: 404 }
);
}
return Response.json(
{
success: true,
message: "Berhasil mengambil data pendidikan berdasarkan ID",
data,
},
{ status: 200 }
);
} catch (e) {
console.error("Find by ID error:", e);
return Response.json(
{
success: false,
message:
"Gagal mengambil data pendidikan: " +
(e instanceof Error ? e.message : "Unknown error"),
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,39 @@
import Elysia, { t } from "elysia";
import dataPendidikanFindMany from "./findMany";
import dataPendidikanFindUnique from "./findUnique";
import dataPendidikanUpdate from "./updt";
import dataPendidikanCreate from "./create";
import dataPendidikanDelete from "./del";
const DataPendidikan = new Elysia({
prefix: "/datapendidikan",
tags: ["Pendidikan/Data Pendidikan"],
})
.get("/findMany", dataPendidikanFindMany)
.get("/:id", async (context) => {
const response = await dataPendidikanFindUnique(
new Request(context.request)
);
return response;
})
.put("/:id", dataPendidikanUpdate, {
params: t.Object({
id: t.String(),
}),
body: t.Object({
name: t.String(),
jumlah: t.String(),
}),
})
.post("/create", dataPendidikanCreate, {
body: t.Object({
name: t.String(),
jumlah: t.String(),
}),
})
.delete("/del/:id", dataPendidikanDelete, {
params: t.Object({
id: t.String(),
}),
});
export default DataPendidikan;

View File

@@ -0,0 +1,48 @@
import { Context } from "elysia";
import prisma from "@/lib/prisma";
export default async function dataPendidikanUpdate(context: Context) {
const id = context.params?.id;
if (!id) {
return {
success: false,
message: "Id tidak ditemukan",
}
}
const existingData = await prisma.dataPendidikan.findUnique({
where: {
id: id,
}
})
const {name, jumlah} = context.body as {
name: string;
jumlah: string;
}
if (!existingData) {
return {
success: false,
message: "Data tidak ditemukan",
data: null,
}
}
const updated = await prisma.dataPendidikan.update({
where: {
id: id,
},
data: {
name: name,
jumlah: jumlah,
},
})
return {
success: true,
message: "Data pendidikan berhasil diupdate",
data: updated,
}
}

View File

@@ -3,6 +3,7 @@ import InfoSekolahPAUD from "./info-sekolah-paud";
import ProgramPendidikanAnak from "./program-pendidikan-anak"; import ProgramPendidikanAnak from "./program-pendidikan-anak";
import BimbinganBelajarDesa from "./bimbingan-belajar-desa"; import BimbinganBelajarDesa from "./bimbingan-belajar-desa";
import PendidikanNonFormal from "./pendidikan-non-formal"; import PendidikanNonFormal from "./pendidikan-non-formal";
import DataPendidikan from "./data-pendidikan";
const Pendidikan = new Elysia({ const Pendidikan = new Elysia({
prefix: "/api/pendidikan", prefix: "/api/pendidikan",
@@ -13,5 +14,6 @@ const Pendidikan = new Elysia({
.use(ProgramPendidikanAnak) .use(ProgramPendidikanAnak)
.use(BimbinganBelajarDesa) .use(BimbinganBelajarDesa)
.use(PendidikanNonFormal) .use(PendidikanNonFormal)
.use(DataPendidikan)
export default Pendidikan; export default Pendidikan;