Bisa yuk
This commit is contained in:
@@ -29,78 +29,130 @@ export async function OPTIONS() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
// Ambil parameter tahun angkatan dari query string
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const tahunAngkatan = searchParams.get('tahun_angkatan');
|
||||||
|
|
||||||
// Jumlah mahasiswa
|
// Jumlah mahasiswa
|
||||||
const { count: totalMahasiswa } = await supabase
|
let totalMahasiswaQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true });
|
.select('*', { count: 'exact', head: true });
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
totalMahasiswaQuery = totalMahasiswaQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalMahasiswa } = await totalMahasiswaQuery;
|
||||||
|
|
||||||
// Ambil mahasiswa aktif
|
// Ambil mahasiswa aktif
|
||||||
const { count: mahasiswaAktif } = await supabase
|
let mahasiswaAktifQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.eq('status_kuliah', 'Aktif');
|
.eq('status_kuliah', 'Aktif');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
mahasiswaAktifQuery = mahasiswaAktifQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: mahasiswaAktif } = await mahasiswaAktifQuery;
|
||||||
|
|
||||||
// Ambil total lulus
|
// Ambil total lulus
|
||||||
const { count: totalLulus } = await supabase
|
let totalLulusQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.eq('status_kuliah', 'Lulus');
|
.eq('status_kuliah', 'Lulus');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
totalLulusQuery = totalLulusQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalLulus } = await totalLulusQuery;
|
||||||
|
|
||||||
// Ambil pria lulus
|
// Ambil pria lulus
|
||||||
const { count: priaLulus } = await supabase
|
let priaLulusQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.eq('status_kuliah', 'Lulus')
|
.eq('status_kuliah', 'Lulus')
|
||||||
.eq('jk', 'Pria');
|
.eq('jk', 'Pria');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
priaLulusQuery = priaLulusQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: priaLulus } = await priaLulusQuery;
|
||||||
|
|
||||||
// Ambil wanita lulus
|
// Ambil wanita lulus
|
||||||
const { count: wanitaLulus } = await supabase
|
let wanitaLulusQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.eq('status_kuliah', 'Lulus')
|
.eq('status_kuliah', 'Lulus')
|
||||||
.eq('jk', 'Wanita');
|
.eq('jk', 'Wanita');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
wanitaLulusQuery = wanitaLulusQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: wanitaLulus } = await wanitaLulusQuery;
|
||||||
|
|
||||||
// Ambil total berprestasi
|
// Ambil total berprestasi
|
||||||
const { count: totalBerprestasi } = await supabase
|
let prestasiTotalQuery = supabase
|
||||||
.from('prestasi_mahasiswa')
|
.from('prestasi_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true });
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true });
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
prestasiTotalQuery = prestasiTotalQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalBerprestasi } = await prestasiTotalQuery;
|
||||||
|
|
||||||
// Ambil prestasi akademik
|
// Ambil prestasi akademik
|
||||||
const { count: prestasiAkademik } = await supabase
|
let prestasiAkademikQuery = supabase
|
||||||
.from('prestasi_mahasiswa')
|
.from('prestasi_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true })
|
||||||
.eq('jenis_prestasi', 'Akademik');
|
.eq('jenis_prestasi', 'Akademik');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
prestasiAkademikQuery = prestasiAkademikQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: prestasiAkademik } = await prestasiAkademikQuery;
|
||||||
|
|
||||||
// Ambil prestasi non-akademik
|
// Ambil prestasi non-akademik
|
||||||
const { count: prestasiNonAkademik } = await supabase
|
let prestasiNonAkademikQuery = supabase
|
||||||
.from('prestasi_mahasiswa')
|
.from('prestasi_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true })
|
||||||
.eq('jenis_prestasi', 'Non-Akademik');
|
.eq('jenis_prestasi', 'Non-Akademik');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
prestasiNonAkademikQuery = prestasiNonAkademikQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: prestasiNonAkademik } = await prestasiNonAkademikQuery;
|
||||||
|
|
||||||
// Ambil total mahasiswa aktif + lulus
|
// Ambil total mahasiswa aktif + lulus
|
||||||
const { count: totalMahasiswaAktifLulus } = await supabase
|
let totalMahasiswaAktifLulusQuery = supabase
|
||||||
.from('mahasiswa')
|
.from('mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.in('status_kuliah', ['Aktif', 'Lulus']);
|
.in('status_kuliah', ['Aktif', 'Lulus']);
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
totalMahasiswaAktifLulusQuery = totalMahasiswaAktifLulusQuery.eq('tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalMahasiswaAktifLulus } = await totalMahasiswaAktifLulusQuery;
|
||||||
|
|
||||||
// Total mahasiswa beasiswa
|
// Total mahasiswa beasiswa
|
||||||
const { count: totalMahasiswaBeasiswa } = await supabase
|
let beasiswaTotalQuery = supabase
|
||||||
.from('beasiswa_mahasiswa')
|
.from('beasiswa_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true });
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true });
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
beasiswaTotalQuery = beasiswaTotalQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalMahasiswaBeasiswa } = await beasiswaTotalQuery;
|
||||||
|
|
||||||
// Total mahasiswa beasiswa Pemerintah
|
// Total mahasiswa beasiswa Pemerintah
|
||||||
const { count: totalMahasiswaBeasiswaPemerintah } = await supabase
|
let beasiswaPemerintahQuery = supabase
|
||||||
.from('beasiswa_mahasiswa')
|
.from('beasiswa_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true })
|
||||||
.eq('jenis_beasiswa', 'Pemerintah');
|
.eq('jenis_beasiswa', 'Pemerintah');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
beasiswaPemerintahQuery = beasiswaPemerintahQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalMahasiswaBeasiswaPemerintah } = await beasiswaPemerintahQuery;
|
||||||
|
|
||||||
// Total mahasiswa beasiswa Non-Pemerintah
|
// Total mahasiswa beasiswa Non-Pemerintah
|
||||||
const { count: totalMahasiswaBeasiswaNonPemerintah } = await supabase
|
let beasiswaNonPemerintahQuery = supabase
|
||||||
.from('beasiswa_mahasiswa')
|
.from('beasiswa_mahasiswa')
|
||||||
.select('*', { count: 'exact', head: true })
|
.select('*, mahasiswa!inner(tahun_angkatan)', { count: 'exact', head: true })
|
||||||
.eq('jenis_beasiswa', 'Non-Pemerintah');
|
.eq('jenis_beasiswa', 'Non-Pemerintah');
|
||||||
|
if (tahunAngkatan && tahunAngkatan !== 'all') {
|
||||||
|
beasiswaNonPemerintahQuery = beasiswaNonPemerintahQuery.eq('mahasiswa.tahun_angkatan', parseInt(tahunAngkatan));
|
||||||
|
}
|
||||||
|
const { count: totalMahasiswaBeasiswaNonPemerintah } = await beasiswaNonPemerintahQuery;
|
||||||
|
|
||||||
const results: MahasiswaTotal = {
|
const results: MahasiswaTotal = {
|
||||||
total_mahasiswa: totalMahasiswa || 0,
|
total_mahasiswa: totalMahasiswa || 0,
|
||||||
|
|||||||
@@ -32,11 +32,55 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { ChevronDown, Navigation, ArrowUp } from "lucide-react";
|
import { ChevronDown, Navigation, ArrowUp } from "lucide-react";
|
||||||
|
import DashboardStats, { DashboardStatsSkeleton } from "@/components/dashboard/DashboardStats";
|
||||||
|
|
||||||
|
interface MahasiswaTotal {
|
||||||
|
total_mahasiswa: number;
|
||||||
|
mahasiswa_aktif: number;
|
||||||
|
total_lulus: number;
|
||||||
|
pria_lulus: number;
|
||||||
|
wanita_lulus: number;
|
||||||
|
total_berprestasi: number;
|
||||||
|
prestasi_akademik: number;
|
||||||
|
prestasi_non_akademik: number;
|
||||||
|
total_mahasiswa_aktif_lulus: number;
|
||||||
|
total_mahasiswa_beasiswa: number;
|
||||||
|
total_mahasiswa_beasiswa_pemerintah: number;
|
||||||
|
total_mahasiswa_beasiswa_non_pemerintah: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default function TotalMahasiswaPage() {
|
export default function TotalMahasiswaPage() {
|
||||||
const [selectedYear, setSelectedYear] = useState<string>("all");
|
const [selectedYear, setSelectedYear] = useState<string>("all");
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||||
|
const [mahasiswaData, setMahasiswaData] = useState<MahasiswaTotal | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Fetch mahasiswa data based on selected year
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMahasiswaData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const url = selectedYear === "all"
|
||||||
|
? '/api/mahasiswa/total'
|
||||||
|
: `/api/mahasiswa/total?tahun_angkatan=${selectedYear}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch data');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setMahasiswaData(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching mahasiswa data:', error);
|
||||||
|
setMahasiswaData(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMahasiswaData();
|
||||||
|
}, [selectedYear]);
|
||||||
|
|
||||||
// Handle scroll event to show/hide back to top button
|
// Handle scroll event to show/hide back to top button
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -107,6 +151,17 @@ export default function TotalMahasiswaPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dashboard Stats Cards */}
|
||||||
|
{loading ? (
|
||||||
|
<DashboardStatsSkeleton />
|
||||||
|
) : mahasiswaData ? (
|
||||||
|
<DashboardStats mahasiswaData={mahasiswaData} />
|
||||||
|
) : (
|
||||||
|
<div className="mb-8 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<p className="text-red-600 dark:text-red-400">Gagal memuat data statistik mahasiswa</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:space-x-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4 sm:space-x-4">
|
||||||
<FilterTahunAngkatan
|
<FilterTahunAngkatan
|
||||||
|
|||||||
117
app/page.tsx
117
app/page.tsx
@@ -1,13 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Users, GraduationCap, Trophy, BookOpen } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
|
import DashboardStats, { DashboardStatsSkeleton } from "@/components/dashboard/DashboardStats";
|
||||||
import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart";
|
import DashboardCharts from "@/components/dashboard/DashboardCharts";
|
||||||
import AsalDaerahChart from "@/components/charts/AsalDaerahChart";
|
|
||||||
import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart";
|
|
||||||
|
|
||||||
interface MahasiswaTotal {
|
interface MahasiswaTotal {
|
||||||
total_mahasiswa: number;
|
total_mahasiswa: number;
|
||||||
@@ -24,22 +20,6 @@ interface MahasiswaTotal {
|
|||||||
total_mahasiswa_beasiswa_non_pemerintah: number;
|
total_mahasiswa_beasiswa_non_pemerintah: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skeleton loading component
|
|
||||||
const CardSkeleton = () => (
|
|
||||||
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
|
||||||
<div className="h-4 w-4 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-8 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2"></div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
|
||||||
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -108,12 +88,7 @@ export default function DashboardPage() {
|
|||||||
<h1 className="text-2xl font-bold mb-4">Visualisasi Data Akademik Mahasiswa Informatika</h1>
|
<h1 className="text-2xl font-bold mb-4">Visualisasi Data Akademik Mahasiswa Informatika</h1>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
|
<DashboardStatsSkeleton />
|
||||||
<CardSkeleton />
|
|
||||||
<CardSkeleton />
|
|
||||||
<CardSkeleton />
|
|
||||||
<CardSkeleton />
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
|
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@@ -129,89 +104,9 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
|
<DashboardStats mahasiswaData={mahasiswaData} />
|
||||||
{/* Kartu Total Mahasiswa */}
|
<DashboardCharts />
|
||||||
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
</>
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium dark:text-white">
|
|
||||||
Total Mahasiswa
|
|
||||||
</CardTitle>
|
|
||||||
<Users className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_mahasiswa}</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
|
||||||
<span className="dark:text-white">Aktif: <span className="text-green-500">{mahasiswaData.mahasiswa_aktif}</span></span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Kartu Total Kelulusan */}
|
|
||||||
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium dark:text-white">
|
|
||||||
Total Kelulusan
|
|
||||||
</CardTitle>
|
|
||||||
<GraduationCap className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_lulus}</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
|
||||||
<span className="dark:text-white">Laki-laki: <span className="text-blue-500">{mahasiswaData.pria_lulus}</span></span>
|
|
||||||
<span className="dark:text-white">Perempuan: <span className="text-pink-500">{mahasiswaData.wanita_lulus}</span></span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Kartu Total Prestasi */}
|
|
||||||
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium dark:text-white">
|
|
||||||
Mahasiswa Berprestasi
|
|
||||||
</CardTitle>
|
|
||||||
<Trophy className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_berprestasi}</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
|
||||||
<span className="dark:text-white">Akademik: <span className="text-yellow-500">{mahasiswaData.prestasi_akademik}</span></span>
|
|
||||||
<span className="dark:text-white">Non-Akademik: <span className="text-purple-500">{mahasiswaData.prestasi_non_akademik}</span></span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Kartu Mahasiswa Beasiswa */}
|
|
||||||
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium dark:text-white">
|
|
||||||
Mahasiswa Beasiswa
|
|
||||||
</CardTitle>
|
|
||||||
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_mahasiswa_beasiswa}</div>
|
|
||||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
|
||||||
<span className="dark:text-white">Pemerintah: <span className="text-green-500">{mahasiswaData.total_mahasiswa_beasiswa_pemerintah}</span></span>
|
|
||||||
<span className="dark:text-white">Non-Pemerintah: <span className="text-amber-400">{mahasiswaData.total_mahasiswa_beasiswa_non_pemerintah}</span></span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-1">
|
|
||||||
<StatistikMahasiswaChart />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1">
|
|
||||||
<JenisPendaftaranChart />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1">
|
|
||||||
<AsalDaerahChart />
|
|
||||||
</div>
|
|
||||||
<div className="col-span-1">
|
|
||||||
<ProvinsiMahasiswaChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
25
components/dashboard/DashboardCharts.tsx
Normal file
25
components/dashboard/DashboardCharts.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
|
||||||
|
import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart";
|
||||||
|
import AsalDaerahChart from "@/components/charts/AsalDaerahChart";
|
||||||
|
import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart";
|
||||||
|
|
||||||
|
export default function DashboardCharts() {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="col-span-1">
|
||||||
|
<StatistikMahasiswaChart />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<JenisPendaftaranChart />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<AsalDaerahChart />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-1">
|
||||||
|
<ProvinsiMahasiswaChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
117
components/dashboard/DashboardStats.tsx
Normal file
117
components/dashboard/DashboardStats.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Users, GraduationCap, Trophy, BookOpen } from "lucide-react";
|
||||||
|
|
||||||
|
// Skeleton loading component for stats cards
|
||||||
|
export const DashboardStatsSkeleton = () => (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<Card key={i} className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<div className="h-4 w-24 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
<div className="h-4 w-4 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="h-8 w-16 bg-gray-200 dark:bg-gray-700 rounded animate-pulse mb-2"></div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
<div className="h-3 w-20 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface MahasiswaTotal {
|
||||||
|
total_mahasiswa: number;
|
||||||
|
mahasiswa_aktif: number;
|
||||||
|
total_lulus: number;
|
||||||
|
pria_lulus: number;
|
||||||
|
wanita_lulus: number;
|
||||||
|
total_berprestasi: number;
|
||||||
|
prestasi_akademik: number;
|
||||||
|
prestasi_non_akademik: number;
|
||||||
|
total_mahasiswa_aktif_lulus: number;
|
||||||
|
total_mahasiswa_beasiswa: number;
|
||||||
|
total_mahasiswa_beasiswa_pemerintah: number;
|
||||||
|
total_mahasiswa_beasiswa_non_pemerintah: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DashboardStatsProps {
|
||||||
|
mahasiswaData: MahasiswaTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardStats({ mahasiswaData }: DashboardStatsProps) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 mb-4">
|
||||||
|
{/* Kartu Total Mahasiswa */}
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium dark:text-white">
|
||||||
|
Total Mahasiswa
|
||||||
|
</CardTitle>
|
||||||
|
<Users className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_mahasiswa}</div>
|
||||||
|
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span className="dark:text-white">Aktif: <span className="text-green-500">{mahasiswaData.mahasiswa_aktif}</span></span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Kartu Total Kelulusan */}
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium dark:text-white">
|
||||||
|
Total Kelulusan
|
||||||
|
</CardTitle>
|
||||||
|
<GraduationCap className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_lulus}</div>
|
||||||
|
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span className="dark:text-white">Laki-laki: <span className="text-blue-500">{mahasiswaData.pria_lulus}</span></span>
|
||||||
|
<span className="dark:text-white">Perempuan: <span className="text-pink-500">{mahasiswaData.wanita_lulus}</span></span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Kartu Total Prestasi */}
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium dark:text-white">
|
||||||
|
Mahasiswa Berprestasi
|
||||||
|
</CardTitle>
|
||||||
|
<Trophy className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_berprestasi}</div>
|
||||||
|
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span className="dark:text-white">Akademik: <span className="text-yellow-500">{mahasiswaData.prestasi_akademik}</span></span>
|
||||||
|
<span className="dark:text-white">Non-Akademik: <span className="text-purple-500">{mahasiswaData.prestasi_non_akademik}</span></span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Kartu Mahasiswa Beasiswa */}
|
||||||
|
<Card className="bg-white dark:bg-slate-900 shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium dark:text-white">
|
||||||
|
Mahasiswa Beasiswa
|
||||||
|
</CardTitle>
|
||||||
|
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold dark:text-white">{mahasiswaData.total_mahasiswa_beasiswa}</div>
|
||||||
|
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||||
|
<span className="dark:text-white">Pemerintah: <span className="text-green-500">{mahasiswaData.total_mahasiswa_beasiswa_pemerintah}</span></span>
|
||||||
|
<span className="dark:text-white">Non-Pemerintah: <span className="text-amber-400">{mahasiswaData.total_mahasiswa_beasiswa_non_pemerintah}</span></span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user