Sinkroniasasi Admin - User, Submenu Info Sekolah Paud

This commit is contained in:
2025-08-29 01:31:05 +08:00
parent a8fd715822
commit b6d6583e77
18 changed files with 1592 additions and 485 deletions

View File

@@ -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({

View File

@@ -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'}`,
};
}
}

View File

@@ -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;

View File

@@ -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'}`,
};
}
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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",