Coba Terus

This commit is contained in:
Randa Firman Putra
2025-08-23 22:45:23 +07:00
parent 744c3db464
commit be030b8510
8 changed files with 1007 additions and 19 deletions

View File

@@ -0,0 +1,77 @@
import { NextResponse } from 'next/server';
import supabase from '@/lib/db';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const tahunAngkatan = searchParams.get('tahunAngkatan');
let query = supabase
.from('beasiswa_mahasiswa')
.select(`
nama_beasiswa,
jenis_beasiswa,
mahasiswa!inner(
tahun_angkatan
)
`);
if (tahunAngkatan && tahunAngkatan !== 'all') {
query = query.eq('mahasiswa.tahun_angkatan', tahunAngkatan);
}
const { data, error } = await query;
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Database error' },
{ status: 500 }
);
}
// Group and count the data in JavaScript
const groupedData = data.reduce((acc: any[], row: any) => {
const tahunAngkatanValue = row.mahasiswa?.tahun_angkatan;
const namaBeasiswa = row.nama_beasiswa;
if (!namaBeasiswa || !tahunAngkatanValue) {
return acc;
}
const existingGroup = acc.find(
(item: any) =>
item.tahun_angkatan === tahunAngkatanValue &&
item.nama_beasiswa === namaBeasiswa
);
if (existingGroup) {
existingGroup.jumlah_nama_beasiswa++;
} else {
acc.push({
tahun_angkatan: tahunAngkatanValue,
nama_beasiswa: namaBeasiswa,
jumlah_nama_beasiswa: 1
});
}
return acc;
}, []);
// Sort the results by tahun_angkatan ascending (as expected by component)
const sortedData = groupedData.sort((a: any, b: any) => {
if (a.tahun_angkatan !== b.tahun_angkatan) {
return a.tahun_angkatan - b.tahun_angkatan;
}
return a.nama_beasiswa.localeCompare(b.nama_beasiswa);
});
return NextResponse.json(sortedData);
} catch (error) {
console.error('Error fetching data:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,60 @@
import { NextResponse } from 'next/server';
import supabase from '@/lib/db';
export async function GET(request: Request) {
try {
// Get all mahasiswa data and process in JavaScript
const { data: mahasiswaData, error } = await supabase
.from('mahasiswa')
.select('provinsi');
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Database error' },
{ status: 500 }
);
}
// Process the data in JavaScript
let kalimantanBarat = 0;
let luarKalimantanBarat = 0;
mahasiswaData.forEach((mahasiswa) => {
const provinsi = mahasiswa.provinsi?.toLowerCase() || '';
if (provinsi.includes('kalimantan barat') || provinsi.includes('kalbar')) {
kalimantanBarat++;
} else if (mahasiswa.provinsi) {
luarKalimantanBarat++;
}
});
// Transform the data to match the expected format
const result = [
{
provinsi: 'Kalimantan Barat',
jumlah_mahasiswa: kalimantanBarat
},
{
provinsi: 'Luar Kalimantan Barat',
jumlah_mahasiswa: luarKalimantanBarat
}
];
return NextResponse.json(result, {
headers: {
'Cache-Control': 'public, max-age=60, stale-while-revalidate=30',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
} catch (error) {
console.error('Error fetching provinsi data:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,91 @@
import { NextResponse } from 'next/server';
import supabase from '@/lib/db';
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const tahunAngkatan = searchParams.get('tahunAngkatan');
// Build query based on parameters
let query = supabase
.from('mahasiswa')
.select(`
tahun_angkatan,
prestasi_mahasiswa(
tingkat_prestasi,
jenis_prestasi
)
`);
// Add tahun angkatan filter if provided and not 'all'
if (tahunAngkatan && tahunAngkatan !== 'null' && tahunAngkatan !== 'undefined' && tahunAngkatan !== 'all') {
query = query.eq('tahun_angkatan', parseInt(tahunAngkatan));
}
const { data, error } = await query;
if (error) {
console.error('Supabase error:', error);
return NextResponse.json(
{ error: 'Database error' },
{ status: 500 }
);
}
// Group and count the data in JavaScript
const groupedData = data.reduce((acc: any[], row: any) => {
const tahunAngkatan = row.tahun_angkatan;
// Handle array of prestasi_mahasiswa
const prestasiArray = Array.isArray(row.prestasi_mahasiswa) ? row.prestasi_mahasiswa : [row.prestasi_mahasiswa];
if (!tahunAngkatan) {
return acc;
}
// Process each prestasi in the array
prestasiArray.forEach((prestasi: any) => {
if (!prestasi || !prestasi.tingkat_prestasi) {
return;
}
const tingkatPrestasi = prestasi.tingkat_prestasi;
const existingGroup = acc.find(
(item: any) =>
item.tahun_angkatan === tahunAngkatan &&
item.tingkat_prestasi === tingkatPrestasi
);
if (existingGroup) {
existingGroup.tingkat_mahasiswa_prestasi++;
} else {
const newGroup = {
tahun_angkatan: tahunAngkatan,
tingkat_prestasi: tingkatPrestasi,
tingkat_mahasiswa_prestasi: 1
};
acc.push(newGroup);
}
});
return acc;
}, []);
// Sort the results
const sortedData = groupedData.sort((a: any, b: any) => {
if (a.tahun_angkatan !== b.tahun_angkatan) {
return b.tahun_angkatan - a.tahun_angkatan;
}
return a.tingkat_prestasi.localeCompare(b.tingkat_prestasi);
});
return NextResponse.json(sortedData);
} catch (error) {
console.error('Error fetching data:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}

View File

@@ -14,6 +14,9 @@ import KelompokKeahlianStatusChart from "@/components/chartsDashboard/kkdashboar
import KelompokKeahlianLulusTepatPieChart from "@/components/chartsDashboard/kkdashboardtepatpiechart";
import MasaStudiAktifChart from "@/components/chartsDashboard/masastudiaktifchart";
import MasaStudiLulusChart from "@/components/chartsDashboard/masastudiluluschart";
import NamaBeasiswaChart from "@/components/chartsDashboard/NamaBeasiswaDashChart";
import TingkatPrestasiChart from "@/components/chartsDashboard/TingkatPrestasiDashChart";
import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function TotalMahasiswaPage() {
@@ -39,23 +42,56 @@ export default function TotalMahasiswaPage() {
</div>
{selectedYear === "all" ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatistikMahasiswaChart />
<JenisPendaftaranChart />
<StatusMahasiswaChart />
<IPKChart />
<KelompokKeahlianStatusChart selectedYear={selectedYear} />
<KelompokKeahlianLulusTepatPieChart />
<AsalDaerahChart />
<MasaStudiAktifChart selectedYear={selectedYear} />
<MasaStudiLulusChart selectedYear={selectedYear} />
<div className="space-y-6">
{/* Overview Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatistikMahasiswaChart />
<StatusMahasiswaChart />
</div>
{/* Academic Performance Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<IPKChart />
<JenisPendaftaranChart />
</div>
{/* Study Duration Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<MasaStudiAktifChart selectedYear={selectedYear} />
<MasaStudiLulusChart selectedYear={selectedYear} />
</div>
{/* Expertise & Achievement Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<KelompokKeahlianStatusChart selectedYear={selectedYear} />
<KelompokKeahlianLulusTepatPieChart />
</div>
{/* Scholarship & Achievement Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<NamaBeasiswaChart selectedYear={selectedYear} />
<TingkatPrestasiChart selectedYear={selectedYear} />
</div>
{/* Demographics Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<AsalDaerahChart />
<ProvinsiMahasiswaChart />
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatistikPerAngkatanChart tahunAngkatan={selectedYear} />
<JenisPendaftaranPerAngkatanChart tahunAngkatan={selectedYear} />
<AsalDaerahPerAngkatanChart tahunAngkatan={selectedYear} />
<KelompokKeahlianStatusChart selectedYear={selectedYear} />
<div className="space-y-6">
{/* Overview Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<StatistikPerAngkatanChart tahunAngkatan={selectedYear} />
<KelompokKeahlianStatusChart selectedYear={selectedYear} />
</div>
{/* Demographics Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<JenisPendaftaranPerAngkatanChart tahunAngkatan={selectedYear} />
<AsalDaerahPerAngkatanChart tahunAngkatan={selectedYear} />
</div>
</div>
)}
</div>

View File

@@ -0,0 +1,265 @@
'use client';
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { ApexOptions } from 'apexcharts';
import { useTheme } from 'next-themes';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// Dynamically import ApexCharts to avoid SSR issues
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
interface NamaBeasiswaData {
tahun_angkatan: number;
nama_beasiswa: string;
jumlah_nama_beasiswa: number;
}
interface Props {
selectedYear: string;
}
export default function NamaBeasiswaDashChart({ selectedYear }: Props) {
const { theme } = useTheme();
const [data, setData] = useState<NamaBeasiswaData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const url = `/api/mahasiswa/nama-beasiswa-dashboard?tahunAngkatan=${selectedYear}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`);
}
const result = await response.json();
if (!Array.isArray(result)) {
throw new Error('Invalid data format received from server');
}
// Sort data by tahun_angkatan
const sortedData = result.sort((a: NamaBeasiswaData, b: NamaBeasiswaData) =>
a.tahun_angkatan - b.tahun_angkatan
);
setData(sortedData);
} catch (err) {
console.error('Error in fetchData:', err);
setError(err instanceof Error ? err.message : 'An error occurred while fetching data');
} finally {
setLoading(false);
}
};
fetchData();
}, [selectedYear]);
// Process data for series
const processSeriesData = () => {
if (!data.length) return [];
const years = [...new Set(data.map(item => item.tahun_angkatan))].sort();
const beasiswaTypes = [...new Set(data.map(item => item.nama_beasiswa))].sort();
return beasiswaTypes.map(beasiswa => ({
name: beasiswa,
data: years.map(year => {
const item = data.find(d => d.tahun_angkatan === year && d.nama_beasiswa === beasiswa);
return item ? item.jumlah_nama_beasiswa : 0;
})
}));
};
const chartOptions: ApexOptions = {
chart: {
type: 'bar',
stacked: false,
toolbar: {
show: true,
tools: {
download: true,
selection: true,
zoom: true,
zoomin: true,
zoomout: true,
pan: true,
reset: true
}
},
background: theme === 'dark' ? '#0F172B' : '#fff',
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
borderRadius: 1,
},
},
dataLabels: {
enabled: true,
formatter: function (val: number) {
return val.toString();
},
style: {
fontSize: '12px',
colors: [theme === 'dark' ? '#fff' : '#000']
}
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(),
title: {
text: 'Tahun Angkatan',
style: {
fontSize: '14px',
fontWeight: 'bold',
color: theme === 'dark' ? '#fff' : '#000'
}
},
labels: {
style: {
fontSize: '12px',
colors: theme === 'dark' ? '#fff' : '#000'
}
},
axisBorder: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
},
axisTicks: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
},
tickAmount: 5,
},
yaxis: {
title: {
text: 'Jumlah Mahasiswa',
style: {
fontSize: '14px',
fontWeight: 'bold',
color: theme === 'dark' ? '#fff' : '#000'
}
},
labels: {
style: {
fontSize: '12px',
colors: theme === 'dark' ? '#fff' : '#000'
}
},
axisBorder: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
}
},
fill: {
opacity: 1
},
colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'],
tooltip: {
theme: theme === 'dark' ? 'dark' : 'light',
y: {
formatter: function (val: number) {
return val + " mahasiswa";
}
}
},
legend: {
position: 'top',
fontSize: '14px',
markers: {
size: 12,
},
itemMargin: {
horizontal: 10,
},
labels: {
colors: theme === 'dark' ? '#fff' : '#000'
}
},
grid: {
borderColor: theme === 'dark' ? '#374151' : '#E5E7EB',
strokeDashArray: 4,
padding: {
top: 20,
right: 0,
bottom: 0,
left: 0
}
}
};
const series = processSeriesData();
if (loading) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Loading...
</CardTitle>
</CardHeader>
</Card>
);
}
if (error) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold text-red-500">
Error: {error}
</CardTitle>
</CardHeader>
</Card>
);
}
if (data.length === 0) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Tidak ada data yang tersedia
</CardTitle>
</CardHeader>
</Card>
);
}
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Nama Beasiswa Dashboard
{selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''}
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] sm:h-[300px] md:h-[300px] w-full max-w-5xl mx-auto">
{typeof window !== 'undefined' && series.length > 0 && (
<Chart
options={chartOptions}
series={series}
type="bar"
height="100%"
width="100%"
/>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,185 @@
'use client';
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { ApexOptions } from 'apexcharts';
import { useTheme } from 'next-themes';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// Dynamically import ApexCharts to avoid SSR issues
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
interface ProvinsiMahasiswaData {
provinsi: string;
jumlah_mahasiswa: number;
}
export default function ProvinsiMahasiswaPieChart() {
const { theme } = useTheme();
const [data, setData] = useState<ProvinsiMahasiswaData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const url = `/api/mahasiswa/provinsi-mahasiswa`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`);
}
const result = await response.json();
if (!Array.isArray(result)) {
throw new Error('Invalid data format received from server');
}
setData(result);
} catch (err) {
console.error('Error in fetchData:', err);
setError(err instanceof Error ? err.message : 'An error occurred while fetching data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const chartOptions: ApexOptions = {
chart: {
type: 'pie',
background: theme === 'dark' ? '#0F172B' : '#fff',
},
labels: data.map(item => item.provinsi),
colors: ['#3B82F6', '#10B981'],
legend: {
position: 'bottom',
fontSize: '14px',
markers: {
size: 12,
},
itemMargin: {
horizontal: 10,
vertical: 5,
},
labels: {
colors: theme === 'dark' ? '#fff' : '#000'
}
},
dataLabels: {
enabled: true,
formatter: function (val: number) {
return `${val.toFixed(0)}%`;
},
style: {
fontSize: '14px',
fontWeight: 'bold',
colors: [theme === 'dark' ? '#fff' : '#000']
},
offsetY: -10,
dropShadow: {
enabled: true,
opacity: 0.3,
blur: 3,
color: '#000',
}
},
tooltip: {
theme: theme === 'dark' ? 'dark' : 'light',
y: {
formatter: function (val: number) {
return val + " mahasiswa";
}
}
},
plotOptions: {
pie: {
donut: {
size: '0%',
},
offsetY: 0,
},
},
stroke: {
width: 2,
colors: [theme === 'dark' ? '#0F172B' : '#fff'],
},
states: {
hover: {
filter: {
type: 'darken',
},
},
},
};
const series = data.map(item => item.jumlah_mahasiswa);
if (loading) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Loading...
</CardTitle>
</CardHeader>
</Card>
);
}
if (error) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold text-red-500">
Error: {error}
</CardTitle>
</CardHeader>
</Card>
);
}
if (data.length === 0) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Tidak ada data yang tersedia
</CardTitle>
</CardHeader>
</Card>
);
}
const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_mahasiswa, 0);
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Asal Provinsi Mahasiswa
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] w-full max-w-5xl mx-auto">
{typeof window !== 'undefined' && series.length > 0 && (
<Chart
options={chartOptions}
series={series}
type="pie"
height="100%"
width="100%"
/>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,270 @@
'use client';
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { ApexOptions } from 'apexcharts';
import { useTheme } from 'next-themes';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
// Dynamically import ApexCharts to avoid SSR issues
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
interface TingkatPrestasiData {
tahun_angkatan: string | number;
tingkat_prestasi: string;
tingkat_mahasiswa_prestasi: number;
}
interface Props {
selectedYear: string;
}
export default function TingkatPrestasiDashChart({ selectedYear }: Props) {
const { theme } = useTheme();
const [data, setData] = useState<TingkatPrestasiData[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const url = `/api/mahasiswa/tingkat-prestasi-dashboard?tahunAngkatan=${selectedYear}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`);
}
const result = await response.json();
if (!Array.isArray(result)) {
throw new Error('Invalid data format received from server');
}
// API already returns sorted data, no need to sort again
setData(result);
} catch (err) {
console.error('Error in fetchData:', err);
setError(err instanceof Error ? err.message : 'An error occurred while fetching data');
} finally {
setLoading(false);
}
};
fetchData();
}, [selectedYear]);
// Process data for series
const processSeriesData = () => {
if (!data.length) {
return [];
}
const years = [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b));
const tingkatTypes = [...new Set(data.map(item => item.tingkat_prestasi))].sort();
return tingkatTypes.map(tingkat => ({
name: tingkat,
data: years.map(year => {
const item = data.find(d => String(d.tahun_angkatan) === String(year) && d.tingkat_prestasi === tingkat);
return item?.tingkat_mahasiswa_prestasi || 0;
})
}));
};
const chartOptions: ApexOptions = {
chart: {
type: 'bar',
stacked: false,
toolbar: {
show: true,
tools: {
download: true,
selection: true,
zoom: true,
zoomin: true,
zoomout: true,
pan: true,
reset: true
}
},
background: theme === 'dark' ? '#0F172B' : '#fff',
},
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
borderRadius: 1,
},
},
dataLabels: {
enabled: true,
formatter: function (val: number) {
return val?.toString() || '0';
},
style: {
fontSize: '12px',
colors: [theme === 'dark' ? '#fff' : '#000']
}
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
categories: [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)),
title: {
text: 'Tahun Angkatan',
style: {
fontSize: '14px',
fontWeight: 'bold',
color: theme === 'dark' ? '#fff' : '#000'
}
},
labels: {
style: {
fontSize: '12px',
colors: theme === 'dark' ? '#fff' : '#000'
}
},
axisBorder: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
},
axisTicks: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
},
},
yaxis: {
title: {
text: 'Jumlah Mahasiswa',
style: {
fontSize: '14px',
fontWeight: 'bold',
color: theme === 'dark' ? '#fff' : '#000'
}
},
labels: {
style: {
fontSize: '12px',
colors: theme === 'dark' ? '#fff' : '#000'
}
},
axisBorder: {
show: true,
color: theme === 'dark' ? '#374151' : '#E5E7EB'
},
tickAmount: 5,
},
fill: {
opacity: 1
},
colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'],
tooltip: {
theme: theme === 'dark' ? 'dark' : 'light',
y: {
formatter: function (val: number) {
return val + " mahasiswa";
}
}
},
legend: {
position: 'top',
fontSize: '14px',
markers: {
size: 12,
},
itemMargin: {
horizontal: 10,
},
labels: {
colors: theme === 'dark' ? '#fff' : '#000'
}
},
grid: {
borderColor: theme === 'dark' ? '#374151' : '#E5E7EB',
strokeDashArray: 4,
padding: {
top: 20,
right: 0,
bottom: 0,
left: 0
}
}
};
const series = processSeriesData();
if (loading) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Loading...
</CardTitle>
</CardHeader>
</Card>
);
}
if (error) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold text-red-500">
Error: {error}
</CardTitle>
</CardHeader>
</Card>
);
}
if (data.length === 0) {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Tidak ada data yang tersedia
</CardTitle>
</CardHeader>
</Card>
);
}
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Tingkat Prestasi Dashboard
{selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''}
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-[300px] sm:h-[300px] md:h-[300px] w-full max-w-5xl mx-auto">
{typeof window !== 'undefined' && series.length > 0 ? (
<Chart
options={chartOptions}
series={series}
type="bar"
height="100%"
width="100%"
/>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-gray-500">
{series.length === 0 ? 'Tidak ada data untuk ditampilkan' : 'Loading chart...'}
</p>
</div>
)}
</div>
</CardContent>
</Card>
);
}

View File

@@ -115,7 +115,7 @@ const Navbar = () => {
<BarChart className="h-4 w-4" />
Dashboard
</Link>
<DropdownMenu>
{/* <DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="flex items-center gap-2 px-3 py-2 text-sm font-medium">
<BarChart className="h-4 w-4" />
@@ -161,7 +161,7 @@ const Navbar = () => {
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenu> */}
</>
)}
@@ -267,8 +267,12 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
{user ? (
<div className="space-y-2">
<h3 className="text-sm font-semibold text-muted-foreground">Menu Utama</h3>
<Link href="/dashboard" className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md transition-colors">
<BarChart className="h-4 w-4" />
Dashboard
</Link>
<div className="space-y-1">
{/* <div className="space-y-1">
<h4 className="text-xs font-medium text-muted-foreground px-3">Visualisasi</h4>
<Link href="/visualisasi/mahasiswa" className="flex items-center gap-2 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground rounded-md transition-colors">
<GraduationCap className="h-4 w-4" />
@@ -290,7 +294,7 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {
<Award className="h-4 w-4" />
Prestasi
</Link>
</div>
</div> */}
{/* Kelola Data - Only for Admin */}
{user.role_user === 'admin' && (