UI & API Menu Pendidikan, Submenu Beasiswa tab beasiswa pendaftar

This commit is contained in:
2025-07-29 12:29:25 +08:00
parent 50e8999205
commit 0109886e00
18 changed files with 1129 additions and 6 deletions

View File

@@ -0,0 +1,282 @@
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 templateBeasiswaPendaftar = z.object({
namaLengkap: z.string().min(1, "Nama harus diisi"),
nik: z.string().min(1, "NIK harus diisi"),
tempatLahir: z.string().min(1, "Tempat lahir harus diisi"),
tanggalLahir: z.string().min(1, "Tanggal lahir harus diisi"),
jenisKelamin: z.string().min(1, "Jenis kelamin harus diisi"),
kewarganegaraan: z.string().min(1, "Kewarganegaraan harus diisi"),
agama: z.string().min(1, "Agama harus diisi"),
alamatKTP: z.string().min(1, "Alamat KTP harus diisi"),
alamatDomisili: z.string().min(1, "Alamat domisili harus diisi"),
noHp: z.string().min(1, "No HP harus diisi"),
email: z.string().min(1, "Email harus diisi"),
statusPernikahan: z.string().min(1, "Status pernikahan harus diisi"),
ukuranBaju: z.string().min(1, "Ukuran baju harus diisi"),
});
const defaultBeasiswaPendaftar = {
namaLengkap: "",
nik: "",
tempatLahir: "",
tanggalLahir: "",
jenisKelamin: "",
kewarganegaraan: "",
agama: "",
alamatKTP: "",
alamatDomisili: "",
noHp: "",
email: "",
statusPernikahan: "",
ukuranBaju: "",
};
const beasiswaPendaftar = proxy({
create: {
form: { ...defaultBeasiswaPendaftar },
loading: false,
async create() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.create.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
return toast.error(err);
}
try {
beasiswaPendaftar.create.loading = true;
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"create"
].post(beasiswaPendaftar.create.form);
if (res.status === 200) {
beasiswaPendaftar.findMany.load();
return toast.success("Data Berhasil Dibuat, Silahkan Menunggu Konfirmasi dari Admin di WhatsApp");
}
console.log(res);
return toast.error("failed create");
} catch (error) {
console.log(error);
return toast.error("failed create");
} finally {
beasiswaPendaftar.create.loading = false;
}
},
},
findMany: {
data: [] as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}>[],
loading: false,
async load() {
const res = await ApiFetch.api.pendidikan.beasiswa.beasiswapendaftar[
"findMany"
].get();
if (res.status === 200) {
beasiswaPendaftar.findMany.data = res.data?.data ?? [];
}
},
},
findUnique: {
data: null as Prisma.BeasiswaPendaftarGetPayload<{
omit: {
isActive: true;
};
}> | null,
loading: false,
async load(id: string) {
try {
const res = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${id}`
);
if (res.ok) {
const data = await res.json();
beasiswaPendaftar.findUnique.data = data.data ?? null;
} else {
console.error("Failed to fetch data", res.status, res.statusText);
beasiswaPendaftar.findUnique.data = null;
}
} catch (error) {
console.error("Error fetching data:", error);
beasiswaPendaftar.findUnique.data = null;
}
},
},
delete: {
loading: false,
async delete(id: string) {
if (!id) return toast.warn("ID tidak valid");
try {
beasiswaPendaftar.delete.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/del/${id}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
}
);
const result = await response.json();
if (response.ok && result?.success) {
toast.success(result.message || "Beasiswa berhasil dihapus");
await beasiswaPendaftar.findMany.load(); // refresh list
} else {
toast.error(result?.message || "Gagal menghapus beasiswa");
}
} catch (error) {
console.error("Gagal delete:", error);
toast.error("Terjadi kesalahan saat menghapus beasiswa");
} finally {
beasiswaPendaftar.delete.loading = false;
}
},
},
update: {
id: "",
form: { ...defaultBeasiswaPendaftar },
loading: false,
async load(id: string) {
if (!id) {
toast.warn("ID tidak valid");
return null;
}
try {
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${id}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result?.success) {
const data = result.data;
this.id = data.id;
this.form = {
namaLengkap: data.namaLengkap,
nik: data.nik,
tempatLahir: data.tempatLahir,
tanggalLahir: data.tanggalLahir,
jenisKelamin: data.jenisKelamin,
kewarganegaraan: data.kewarganegaraan,
agama: data.agama,
alamatKTP: data.alamatKTP,
alamatDomisili: data.alamatDomisili,
noHp: data.noHp,
email: data.email,
statusPernikahan: data.statusPernikahan,
ukuranBaju: data.ukuranBaju,
};
return data; // Return the loaded data
} else {
throw new Error(result?.message || "Gagal memuat data");
}
} catch (error) {
console.error("Error loading beasiswa pendaftar:", error);
toast.error(
error instanceof Error ? error.message : "Gagal memuat data"
);
return null;
}
},
async update() {
const cek = templateBeasiswaPendaftar.safeParse(
beasiswaPendaftar.update.form
);
if (!cek.success) {
const err = `[${cek.error.issues
.map((v) => `${v.path.join(".")}`)
.join("\n")}] required`;
toast.error(err);
return false;
}
try {
beasiswaPendaftar.update.loading = true;
const response = await fetch(
`/api/pendidikan/beasiswa/beasiswapendaftar/${this.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
namaLengkap: this.form.namaLengkap,
nik: this.form.nik,
tanggalLahir: this.form.tanggalLahir,
jenisKelamin: this.form.jenisKelamin,
kewarganegaraan: this.form.kewarganegaraan,
agama: this.form.agama,
alamatKTP: this.form.alamatKTP,
alamatDomisili: this.form.alamatDomisili,
noHp: this.form.noHp,
email: this.form.email,
statusPernikahan: this.form.statusPernikahan,
ukuranBaju: this.form.ukuranBaju,
}),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP error! status: ${response.status}`
);
}
const result = await response.json();
if (result.success) {
toast.success("Berhasil update beasiswa pendaftar");
await beasiswaPendaftar.findMany.load(); // refresh list
return true;
} else {
throw new Error(result.message || "Gagal update beasiswa pendaftar");
}
} catch (error) {
console.error("Error updating beasiswa pendaftar:", error);
toast.error(
error instanceof Error
? error.message
: "Terjadi kesalahan saat update beasiswa pendaftar"
);
return false;
} finally {
beasiswaPendaftar.update.loading = false;
}
},
reset() {
beasiswaPendaftar.update.id = "";
beasiswaPendaftar.update.form = { ...defaultBeasiswaPendaftar };
},
},
});
const beasiswaDesaState = proxy({
beasiswaPendaftar,
});
export default beasiswaDesaState;

View File

@@ -21,7 +21,7 @@ function KeamananLingkungan() {
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListKeamananLingkungan search={search}/>
<ListKeamananLingkungan search={search} />
</Box>
);
}

View File

@@ -0,0 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Stack, Tabs, TabsList, TabsPanel, TabsTab, Title } from '@mantine/core';
import { usePathname, useRouter } from 'next/navigation';
import React, { useEffect, useState } from 'react';
function LayoutTabs({ children }: { children: React.ReactNode }) {
const router = useRouter()
const pathname = usePathname()
const tabs = [
{
label: "Beasiswa Pendaftar",
value: "beasiswa-pendaftar",
href: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
},
{
label: "Keunggulan Program",
value: "keunggulan-program",
href: "/admin/pendidikan/beasiswa-desa/keunggulan-program"
},
];
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 tab = tabs.find(t => t.value === value)
if (tab) {
router.push(tab.href)
}
setActiveTab(value)
}
useEffect(() => {
const match = tabs.find(tab => tab.href === pathname)
if (match) {
setActiveTab(match.value)
}
}, [pathname])
return (
<Stack>
<Title order={3}>Beasiswa Desa</Title>
<Tabs color={colors['blue-button']} variant='pills' value={activeTab} onChange={handleTabChange}>
<TabsList p={"xs"} bg={"#BBC8E7FF"}>
{tabs.map((e, i) => (
<TabsTab key={i} value={e.value}>{e.label}</TabsTab>
))}
</TabsList>
{tabs.map((e, i) => (
<TabsPanel key={i} value={e.value}>
{/* Konten dummy, bisa diganti tergantung routing */}
<></>
</TabsPanel>
))}
</Tabs>
{children}
</Stack>
);
}
export default LayoutTabs;

View File

@@ -0,0 +1,137 @@
'use client'
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
import beasiswaDesaState from '@/app/admin/(dashboard)/_state/pendidikan/beasiswa-desa';
import colors from '@/con/colors';
import { Box, Button, Flex, Paper, Skeleton, Stack, Text } from '@mantine/core';
import { useShallowEffect } from '@mantine/hooks';
import { IconArrowBack, IconX } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailBeasiswaPendaftar() {
const state = useProxy(beasiswaDesaState.beasiswaPendaftar)
const [modalHapus, setModalHapus] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const router = useRouter()
const params = useParams()
useShallowEffect(() => {
state.findUnique.load(params?.id as string)
}, [])
const handleHapus = () => {
if (selectedId) {
state.delete.delete(selectedId)
setModalHapus(false)
setSelectedId(null)
router.push("/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar")
}
}
if (!state.findUnique.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box>
<Box mb={10}>
<Button variant="subtle" onClick={() => router.back()}>
<IconArrowBack color={colors['blue-button']} size={25} />
</Button>
</Box>
<Paper bg={colors['white-1']} w={{ base: "100%", md: "100%", lg: "50%" }} p={'md'}>
<Stack>
<Flex justify="space-between" gap={"xs"}>
<Text fz={"xl"} fw={"bold"}>Detail Beasiswa Pendaftar</Text>
<Button
onClick={() => {
if (state.findUnique.data) {
setSelectedId(state.findUnique.data.id);
setModalHapus(true);
}
}}
disabled={state.delete.loading || !state.findUnique.data}
color={"red"}
>
<IconX size={20} />
</Button>
</Flex>
{state.findUnique.data ? (
<Paper key={state.findUnique.data.id} bg={colors['BG-trans']} p={'md'}>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Nama Lengkap</Text>
<Text fz={"lg"}>{state.findUnique.data?.namaLengkap}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>NIK</Text>
<Text fz={"lg"}>{state.findUnique.data?.nik}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tempat Lahir</Text>
<Text fz={"lg"}>{state.findUnique.data?.tempatLahir}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Lahir</Text>
<Text fz={"lg"}>{state.findUnique.data?.tanggalLahir ? new Date(state.findUnique.data.tanggalLahir).toLocaleDateString() : '-'}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Jenis Kelamin</Text>
<Text fz={"lg"}>{state.findUnique.data?.jenisKelamin}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Kewarganegaraan</Text>
<Text fz={"lg"}>{state.findUnique.data?.kewarganegaraan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Agama</Text>
<Text fz={"lg"}>{state.findUnique.data?.agama}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat KTP</Text>
<Text fz={"lg"}>{state.findUnique.data?.alamatKTP}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Alamat Domisili</Text>
<Text fz={"lg"}>{state.findUnique.data?.alamatDomisili}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>No HP</Text>
<Text fz={"lg"}>{state.findUnique.data?.noHp}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Email</Text>
<Text fz={"lg"}>{state.findUnique.data?.email}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Status Pernikahan</Text>
<Text fz={"lg"}>{state.findUnique.data?.statusPernikahan}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Ukuran Baju</Text>
<Text fz={"lg"}>{state.findUnique.data?.ukuranBaju}</Text>
</Box>
</Stack>
</Paper>
) : null}
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<ModalKonfirmasiHapus
opened={modalHapus}
onClose={() => setModalHapus(false)}
onConfirm={handleHapus}
text='Apakah anda yakin ingin menghapus beasiswa desa ini?'
/>
</Box>
);
}
export default DetailBeasiswaPendaftar;

View File

@@ -0,0 +1,107 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client'
import colors from '@/con/colors';
import { Box, Button, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../../_com/header';
import { useEffect, useState } from 'react';
import { useProxy } from 'valtio/utils';
import beasiswaDesaState from '../../../_state/pendidikan/beasiswa-desa';
function BeasiswaPendaftar() {
const [search, setSearch] = useState('');
return (
<Box>
<HeaderSearch
title='Beasiswa Pendaftar'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
/>
<ListBeasiswaPendaftar search={search} />
</Box>
);
}
function ListBeasiswaPendaftar({ search }: { search: string }) {
const listDataState = useProxy(beasiswaDesaState.beasiswaPendaftar)
const router = useRouter();
useEffect(() => {
listDataState.findMany.load()
}, [])
const filteredData = (listDataState.findMany.data || []).filter(item => {
const keyword = search.toLowerCase();
return (
item.namaLengkap.toLowerCase().includes(keyword) ||
item.alamatKTP.toLowerCase().includes(keyword) ||
(item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '').toLowerCase().includes(keyword) ||
item.jenisKelamin.toLowerCase().includes(keyword)
);
});
if (!listDataState.findMany.data) {
return (
<Stack py={10}>
<Skeleton h={500} />
</Stack>
)
}
return (
<Box py={10}>
<Paper bg={colors['white-1']} p="md">
<Stack>
<Title order={4}>List Beasiswa Pendaftar</Title>
<Box style={{ overflowX: 'auto' }}>
<Table striped withRowBorders withTableBorder style={{ minWidth: '700px' }}>
<TableThead>
<TableTr>
<TableTh>No</TableTh>
<TableTh>Nama Lengkap</TableTh>
<TableTh>Alamat</TableTh>
<TableTh>Tanggal Lahir</TableTh>
<TableTh>Jenis Kelamin</TableTh>
<TableTh>Detail</TableTh>
</TableTr>
</TableThead>
<TableTbody>
{filteredData.map((item, index) => (
<TableTr key={item.id}>
<TableTd>
<Box w={100}>
<Text truncate="end" fz={"sm"}>{index + 1}</Text>
</Box>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.namaLengkap}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.alamatKTP}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.tanggalLahir ? new Date(item.tanggalLahir).toLocaleDateString() : '-'}</Text>
</TableTd>
<TableTd>
<Text truncate="end" fz={"sm"}>{item.jenisKelamin}</Text>
</TableTd>
<TableTd>
<Button onClick={() => router.push(`/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar/${item.id}`)}>
<IconDeviceImacCog size={25} />
</Button>
</TableTd>
</TableTr>
))}
</TableTbody>
</Table>
</Box>
</Stack>
</Paper>
</Box>
)
}
export default BeasiswaPendaftar;

View File

@@ -3,13 +3,14 @@ import colors from '@/con/colors';
import { Box, Button, Paper, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
import { IconDeviceImacCog, IconSearch } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import HeaderSearch from '../../_com/header';
import HeaderSearch from '../../../_com/header';
function BeasiswaDesa() {
return (
<Box>
<HeaderSearch
title='Beasiswa Desa'
title='Keunggulan Program'
placeholder='pencarian'
searchIcon={<IconSearch size={20} />}
/>

View File

@@ -0,0 +1,13 @@
'use client'
import React from 'react';
import LayoutTabs from './_lib/layoutTabs';
function Layout({ children }: { children: React.ReactNode }) {
return (
<LayoutTabs>
{children}
</LayoutTabs>
);
}
export default Layout;

View File

@@ -349,7 +349,7 @@ export const navBar = [
{
id: "Pendidikan_2",
name: "Beasiswa Desa",
path: "/admin/pendidikan/beasiswa-desa"
path: "/admin/pendidikan/beasiswa-desa/beasiswa-pendaftar"
},
{
id: "Pendidikan_3",