Sinkroniasasi Admin - User, Submenu Info Sekolah Paud
This commit is contained in:
@@ -343,33 +343,40 @@ const lembagaPendidikan = proxy({
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
lembagaPendidikan.findMany.loading = true; // Use the full path to access the property
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
lembagaPendidikan.findMany.loading = true;
|
||||
lembagaPendidikan.findMany.page = page;
|
||||
lembagaPendidikan.findMany.search = search;
|
||||
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
const res =
|
||||
await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan[
|
||||
"find-many"
|
||||
].get({
|
||||
query,
|
||||
});
|
||||
const query: any = {
|
||||
page,
|
||||
limit,
|
||||
...(search && { search }),
|
||||
...(jenjangPendidikan && { jenjangPendidikanId: jenjangPendidikan })
|
||||
};
|
||||
|
||||
console.log('Fetching lembaga with query:', query);
|
||||
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.lembagapendidikan["find-many"].get({ query });
|
||||
|
||||
console.log('API Response:', res);
|
||||
|
||||
if (res.status === 200 && res.data?.success) {
|
||||
lembagaPendidikan.findMany.data = res.data.data || [];
|
||||
lembagaPendidikan.findMany.total = res.data.total || 0;
|
||||
lembagaPendidikan.findMany.totalPages = res.data.totalPages || 1;
|
||||
lembagaPendidikan.findMany.data = Array.isArray(res.data.data) ? res.data.data : [];
|
||||
lembagaPendidikan.findMany.total = typeof res.data.total === 'number' ? res.data.total : 0;
|
||||
lembagaPendidikan.findMany.totalPages = typeof res.data.totalPages === 'number' ? res.data.totalPages : 1;
|
||||
console.log('Successfully loaded lembaga data:', {
|
||||
count: lembagaPendidikan.findMany.data.length,
|
||||
total: lembagaPendidikan.findMany.total,
|
||||
totalPages: lembagaPendidikan.findMany.totalPages
|
||||
});
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load lembaga pendidikan:",
|
||||
res.data?.message
|
||||
res.data?.message || 'No error message provided'
|
||||
);
|
||||
lembagaPendidikan.findMany.data = [];
|
||||
lembagaPendidikan.findMany.total = 0;
|
||||
lembagaPendidikan.findMany.totalPages = 1;
|
||||
throw new Error(res.data?.message || 'Failed to load lembaga pendidikan');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading lembaga pendidikan:", error);
|
||||
@@ -621,7 +628,11 @@ const siswa = proxy({
|
||||
data: null as Array<
|
||||
Prisma.SiswaGetPayload<{
|
||||
include: {
|
||||
lembaga: true;
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
@@ -630,14 +641,16 @@ const siswa = proxy({
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
// Change to arrow function
|
||||
siswa.findMany.loading = true; // Use the full path to access the property
|
||||
jenjangPendidikan: "",
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
siswa.findMany.loading = true;
|
||||
siswa.findMany.page = page;
|
||||
siswa.findMany.search = search;
|
||||
siswa.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (jenjangPendidikan) query.jenjangPendidikanName = jenjangPendidikan;
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.siswa[
|
||||
"find-many"
|
||||
].get({
|
||||
@@ -894,7 +907,11 @@ const pengajar = proxy({
|
||||
data: null as Array<
|
||||
Prisma.PengajarGetPayload<{
|
||||
include: {
|
||||
lembaga: true;
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}>
|
||||
> | null,
|
||||
@@ -903,14 +920,17 @@ const pengajar = proxy({
|
||||
total: 0,
|
||||
loading: false,
|
||||
search: "",
|
||||
load: async (page = 1, limit = 10, search = "") => {
|
||||
jenjangPendidikan: "",
|
||||
load: async (page = 1, limit = 10, search = "", jenjangPendidikan = "") => {
|
||||
// Change to arrow function
|
||||
pengajar.findMany.loading = true; // Use the full path to access the property
|
||||
pengajar.findMany.page = page;
|
||||
pengajar.findMany.search = search;
|
||||
pengajar.findMany.jenjangPendidikan = jenjangPendidikan;
|
||||
try {
|
||||
const query: any = { page, limit };
|
||||
if (search) query.search = search;
|
||||
if (jenjangPendidikan) query.jenjangPendidikanId = jenjangPendidikan;
|
||||
const res = await ApiFetch.api.pendidikan.infosekolahpaud.pengajar[
|
||||
"find-many"
|
||||
].get({
|
||||
|
||||
@@ -4,25 +4,58 @@ import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function lembagaPendidikanFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const search = (context.query.search as string) || "";
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ siswa: { contains: search, mode: "insensitive" } },
|
||||
{ pengajar: { contains: search, mode: "insensitive" } },
|
||||
{ jenjangPendidikan: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
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 jenjangPendidikanName = (context.query.jenjangPendidikanId as string) || "";
|
||||
|
||||
console.log('Lembaga API Query Params:', { page, limit, search, jenjangPendidikanName });
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Filter berdasarkan jenjang pendidikan (jika ada)
|
||||
if (jenjangPendidikanName) {
|
||||
// Cari jenjang pendidikan berdasarkan nama
|
||||
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
|
||||
where: {
|
||||
nama: {
|
||||
equals: jenjangPendidikanName,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
isActive: true,
|
||||
},
|
||||
orderBy: { nama: 'desc' },
|
||||
});
|
||||
|
||||
if (jenjangPendidikan) {
|
||||
where.jenjangId = jenjangPendidikan.id;
|
||||
} else {
|
||||
// Jika tidak ditemukan, return data kosong
|
||||
return {
|
||||
success: true,
|
||||
message: "Jenjang pendidikan tidak ditemukan",
|
||||
data: [],
|
||||
page,
|
||||
limit,
|
||||
totalPages: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ siswa: { nama: { contains: search, mode: "insensitive" } } },
|
||||
{ pengajar: { nama: { contains: search, mode: "insensitive" } } },
|
||||
{ jenjangPendidikan: { nama: { contains: search, mode: "insensitive" } } },
|
||||
];
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.lembaga.findMany({
|
||||
where,
|
||||
@@ -33,13 +66,16 @@ async function lembagaPendidikanFindMany(context: Context) {
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
|
||||
orderBy: { jenjangPendidikan: { nama: 'asc' } },
|
||||
}),
|
||||
prisma.lembaga.count({
|
||||
where,
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('Fetched data count:', data.length);
|
||||
console.log('Total count:', total);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch lembaga pendidikan with pagination",
|
||||
@@ -53,7 +89,7 @@ async function lembagaPendidikanFindMany(context: Context) {
|
||||
console.error("Find many paginated error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch lembaga pendidikan with pagination",
|
||||
message: `Failed fetch lembaga pendidikan: ${e instanceof Error ? e.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,82 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function pengajarFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
const search = (context.query.search as string) || "";
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ lembaga: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
const search = (context.query.search as string) || "";
|
||||
const jenjangPendidikanName = (context.query.jenjangPendidikanId as string) || "";
|
||||
|
||||
console.log('Pengajar API Query Params:', { page, limit, search, jenjangPendidikanId: jenjangPendidikanName });
|
||||
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Filter berdasarkan jenjang pendidikan (jika ada)
|
||||
if (jenjangPendidikanName) {
|
||||
// Cari jenjang pendidikan berdasarkan nama
|
||||
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
|
||||
where: {
|
||||
nama: {
|
||||
equals: jenjangPendidikanName,
|
||||
mode: 'insensitive'
|
||||
},
|
||||
isActive: true
|
||||
},
|
||||
orderBy: { nama: 'desc' },
|
||||
});
|
||||
|
||||
if (jenjangPendidikan) {
|
||||
where.lembaga = {
|
||||
...where.lembaga,
|
||||
jenjangId: jenjangPendidikan.id
|
||||
};
|
||||
} else {
|
||||
// Jika tidak ditemukan, return data kosong
|
||||
return {
|
||||
success: true,
|
||||
message: "Jenjang pendidikan tidak ditemukan",
|
||||
data: [],
|
||||
page,
|
||||
limit,
|
||||
totalPages: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add search condition if search term exists
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: 'insensitive' } },
|
||||
{ lembaga: { nama: { contains: search, mode: 'insensitive' } } }
|
||||
];
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.pengajar.findMany({
|
||||
where,
|
||||
include: {
|
||||
lembaga: true,
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true
|
||||
}
|
||||
}
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
|
||||
orderBy: { lembaga: { jenjangPendidikan: { nama: 'asc' } } },
|
||||
}),
|
||||
prisma.pengajar.count({
|
||||
where,
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('Fetched pengajar data count:', data.length);
|
||||
console.log('Total pengajar count:', total);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch pengajar with pagination",
|
||||
@@ -45,13 +86,12 @@ async function pengajarFindMany(context: Context) {
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
} catch (error) {
|
||||
console.error("Error in pengajarFindMany:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch pengajar with pagination",
|
||||
message: `Failed fetch pengajar: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default pengajarFindMany;
|
||||
export default pengajarFindMany;
|
||||
@@ -1,41 +1,82 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// /api/berita/findManyPaginated.ts
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Context } from "elysia";
|
||||
|
||||
async function siswaFindMany(context: Context) {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
const search = (context.query.search as string) || "";
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ lembaga: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
const page = Number(context.query.page) || 1;
|
||||
const limit = Number(context.query.limit) || 10;
|
||||
const skip = (page - 1) * limit;
|
||||
const search = (context.query.search as string) || "";
|
||||
const jenjangPendidikanName = (context.query.jenjangPendidikanName as string) || "";
|
||||
|
||||
console.log('Siswa API Query Params:', { page, limit, search, jenjangPendidikanId: jenjangPendidikanName });
|
||||
|
||||
// Buat where clause
|
||||
const where: any = { isActive: true };
|
||||
|
||||
// Filter berdasarkan jenjang pendidikan (jika ada)
|
||||
if (jenjangPendidikanName) {
|
||||
// Cari jenjang pendidikan berdasarkan nama
|
||||
const jenjangPendidikan = await prisma.jenjangPendidikan.findFirst({
|
||||
where: {
|
||||
nama: {
|
||||
equals: jenjangPendidikanName,
|
||||
mode: 'insensitive'
|
||||
},
|
||||
isActive: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (jenjangPendidikan) {
|
||||
where.lembaga = {
|
||||
...where.lembaga,
|
||||
jenjangId: jenjangPendidikan.id
|
||||
};
|
||||
} else {
|
||||
// Jika tidak ditemukan, return data kosong
|
||||
return {
|
||||
success: true,
|
||||
message: "Jenjang pendidikan tidak ditemukan",
|
||||
data: [],
|
||||
page,
|
||||
limit,
|
||||
totalPages: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Tambahkan pencarian (jika ada)
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ nama: { contains: search, mode: "insensitive" } },
|
||||
{ lembaga: { nama: { contains: search, mode: 'insensitive' } } }
|
||||
];
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
prisma.siswa.findMany({
|
||||
where,
|
||||
include: {
|
||||
lembaga: true,
|
||||
lembaga: {
|
||||
include: {
|
||||
jenjangPendidikan: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: { createdAt: 'desc' }, // opsional, kalau mau urut berdasarkan waktu
|
||||
orderBy: { lembaga: { jenjangPendidikan: { nama: 'asc' } } },
|
||||
}),
|
||||
prisma.siswa.count({
|
||||
where,
|
||||
})
|
||||
]);
|
||||
|
||||
console.log('Fetched siswa data count:', data.length);
|
||||
console.log('Total siswa count:', total);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Success fetch siswa with pagination",
|
||||
@@ -45,11 +86,11 @@ async function siswaFindMany(context: Context) {
|
||||
totalPages: Math.ceil(total / limit),
|
||||
total,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Find many paginated error:", e);
|
||||
} catch (error) {
|
||||
console.error("Error in siswaFindMany:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed fetch siswa with pagination",
|
||||
message: `Failed fetch siswa: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,53 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||
import { Badge, Box, Button, Card, Center, Container, Divider, Flex, Grid, GridCol, Group, Image, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Badge, Box, Button, Card, Center, Container, Divider,
|
||||
Flex, Grid, GridCol, Group, Image, Pagination,
|
||||
Paper, SimpleGrid, Skeleton, Stack, Text, Title
|
||||
} from '@mantine/core';
|
||||
import { IconArrowRight, IconCalendar } from '@tabler/icons-react';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Semua() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useTransitionRouter();
|
||||
|
||||
// Parameter URL
|
||||
// Ambil parameter langsung dari URL
|
||||
const search = searchParams.get('search') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || '1');
|
||||
const [page, setPage] = useState(currentPage);
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
|
||||
// Gunakan proxy untuk state
|
||||
// Gunakan proxy untuk state global
|
||||
const state = useProxy(stateDashboardBerita.berita);
|
||||
const featured = useProxy(stateDashboardBerita.berita.findFirst); // ✅ Berita utama
|
||||
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
||||
const loadingGrid = state.findMany.loading;
|
||||
const loadingFeatured = featured.loading;
|
||||
|
||||
// Load berita utama (hanya sekali)
|
||||
// Load berita utama sekali saja
|
||||
useEffect(() => {
|
||||
if (!featured.data && !loadingFeatured) {
|
||||
stateDashboardBerita.berita.findFirst.load();
|
||||
}
|
||||
}, [featured.data, loadingFeatured]);
|
||||
|
||||
// Load berita terbaru (untuk grid) saat page/search berubah
|
||||
// Load berita terbaru tiap page / search berubah
|
||||
useEffect(() => {
|
||||
const limit = 3; // Sesuaikan dengan tampilan grid
|
||||
const limit = 3;
|
||||
state.findMany.load(page, limit, search);
|
||||
}, [page, search]);
|
||||
|
||||
// Update URL saat page berubah
|
||||
useEffect(() => {
|
||||
const url = new URLSearchParams();
|
||||
// Handler pagination → langsung update URL
|
||||
const handlePageChange = (newPage: number) => {
|
||||
const url = new URLSearchParams(searchParams.toString());
|
||||
if (search) url.set('search', search);
|
||||
if (page > 1) url.set('page', page.toString());
|
||||
if (newPage > 1) url.set('page', newPage.toString());
|
||||
else url.delete('page'); // biar page=1 ga muncul di URL
|
||||
|
||||
router.replace(`?${url.toString()}`);
|
||||
}, [search, page, router]);
|
||||
};
|
||||
|
||||
const featuredData = featured.data;
|
||||
const paginatedNews = state.findMany.data || [];
|
||||
@@ -51,7 +56,7 @@ function Semua() {
|
||||
return (
|
||||
<Box py={20}>
|
||||
<Container size="xl" px={{ base: "md", md: "xl" }}>
|
||||
{/* === Berita Utama (Tetap) === */}
|
||||
{/* === Berita Utama === */}
|
||||
{loadingFeatured ? (
|
||||
<Center><Skeleton h={400} /></Center>
|
||||
) : featuredData ? (
|
||||
@@ -94,7 +99,9 @@ function Semua() {
|
||||
<Button
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() => router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/berita/${featuredData.kategoriBerita?.name}/${featuredData.id}`)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
@@ -106,7 +113,7 @@ function Semua() {
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{/* === Berita Terbaru (Berubah Saat Pagination) === */}
|
||||
{/* === Berita Terbaru === */}
|
||||
<Box mt={50}>
|
||||
<Title order={2} mb="md">Berita Terbaru</Title>
|
||||
<Divider mb="xl" />
|
||||
@@ -122,13 +129,7 @@ function Semua() {
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="xl" verticalSpacing="xl">
|
||||
{paginatedNews.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
shadow="sm"
|
||||
p="lg"
|
||||
radius="md"
|
||||
withBorder
|
||||
>
|
||||
<Card key={item.id} shadow="sm" p="lg" radius="md" withBorder>
|
||||
<Card.Section>
|
||||
<Image
|
||||
src={item.image?.link || '/images/placeholder-small.jpg'}
|
||||
@@ -143,7 +144,6 @@ function Semua() {
|
||||
</Badge>
|
||||
|
||||
<Text fw={600} size="lg" mt="sm" lineClamp={2}>{item.judul}</Text>
|
||||
|
||||
<Text size="sm" color="dimmed" lineClamp={3} mt="xs">{item.deskripsi}</Text>
|
||||
|
||||
<Flex align="center" justify="apart" mt="md" gap="xs">
|
||||
@@ -154,20 +154,28 @@ function Semua() {
|
||||
year: 'numeric'
|
||||
})}
|
||||
</Text>
|
||||
|
||||
<Button p="xs" variant="light" rightSection={<IconArrowRight size={16} />} onClick={() => router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)}>Baca Selengkapnya</Button>
|
||||
<Button
|
||||
p="xs"
|
||||
variant="light"
|
||||
rightSection={<IconArrowRight size={16} />}
|
||||
onClick={() =>
|
||||
router.push(`/darmasaba/desa/berita/${item.kategoriBerita?.name}/${item.id}`)
|
||||
}
|
||||
>
|
||||
Baca Selengkapnya
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{/* Pagination hanya untuk berita terbaru */}
|
||||
{/* Pagination */}
|
||||
<Center mt="xl">
|
||||
<Pagination
|
||||
total={totalPages}
|
||||
value={page}
|
||||
onChange={setPage}
|
||||
onChange={handlePageChange}
|
||||
siblings={1}
|
||||
boundaries={1}
|
||||
withEdges
|
||||
@@ -179,4 +187,4 @@ function Semua() {
|
||||
);
|
||||
}
|
||||
|
||||
export default Semua;
|
||||
export default Semua;
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client';
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import { Box, Button, Center, Container, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Tooltip, ActionIcon } from '@mantine/core';
|
||||
import { IconChalkboard, IconMicroscope, IconProps, IconRefresh, IconSchool, IconInfoCircle } from '@tabler/icons-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
|
||||
|
||||
interface Stat {
|
||||
jenjangPendidikan: any;
|
||||
icon: React.ComponentType<IconProps>;
|
||||
jumlah: number;
|
||||
nama: string;
|
||||
helper?: string;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export default function KategoriPage({ jenjangPendidikan }: { jenjangPendidikan: string }) {
|
||||
const router = useTransitionRouter();
|
||||
const [stats, setStats] = useState<Stat[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Decode the URL parameter
|
||||
const decodedJenjangPendidikan = decodeURIComponent(jenjangPendidikan);
|
||||
const jenjangFilter = decodedJenjangPendidikan.toLowerCase() === 'semua'
|
||||
? undefined
|
||||
: decodedJenjangPendidikan;
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
if (!decodedJenjangPendidikan) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// Load all data in parallel with the jenjang filter
|
||||
await Promise.all([
|
||||
infoSekolahPaud.lembagaPendidikan.findMany.load(1, 100, '', jenjangFilter),
|
||||
infoSekolahPaud.siswa.findMany.load(1, 100, '', jenjangFilter),
|
||||
infoSekolahPaud.pengajar.findMany.load(1, 100, '', jenjangFilter),
|
||||
]);
|
||||
|
||||
// Get filtered totals based on jenjang
|
||||
const totalLembaga = infoSekolahPaud.lembagaPendidikan.findMany.total || 0;
|
||||
const totalSiswa = infoSekolahPaud.siswa.findMany.total || 0;
|
||||
const totalPengajar = infoSekolahPaud.pengajar.findMany.total || 0;
|
||||
|
||||
setStats([
|
||||
{
|
||||
|
||||
icon: IconChalkboard,
|
||||
jumlah: totalLembaga,
|
||||
nama: 'Lembaga Pendidikan',
|
||||
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
|
||||
loading: false,
|
||||
jenjangPendidikan: decodedJenjangPendidikan,
|
||||
},
|
||||
{
|
||||
icon: IconSchool,
|
||||
jumlah: totalSiswa,
|
||||
nama: 'Siswa Terdaftar',
|
||||
helper: 'Total siswa aktif di semua jenjang',
|
||||
loading: false,
|
||||
jenjangPendidikan: decodedJenjangPendidikan,
|
||||
},
|
||||
{
|
||||
icon: IconMicroscope,
|
||||
jumlah: totalPengajar,
|
||||
nama: 'Tenaga Pengajar',
|
||||
helper: 'Jumlah guru dan staf pengajar aktif',
|
||||
loading: false,
|
||||
jenjangPendidikan: decodedJenjangPendidikan,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
// Set error state or show toast notification
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [decodedJenjangPendidikan, jenjangFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData, decodedJenjangPendidikan]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
const hasilCount = stats.reduce((sum, stat) => sum + stat.jumlah, 0);
|
||||
const filtered = stats;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box style={{ minHeight: '100vh', background: '#f8fafc', padding: '48px 0' }}>
|
||||
<Container size="xl">
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} height={260} radius="lg" />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
|
||||
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
<Box>
|
||||
<Group justify="space-between" mb="md">
|
||||
<Box aria-live="polite" aria-atomic>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Menampilkan <Text component="span" c="#0f172a" fw={700}>{hasilCount}</Text> hasil.
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleRefresh}
|
||||
loading={stats.some(stat => stat.loading)}
|
||||
>
|
||||
Segarkan Data
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{filtered.length === 0 ? (
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="md"
|
||||
style={{
|
||||
background: '#f9fafb',
|
||||
border: '1px dashed #e2e8f0',
|
||||
minHeight: 220,
|
||||
}}
|
||||
role="status"
|
||||
aria-label="Tidak ada hasil"
|
||||
>
|
||||
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
|
||||
<Text fz="lg" fw={800} c="#2563eb">
|
||||
Tidak ditemukan
|
||||
</Text>
|
||||
<Text c="dimmed" mt="6px">
|
||||
Coba gunakan kata kunci lain atau setel ulang filter.
|
||||
</Text>
|
||||
<Button
|
||||
mt="md"
|
||||
radius="xl"
|
||||
onClick={handleRefresh}
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
|
||||
}}
|
||||
aria-label="Tampilkan semua"
|
||||
>
|
||||
Tampilkan Semua
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
) : (
|
||||
filtered.map((v) => (
|
||||
<motion.div
|
||||
key={v.nama}
|
||||
whileHover={{ scale: 1.025 }}
|
||||
whileTap={{ scale: 0.995 }}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Skeleton visible={v.loading}>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
|
||||
minHeight: 260,
|
||||
}}
|
||||
role="article"
|
||||
aria-label={`${v.nama} kartu statistik`}
|
||||
>
|
||||
<Stack gap="sm" mb="md">
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#eff6ff',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: '#2563eb',
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
<Group justify="center" align="center" gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Group gap={6} align="center">
|
||||
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group justify="center" mt="8px">
|
||||
<Button
|
||||
radius="xl"
|
||||
variant="outline"
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#2563eb',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/lembaga`);
|
||||
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/siswa`);
|
||||
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/${jenjangPendidikan}/pengajar`);
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Skeleton>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { use } from 'react';
|
||||
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ jenjangPendidikan: string }>
|
||||
}
|
||||
|
||||
function Page({ params }: PageProps) {
|
||||
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
|
||||
const { jenjangPendidikan } = use(params);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
// Decode the URL parameter and pass it to load
|
||||
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
|
||||
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang)
|
||||
}, [page, jenjangPendidikan])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data lembaga pendidikan</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="60%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fw={500}>{item.nama}</TableTd>
|
||||
<TableTd>{item.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,14 @@
|
||||
// src/app/darmasaba/(pages)/desa/berita/[kategori]/page.tsx
|
||||
import { Suspense } from "react";
|
||||
import Content from "./content";
|
||||
|
||||
|
||||
export default async function Page({ params }: { params: Promise<{ jenjangPendidikan: string }> }) {
|
||||
const { jenjangPendidikan } = await params;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Content jenjangPendidikan={jenjangPendidikan} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { use } from 'react';
|
||||
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ jenjangPendidikan: string }>
|
||||
}
|
||||
|
||||
function Page({ params }: PageProps) {
|
||||
const stateList = useProxy(infoSekolahPaud.pengajar)
|
||||
const { jenjangPendidikan } = use(params);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
// Decode the URL parameter and pass it to load
|
||||
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
|
||||
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang)
|
||||
}, [page, jenjangPendidikan])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Pengajar</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data pengajar</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="30%">Nama Pengajar</TableTh>
|
||||
<TableTh w="60%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fw={500}>{item.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,111 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
import { use } from 'react';
|
||||
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ jenjangPendidikan: string }>
|
||||
}
|
||||
|
||||
function Page({ params }: PageProps) {
|
||||
const stateList = useProxy(infoSekolahPaud.siswa)
|
||||
const { jenjangPendidikan } = use(params);
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
// Decode the URL parameter and pass it to load
|
||||
const decodedJenjang = decodeURIComponent(jenjangPendidikan).toLowerCase()
|
||||
load(page, 10, '', decodedJenjang === 'semua' ? '' : decodedJenjang)
|
||||
}, [page, jenjangPendidikan])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Siswa</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data siswa</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="30%">Nama Siswa</TableTh>
|
||||
<TableTh w="60%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fw={500}>{item.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
import colors from '@/con/colors';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
VisuallyHidden,
|
||||
} from '@mantine/core';
|
||||
import { IconArrowLeft, IconSearch } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
type LayoutSekolahProps = {
|
||||
title?: string;
|
||||
jenjangPendidikanList?: string[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function LayoutSekolah({
|
||||
title = 'Cari Informasi Sekolah',
|
||||
jenjangPendidikanList = ['Semua', 'TK', 'SD', 'SMP', 'SMA'],
|
||||
children,
|
||||
}: LayoutSekolahProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const initialQuery = searchParams.get('search') || '';
|
||||
const initialJenjangPendidikan = searchParams.get('jenjangPendidikan') || 'Semua';
|
||||
|
||||
const [query, setQuery] = useState(initialQuery);
|
||||
const [jenjangPendidikanAktif, setJenjangPendidikanAktif] = useState(initialJenjangPendidikan);
|
||||
const [searchTimeout, setSearchTimeout] = useState<number | null>(null);
|
||||
|
||||
// Cleanup timeout
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
};
|
||||
}, [searchTimeout]);
|
||||
|
||||
// Handle Search with debounce
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
|
||||
setQuery(val);
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
const t = window.setTimeout(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (val) params.set('search', val);
|
||||
else params.delete('search');
|
||||
params.set('jenjangPendidikan', jenjangPendidikanAktif);
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
}, 500);
|
||||
setSearchTimeout(t);
|
||||
};
|
||||
|
||||
// Handle jenjang pendidikan click
|
||||
const handleJenjangPendidikanChange = (k: string) => {
|
||||
// arahkan langsung ke route jenjang pendidikan
|
||||
if (k.toLowerCase() === 'semua') {
|
||||
setJenjangPendidikanAktif(k);
|
||||
router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua`);
|
||||
} else {
|
||||
setJenjangPendidikanAktif(k);
|
||||
router.push(`/darmasaba/pendidikan/info-sekolah-paud/${encodeURIComponent(k.toLowerCase())}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Box style={{ minHeight: '100vh', background: colors.Bg, paddingBottom: 48 }}>
|
||||
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
<Stack gap="lg">
|
||||
{/* Back Button */}
|
||||
<ActionIcon onClick={() => window.history.back()} variant="light" radius="md" size="lg">
|
||||
<IconArrowLeft size={20} />
|
||||
<VisuallyHidden>Kembali</VisuallyHidden>
|
||||
</ActionIcon>
|
||||
|
||||
{/* Search & Filter */}
|
||||
<Paper radius="lg" p="xl" withBorder>
|
||||
<Stack gap="md">
|
||||
<Text ta="center" fw={800} fz={28}>{title}</Text>
|
||||
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
placeholder="Cari sekolah..."
|
||||
leftSection={<IconSearch size={18} />}
|
||||
radius="xl"
|
||||
size="md"
|
||||
/>
|
||||
|
||||
<Group justify="center" gap="xs" wrap="wrap">
|
||||
{jenjangPendidikanList.map((k) => {
|
||||
const aktif = k === jenjangPendidikanAktif;
|
||||
return (
|
||||
<Button
|
||||
key={k}
|
||||
onClick={() => handleJenjangPendidikanChange(k)}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
variant={aktif ? 'filled' : 'light'}
|
||||
>
|
||||
{k}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Slot konten */}
|
||||
{children}
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
const LayoutSekolah = dynamic(
|
||||
() => import('./_lib/layoutTabs'),
|
||||
{ ssr: false }
|
||||
);
|
||||
|
||||
function Layout({children} : {children: React.ReactNode}) {
|
||||
return (
|
||||
<LayoutSekolah>
|
||||
{children}
|
||||
</LayoutSekolah>
|
||||
);
|
||||
}
|
||||
|
||||
export default Layout;
|
||||
@@ -1,363 +0,0 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
Progress,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Tooltip,
|
||||
VisuallyHidden,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconChalkboard,
|
||||
IconInfoCircle,
|
||||
IconMicroscope,
|
||||
IconSchool,
|
||||
IconSearch,
|
||||
IconArrowLeft,
|
||||
} from '@tabler/icons-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { IconProps } from '@tabler/icons-react';
|
||||
|
||||
type Stat = {
|
||||
id: number;
|
||||
icon: React.ComponentType<IconProps>;
|
||||
jumlah: number;
|
||||
nama: string;
|
||||
helper?: string;
|
||||
};
|
||||
|
||||
const dataSekolah: Stat[] = [
|
||||
{
|
||||
id: 1,
|
||||
icon: IconChalkboard,
|
||||
jumlah: 15,
|
||||
nama: 'Lembaga Pendidikan',
|
||||
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: IconSchool,
|
||||
jumlah: 3209,
|
||||
nama: 'Siswa Terdaftar',
|
||||
helper: 'Total siswa aktif di semua jenjang',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: IconMicroscope,
|
||||
jumlah: 285,
|
||||
nama: 'Tenaga Pengajar',
|
||||
helper: 'Jumlah guru dan staf pengajar aktif',
|
||||
},
|
||||
];
|
||||
|
||||
export default function SekolahPage() {
|
||||
const [query, setQuery] = useState('');
|
||||
const [kategoriAktif, setKategoriAktif] = useState('Semua');
|
||||
const kategoriList = ['Semua', 'TK/PAUD', 'SD', 'SMP', 'SMA/SMK'];
|
||||
const maxJumlah = useMemo(() => Math.max(...dataSekolah.map((d) => d.jumlah)), []);
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return dataSekolah.filter((d) => {
|
||||
const teks = `${d.nama} ${d.jumlah}`.toLowerCase();
|
||||
const matchQuery = q ? teks.includes(q) : true;
|
||||
return matchQuery;
|
||||
});
|
||||
}, [query, kategoriAktif]);
|
||||
|
||||
const hasilCount = filtered.length;
|
||||
|
||||
return (
|
||||
<Box style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
|
||||
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
<Stack gap="lg">
|
||||
<Box>
|
||||
<ActionIcon
|
||||
aria-label="Kembali"
|
||||
onClick={() => window.history.back()}
|
||||
size="lg"
|
||||
radius="md"
|
||||
variant="light"
|
||||
style={{
|
||||
color: '#1e293b',
|
||||
background: 'white',
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
>
|
||||
<IconArrowLeft size={20} stroke={2} />
|
||||
<VisuallyHidden>Tombol kembali</VisuallyHidden>
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
|
||||
<Paper
|
||||
radius="lg"
|
||||
p={{ base: 'md', md: 'xl' }}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #ffffff 0%, #f1f5f9 100%)',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 6px 24px rgba(0,0,0,0.06)',
|
||||
}}
|
||||
role="search"
|
||||
aria-label="Pencarian sekolah"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Center>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.45, ease: 'easeOut' }}
|
||||
>
|
||||
<Text
|
||||
ta="center"
|
||||
c="#0f172a"
|
||||
fz={{ base: 22, md: 30 }}
|
||||
fw={800}
|
||||
style={{ letterSpacing: -0.3 }}
|
||||
>
|
||||
Cari Informasi Sekolah
|
||||
</Text>
|
||||
<Text ta="center" c="dimmed" fz="sm" mt={6}>
|
||||
Masukkan nama, jenjang, atau alamat sekolah untuk hasil lebih spesifik.
|
||||
</Text>
|
||||
</motion.div>
|
||||
</Center>
|
||||
|
||||
<Group align="center" justify="center" gap="sm" style={{ width: '100%' }}>
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||
placeholder="Contoh: SMP Negeri, SD 01, Kelurahan..."
|
||||
leftSection={<IconSearch size={18} aria-hidden />}
|
||||
aria-label="Masukkan kata kunci pencarian"
|
||||
radius="xl"
|
||||
size="md"
|
||||
rightSection={
|
||||
<Button
|
||||
radius="xl"
|
||||
size="sm"
|
||||
aria-label="Telusuri"
|
||||
onClick={() => {}}
|
||||
style={{
|
||||
height: 38,
|
||||
minWidth: 110,
|
||||
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 4px 16px rgba(59,130,246,0.3)',
|
||||
}}
|
||||
>
|
||||
Telusuri
|
||||
</Button>
|
||||
}
|
||||
rightSectionWidth={120}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 920,
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<Group justify="center" gap="xs" wrap="wrap" style={{ marginTop: 4 }}>
|
||||
{kategoriList.map((k) => {
|
||||
const aktif = k === kategoriAktif;
|
||||
return (
|
||||
<motion.div
|
||||
key={k}
|
||||
initial={{ scale: 0.98, opacity: 0.9 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
>
|
||||
<Button
|
||||
onClick={() => setKategoriAktif(k)}
|
||||
radius="xl"
|
||||
size="sm"
|
||||
variant={aktif ? 'filled' : 'light'}
|
||||
style={{
|
||||
background: aktif
|
||||
? 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'
|
||||
: 'white',
|
||||
color: aktif ? 'white' : '#2563eb',
|
||||
boxShadow: aktif ? '0 4px 16px rgba(59,130,246,0.25)' : 'none',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
{k}
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
<Box aria-live="polite" aria-atomic>
|
||||
<Text fz="sm" c="dimmed">
|
||||
Menampilkan <Text component="span" c="#0f172a" fw={700}>{hasilCount}</Text> hasil.
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{filtered.length === 0 ? (
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="md"
|
||||
style={{
|
||||
background: '#f9fafb',
|
||||
border: '1px dashed #e2e8f0',
|
||||
minHeight: 220,
|
||||
}}
|
||||
role="status"
|
||||
aria-label="Tidak ada hasil"
|
||||
>
|
||||
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
|
||||
<Text fz="lg" fw={800} c="#2563eb">
|
||||
Tidak ditemukan
|
||||
</Text>
|
||||
<Text c="dimmed" mt="6px">
|
||||
Coba gunakan kata kunci lain atau setel ulang filter.
|
||||
</Text>
|
||||
<Button
|
||||
mt="md"
|
||||
radius="xl"
|
||||
onClick={() => {
|
||||
setQuery('');
|
||||
setKategoriAktif('Semua');
|
||||
}}
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
|
||||
}}
|
||||
aria-label="Tampilkan semua"
|
||||
>
|
||||
Tampilkan Semua
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
) : (
|
||||
filtered.map((v) => {
|
||||
const percent = Math.round((v.jumlah / maxJumlah) * 100) || 0;
|
||||
return (
|
||||
<motion.div
|
||||
key={v.id}
|
||||
whileHover={{ scale: 1.025 }}
|
||||
whileTap={{ scale: 0.995 }}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
|
||||
minHeight: 260,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
role="article"
|
||||
aria-label={`${v.nama} kartu statistik`}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#eff6ff',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: '#2563eb',
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
<Group justify="apart" align="center" gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Group gap={6} align="center">
|
||||
<Text fz="sm" fw={700} c="#2563eb">
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Badge
|
||||
radius="md"
|
||||
variant="light"
|
||||
style={{
|
||||
background: '#eff6ff',
|
||||
color: '#2563eb',
|
||||
border: '1px solid #e2e8f0',
|
||||
}}
|
||||
>
|
||||
Statistik
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Box>
|
||||
<Progress
|
||||
value={percent}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
aria-label={`${v.nama} progres ${percent} persen`}
|
||||
/>
|
||||
<Text fz="xs" c="dimmed" mt="6px">
|
||||
Perbandingan dengan jumlah terbesar.
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Group justify="right" mt="8px">
|
||||
<Button
|
||||
radius="xl"
|
||||
variant="outline"
|
||||
onClick={() => {}}
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#2563eb',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
const stateList = useProxy(infoSekolahPaud.lembagaPendidikan)
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10)
|
||||
}, [page])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Lembaga Pendidikan</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data lembaga pendidikan</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="60%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd fw={500}>{item.nama}</TableTd>
|
||||
<TableTd>{item.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,271 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import type { IconProps } from '@tabler/icons-react';
|
||||
import {
|
||||
IconChalkboard,
|
||||
IconInfoCircle,
|
||||
IconMicroscope,
|
||||
IconRefresh,
|
||||
IconSchool,
|
||||
} from '@tabler/icons-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useTransitionRouter } from 'next-view-transitions';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
type Stat = {
|
||||
icon: React.ComponentType<IconProps>;
|
||||
jumlah: number;
|
||||
nama: string;
|
||||
helper?: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export default function SekolahPage() {
|
||||
const [stats, setStats] = useState<Stat[]>([
|
||||
{
|
||||
icon: IconChalkboard,
|
||||
jumlah: 0,
|
||||
nama: 'Lembaga Pendidikan',
|
||||
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
|
||||
loading: true,
|
||||
},
|
||||
{
|
||||
icon: IconSchool,
|
||||
jumlah: 0,
|
||||
nama: 'Siswa Terdaftar',
|
||||
helper: 'Total siswa aktif di semua jenjang',
|
||||
loading: true,
|
||||
},
|
||||
{
|
||||
icon: IconMicroscope,
|
||||
jumlah: 0,
|
||||
nama: 'Tenaga Pengajar',
|
||||
helper: 'Jumlah guru dan staf pengajar aktif',
|
||||
loading: true,
|
||||
},
|
||||
]);
|
||||
const router = useTransitionRouter()
|
||||
const stateLembaga = useProxy(infoSekolahPaud.lembagaPendidikan);
|
||||
const stateSiswa = useProxy(infoSekolahPaud.siswa);
|
||||
const statePengajar = useProxy(infoSekolahPaud.pengajar);
|
||||
const loadData = async () => {
|
||||
try {
|
||||
// Load lembaga data
|
||||
await stateLembaga.findMany.load(1, 1, '');
|
||||
const totalLembaga = stateLembaga.findMany.total || 0;
|
||||
|
||||
// Load siswa data
|
||||
await stateSiswa.findMany.load(1, 1, '');
|
||||
const totalSiswa = stateSiswa.findMany.total || 0;
|
||||
|
||||
// Load pengajar data
|
||||
await statePengajar.findMany.load(1, 1, '');
|
||||
const totalPengajar = statePengajar.findMany.total || 0;
|
||||
|
||||
setStats([
|
||||
{
|
||||
icon: IconChalkboard,
|
||||
jumlah: totalLembaga,
|
||||
nama: 'Lembaga Pendidikan',
|
||||
helper: 'Jumlah institusi pendidikan resmi di wilayah ini',
|
||||
loading: false,
|
||||
},
|
||||
{
|
||||
icon: IconSchool,
|
||||
jumlah: totalSiswa,
|
||||
nama: 'Siswa Terdaftar',
|
||||
helper: 'Total siswa aktif di semua jenjang',
|
||||
loading: false,
|
||||
},
|
||||
{
|
||||
icon: IconMicroscope,
|
||||
jumlah: totalPengajar,
|
||||
nama: 'Tenaga Pengajar',
|
||||
helper: 'Jumlah guru dan staf pengajar aktif',
|
||||
loading: false,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
// Set error state or show toast notification
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setStats(prev => prev.map(stat => ({ ...stat, loading: true })));
|
||||
loadData();
|
||||
};
|
||||
const [query] = useState('');
|
||||
|
||||
const filtered = stats.filter((d) => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return true;
|
||||
const teks = `${d.nama} ${d.jumlah}`.toLowerCase();
|
||||
return teks.includes(q);
|
||||
});
|
||||
|
||||
return (
|
||||
<Paper radius="md" style={{ minHeight: '100vh', background: '#f8fafc', paddingBottom: 48 }}>
|
||||
<Container size="xl" py={{ base: 'md', md: 'xl' }}>
|
||||
<Box>
|
||||
<Group justify="start" mb="md">
|
||||
<Button
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={handleRefresh}
|
||||
loading={stats.some(stat => stat.loading)}
|
||||
>
|
||||
Segarkan Data
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="lg">
|
||||
{filtered.length === 0 ? (
|
||||
<Paper
|
||||
p="xl"
|
||||
radius="md"
|
||||
style={{
|
||||
background: '#f9fafb',
|
||||
border: '1px dashed #e2e8f0',
|
||||
minHeight: 220,
|
||||
}}
|
||||
role="status"
|
||||
aria-label="Tidak ada hasil"
|
||||
>
|
||||
<Center style={{ minHeight: 180, flexDirection: 'column' }}>
|
||||
<Text fz="lg" fw={800} c="#2563eb">
|
||||
Tidak ditemukan
|
||||
</Text>
|
||||
<Text c="dimmed" mt="6px">
|
||||
Coba gunakan kata kunci lain atau setel ulang filter.
|
||||
</Text>
|
||||
<Button
|
||||
mt="md"
|
||||
radius="xl"
|
||||
onClick={handleRefresh}
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%)',
|
||||
color: 'white',
|
||||
boxShadow: '0 6px 18px rgba(59,130,246,0.25)',
|
||||
}}
|
||||
aria-label="Tampilkan semua"
|
||||
>
|
||||
Tampilkan Semua
|
||||
</Button>
|
||||
</Center>
|
||||
</Paper>
|
||||
) : (
|
||||
filtered.map((v) => (
|
||||
<motion.div
|
||||
key={v.nama}
|
||||
whileHover={{ scale: 1.025 }}
|
||||
whileTap={{ scale: 0.995 }}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Skeleton visible={v.loading}>
|
||||
<Paper
|
||||
p="lg"
|
||||
radius="lg"
|
||||
style={{
|
||||
background: 'white',
|
||||
border: '1px solid #e2e8f0',
|
||||
boxShadow: '0 8px 28px rgba(0,0,0,0.06)',
|
||||
minHeight: 260,
|
||||
}}
|
||||
role="article"
|
||||
aria-label={`${v.nama} kartu statistik`}
|
||||
>
|
||||
<Stack gap="sm" mb="md">
|
||||
<Center>
|
||||
<Box
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 16,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#eff6ff',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.6)',
|
||||
}}
|
||||
aria-hidden
|
||||
>
|
||||
{React.createElement(v.icon, {
|
||||
color: '#2563eb',
|
||||
size: 34,
|
||||
stroke: 1.6,
|
||||
})}
|
||||
</Box>
|
||||
</Center>
|
||||
|
||||
<Group justify="center" align="center" gap="xs">
|
||||
<Stack gap={0}>
|
||||
<Text ta={"center"} fz={{ base: 18, md: 22 }} fw={800} c="#0f172a">
|
||||
{v.jumlah.toLocaleString()}
|
||||
</Text>
|
||||
<Group gap={6} align="center">
|
||||
<Text ta={"center"} fz="sm" fw={700} c="#2563eb">
|
||||
{v.nama}
|
||||
</Text>
|
||||
<Tooltip label={v.helper ?? ''} position="right" withArrow>
|
||||
<ActionIcon aria-label={`Info ${v.nama}`} variant="transparent" size="xs">
|
||||
<IconInfoCircle size={16} style={{ color: '#2563eb' }} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group justify="center" mt="8px">
|
||||
<Button
|
||||
radius="xl"
|
||||
variant="outline"
|
||||
aria-label={`Lihat detail ${v.nama}`}
|
||||
style={{
|
||||
borderColor: '#e2e8f0',
|
||||
color: '#2563eb',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (v.nama === "Lembaga Pendidikan") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/lembaga`);
|
||||
if (v.nama === "Siswa Terdaftar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/siswa`);
|
||||
if (v.nama === "Tenaga Pengajar") router.push(`/darmasaba/pendidikan/info-sekolah-paud/semua/pengajar`);
|
||||
}}
|
||||
>
|
||||
Lihat Detail
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
</Skeleton>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</Container>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
const stateList = useProxy(infoSekolahPaud.pengajar)
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10)
|
||||
}, [page])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Pengajar</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data pengajar</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="30%">Nama Pengajar</TableTh>
|
||||
<TableTh w="30%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,102 @@
|
||||
'use client'
|
||||
import infoSekolahPaud from '@/app/admin/(dashboard)/_state/pendidikan/info-sekolah-paud';
|
||||
import colors from '@/con/colors';
|
||||
import { Box, Center, Group, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||
import { IconSchool, IconLayersSubtract } from '@tabler/icons-react';
|
||||
import { useShallowEffect } from '@mantine/hooks';
|
||||
import { useProxy } from 'valtio/utils';
|
||||
|
||||
function Page() {
|
||||
const stateList = useProxy(infoSekolahPaud.siswa)
|
||||
|
||||
const {
|
||||
data,
|
||||
page,
|
||||
totalPages,
|
||||
loading,
|
||||
load,
|
||||
} = stateList.findMany
|
||||
|
||||
useShallowEffect(() => {
|
||||
load(page, 10)
|
||||
}, [page])
|
||||
|
||||
const filteredData = data || []
|
||||
|
||||
if (loading || !data) {
|
||||
return (
|
||||
<Stack py="lg" gap="md">
|
||||
<Skeleton h={40} radius="xl" />
|
||||
<Skeleton h={500} radius="md" />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box py="lg">
|
||||
<Paper
|
||||
bg={colors['white-1']}
|
||||
p="lg"
|
||||
radius="xl"
|
||||
shadow="sm"
|
||||
withBorder
|
||||
>
|
||||
<Group justify="space-between" align="center" mb="md">
|
||||
<Group gap="sm">
|
||||
<IconSchool size={28} stroke={1.5} color={colors['blue-button']} />
|
||||
<Title order={2} fz="xl">Daftar Siswa</Title>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{filteredData.length === 0 ? (
|
||||
<Stack align="center" justify="center" py="xl" gap="sm">
|
||||
<IconLayersSubtract size={48} stroke={1.5} color={"gray"} />
|
||||
<Text fz="lg" c="dimmed">Belum ada data siswa</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
horizontalSpacing="md"
|
||||
verticalSpacing="sm"
|
||||
fz="sm"
|
||||
>
|
||||
<TableThead>
|
||||
<TableTr>
|
||||
<TableTh w="30%">Nama Siswa</TableTh>
|
||||
<TableTh w="30%">Nama Lembaga</TableTh>
|
||||
<TableTh w="40%">Jenjang Pendidikan</TableTh>
|
||||
</TableTr>
|
||||
</TableThead>
|
||||
<TableTbody>
|
||||
{filteredData.map((item) => (
|
||||
<TableTr key={item.id}>
|
||||
<TableTd>{item.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.nama}</TableTd>
|
||||
<TableTd>{item.lembaga.jenjangPendidikan?.nama || '-'}</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
</TableTbody>
|
||||
</Table>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{filteredData.length > 0 && (
|
||||
<Center mt="lg">
|
||||
<Pagination
|
||||
value={page}
|
||||
onChange={(newPage) => load(newPage)}
|
||||
total={totalPages}
|
||||
size="md"
|
||||
radius="xl"
|
||||
withEdges
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -307,7 +307,7 @@ const navbarListMenu = [
|
||||
{
|
||||
id: "8.1",
|
||||
name: "Info Sekolah & PAUD",
|
||||
href: "/darmasaba/pendidikan/info-sekolah-paud"
|
||||
href: "/darmasaba/pendidikan/info-sekolah-paud/semua"
|
||||
},
|
||||
{
|
||||
id: "8.2",
|
||||
|
||||
Reference in New Issue
Block a user