diff --git a/app/api/keloladata/data-dosen/route.ts b/app/api/keloladata/data-dosen/route.ts
new file mode 100644
index 0000000..45a2aad
--- /dev/null
+++ b/app/api/keloladata/data-dosen/route.ts
@@ -0,0 +1,251 @@
+import { NextRequest, NextResponse } from 'next/server';
+import supabase from '@/lib/db';
+
+// GET - Ambil semua data dosen atau filter berdasarkan kriteria
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const id = searchParams.get('id');
+ const search = searchParams.get('search');
+
+ // Jika ID diberikan, ambil data dosen spesifik berdasarkan ID
+ if (id) {
+ const { data, error } = await supabase
+ .from('dosen')
+ .select('*')
+ .eq('id_dosen', id)
+ .single();
+
+ if (error || !data) {
+ return NextResponse.json({ message: 'Dosen not found' }, { status: 404 });
+ }
+
+ return NextResponse.json(data);
+ }
+
+ // Bangun query berdasarkan filter
+ let query = supabase.from('dosen').select('*');
+
+ // Tambahkan kondisi pencarian jika diberikan
+ if (search) {
+ query = query.or(`nama_dosen.ilike.%${search}%,nip.ilike.%${search}%`);
+ }
+
+ // Tambahkan pengurutan berdasarkan nama dosen
+ query = query.order('nama_dosen', { ascending: true });
+
+ // Eksekusi query
+ const { data, error } = await query;
+
+ if (error) {
+ console.error('Error fetching data:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+}
+
+// POST - Buat data dosen baru
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const {
+ nama_dosen,
+ nip
+ } = body;
+
+ // Validate required fields
+ if (!nama_dosen || !nip) {
+ return NextResponse.json(
+ { message: 'Missing required fields: nama_dosen, nip' },
+ { status: 400 }
+ );
+ }
+
+ // Validate NIP length (harus 18 karakter)
+ if (nip.length !== 18) {
+ return NextResponse.json(
+ { message: 'NIP harus terdiri dari 18 karakter' },
+ { status: 400 }
+ );
+ }
+
+ // Cek apakah NIP sudah ada
+ const { data: existingDosen, error: checkError } = await supabase
+ .from('dosen')
+ .select('nip')
+ .eq('nip', nip)
+ .single();
+
+ if (!checkError && existingDosen) {
+ return NextResponse.json(
+ { message: 'NIP sudah terdaftar dalam database' },
+ { status: 409 }
+ );
+ }
+
+ // Insert data dosen baru
+ const { data, error } = await supabase
+ .from('dosen')
+ .insert({
+ nama_dosen,
+ nip
+ })
+ .select()
+ .single();
+
+ if (error) {
+ console.error('Error creating dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+
+ return NextResponse.json(
+ {
+ message: `Dosen berhasil ditambahkan`,
+ id: data.id_dosen
+ },
+ { status: 201 }
+ );
+ } catch (error) {
+ console.error('Error creating dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+}
+
+// PUT - Update data dosen yang sudah ada
+export async function PUT(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const id = searchParams.get('id');
+
+ if (!id) {
+ return NextResponse.json({ message: 'ID is required' }, { status: 400 });
+ }
+
+ const body = await request.json();
+ const {
+ nama_dosen,
+ nip
+ } = body;
+
+ // Validate required fields
+ if (!nama_dosen || !nip) {
+ return NextResponse.json(
+ { message: 'Missing required fields: nama_dosen, nip' },
+ { status: 400 }
+ );
+ }
+
+ // Validate NIP length (harus 18 karakter)
+ if (nip.length !== 18) {
+ return NextResponse.json(
+ { message: 'NIP harus terdiri dari 18 karakter' },
+ { status: 400 }
+ );
+ }
+
+ // Cek apakah dosen ada
+ const { data: existing, error: checkError } = await supabase
+ .from('dosen')
+ .select('*')
+ .eq('id_dosen', id)
+ .single();
+
+ if (checkError || !existing) {
+ return NextResponse.json({ message: 'Dosen not found' }, { status: 404 });
+ }
+
+ // Cek apakah NIP sudah ada untuk dosen lain
+ const { data: existingNip, error: nipCheckError } = await supabase
+ .from('dosen')
+ .select('id_dosen, nip')
+ .eq('nip', nip)
+ .neq('id_dosen', id)
+ .single();
+
+ if (!nipCheckError && existingNip) {
+ return NextResponse.json(
+ { message: 'NIP sudah digunakan oleh dosen lain' },
+ { status: 409 }
+ );
+ }
+
+ // Update dosen
+ const { error } = await supabase
+ .from('dosen')
+ .update({
+ nama_dosen,
+ nip
+ })
+ .eq('id_dosen', id);
+
+ if (error) {
+ console.error('Error updating dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+
+ return NextResponse.json({
+ message: `Dosen berhasil diperbarui`
+ });
+ } catch (error) {
+ console.error('Error updating dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+}
+
+// DELETE - Hapus data dosen
+export async function DELETE(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const id = searchParams.get('id');
+
+ if (!id) {
+ return NextResponse.json({ message: 'ID is required' }, { status: 400 });
+ }
+
+ // Cek apakah dosen ada
+ const { data: existing, error: checkError } = await supabase
+ .from('dosen')
+ .select('id_dosen')
+ .eq('id_dosen', id)
+ .single();
+
+ if (checkError || !existing) {
+ return NextResponse.json({ message: 'Dosen not found' }, { status: 404 });
+ }
+
+ // Cek apakah dosen sedang digunakan sebagai pembimbing
+ const { data: bimbinganCheck, error: bimbinganError } = await supabase
+ .from('mahasiswa')
+ .select('id_mahasiswa')
+ .or(`pembimbing1_id.eq.${id},pembimbing2_id.eq.${id}`)
+ .limit(1);
+
+ if (!bimbinganError && bimbinganCheck && bimbinganCheck.length > 0) {
+ return NextResponse.json(
+ { message: 'Tidak dapat menghapus dosen yang sedang menjadi pembimbing mahasiswa' },
+ { status: 409 }
+ );
+ }
+
+ // Hapus dosen
+ const { error } = await supabase
+ .from('dosen')
+ .delete()
+ .eq('id_dosen', id);
+
+ if (error) {
+ console.error('Error deleting dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+
+ return NextResponse.json({ message: 'Dosen berhasil dihapus' });
+ } catch (error) {
+ console.error('Error deleting dosen:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+}
diff --git a/app/api/keloladata/data-dosen/upload/route.ts b/app/api/keloladata/data-dosen/upload/route.ts
new file mode 100644
index 0000000..f155f5f
--- /dev/null
+++ b/app/api/keloladata/data-dosen/upload/route.ts
@@ -0,0 +1,167 @@
+import { NextRequest, NextResponse } from 'next/server';
+import * as XLSX from 'xlsx';
+import supabase from '@/lib/db';
+
+// POST - Upload dan proses file Excel/CSV untuk data dosen
+export async function POST(request: NextRequest) {
+ try {
+ // Ambil data formulir dari request
+ const formData = await request.formData();
+ const file = formData.get('file') as File;
+
+ if (!file) {
+ return NextResponse.json(
+ { message: 'File is required' },
+ { status: 400 }
+ );
+ }
+
+ // Validasi tipe file (Excel atau CSV)
+ const allowedTypes = [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
+ 'application/vnd.ms-excel', // .xls
+ 'text/csv' // .csv
+ ];
+
+ if (!allowedTypes.includes(file.type)) {
+ return NextResponse.json(
+ { message: 'File type not supported. Please upload Excel (.xlsx, .xls) or CSV file.' },
+ { status: 400 }
+ );
+ }
+
+ // Baca file sebagai buffer
+ const buffer = Buffer.from(await file.arrayBuffer());
+
+ // Parse file menggunakan XLSX
+ const workbook = XLSX.read(buffer, { type: 'buffer' });
+ const sheetName = workbook.SheetNames[0];
+ const worksheet = workbook.Sheets[sheetName];
+
+ // Konversi ke JSON
+ const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
+
+ if (jsonData.length === 0) {
+ return NextResponse.json(
+ { message: 'File is empty' },
+ { status: 400 }
+ );
+ }
+
+ // Ambil header dari baris pertama
+ const headers = jsonData[0] as string[];
+ const dataRows = jsonData.slice(1);
+
+ // Validasi header yang diperlukan
+ const requiredHeaders = ['nama_dosen', 'nip'];
+ const missingHeaders = requiredHeaders.filter(header =>
+ !headers.some(h => h && h.toString().toLowerCase().includes(header.toLowerCase()))
+ );
+
+ if (missingHeaders.length > 0) {
+ return NextResponse.json(
+ { message: `Missing required columns: ${missingHeaders.join(', ')}` },
+ { status: 400 }
+ );
+ }
+
+ // Mapping index kolom
+ const getColumnIndex = (headerName: string) => {
+ return headers.findIndex(h => h && h.toString().toLowerCase().includes(headerName.toLowerCase()));
+ };
+
+ const nama_dosenIndex = getColumnIndex('nama_dosen');
+ const nipIndex = getColumnIndex('nip');
+
+ // Proses setiap baris data
+ const results = {
+ success: 0,
+ failed: 0,
+ errors: [] as string[]
+ };
+
+ for (let i = 0; i < dataRows.length; i++) {
+ const row = dataRows[i] as any[];
+ const rowNumber = i + 2; // +2 karena index dimulai dari 0 dan ada header
+
+ try {
+ // Ekstrak data dari row
+ const nama_dosen = row[nama_dosenIndex]?.toString().trim();
+ const nip = row[nipIndex]?.toString().trim();
+
+ // Validasi data yang diperlukan
+ if (!nama_dosen || !nip) {
+ results.failed++;
+ results.errors.push(`Row ${rowNumber}: Missing required data (nama_dosen or nip)`);
+ continue;
+ }
+
+ // Validasi panjang NIP
+ if (nip.length !== 18) {
+ results.failed++;
+ results.errors.push(`Row ${rowNumber}: NIP harus terdiri dari 18 karakter`);
+ continue;
+ }
+
+ // Cek apakah NIP sudah ada
+ const { data: existingDosen, error: checkError } = await supabase
+ .from('dosen')
+ .select('nip')
+ .eq('nip', nip)
+ .single();
+
+ if (!checkError && existingDosen) {
+ // Update data yang sudah ada
+ const { error: updateError } = await supabase
+ .from('dosen')
+ .update({
+ nama_dosen
+ })
+ .eq('nip', nip);
+
+ if (updateError) {
+ results.failed++;
+ results.errors.push(`Row ${rowNumber}: Error updating dosen with NIP ${nip}: ${updateError.message}`);
+ continue;
+ }
+ } else {
+ // Insert data baru
+ const { error: insertError } = await supabase
+ .from('dosen')
+ .insert({
+ nama_dosen,
+ nip
+ });
+
+ if (insertError) {
+ results.failed++;
+ results.errors.push(`Row ${rowNumber}: Error inserting dosen with NIP ${nip}: ${insertError.message}`);
+ continue;
+ }
+ }
+
+ results.success++;
+ } catch (error) {
+ results.failed++;
+ results.errors.push(`Row ${rowNumber}: Unexpected error: ${error}`);
+ }
+ }
+
+ return NextResponse.json({
+ message: `Upload completed. Success: ${results.success}, Failed: ${results.failed}`,
+ details: {
+ totalRows: dataRows.length,
+ successCount: results.success,
+ failedCount: results.failed,
+ errors: results.errors
+ }
+ });
+
+ } catch (error) {
+ console.error('Error processing file:', error);
+ return NextResponse.json(
+ { message: 'Error processing file' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/keloladata/data-mahasiswa/route.ts b/app/api/keloladata/data-mahasiswa/route.ts
index a8a59fc..69a0ad3 100644
--- a/app/api/keloladata/data-mahasiswa/route.ts
+++ b/app/api/keloladata/data-mahasiswa/route.ts
@@ -13,7 +13,9 @@ export async function GET(request: NextRequest) {
.from('mahasiswa')
.select(`
*,
- kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok)
+ kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok),
+ dosen_pembimbing_1:dosen!pembimbing_1(id_dosen, nama_dosen),
+ dosen_pembimbing_2:dosen!pembimbing_2(id_dosen, nama_dosen)
`)
.eq('nim', nim)
.single();
@@ -25,9 +27,13 @@ export async function GET(request: NextRequest) {
// Transformasi data untuk meratakan field yang di-join
const transformedData = {
...data,
- nama_kelompok_keahlian: data.kelompok_keahlian?.nama_kelompok || null
+ nama_kelompok_keahlian: data.kelompok_keahlian?.nama_kelompok || null,
+ nama_pembimbing_1: data.dosen_pembimbing_1?.nama_dosen || null,
+ nama_pembimbing_2: data.dosen_pembimbing_2?.nama_dosen || null
};
delete transformedData.kelompok_keahlian;
+ delete transformedData.dosen_pembimbing_1;
+ delete transformedData.dosen_pembimbing_2;
return NextResponse.json(transformedData);
} else {
@@ -36,7 +42,9 @@ export async function GET(request: NextRequest) {
.from('mahasiswa')
.select(`
*,
- kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok)
+ kelompok_keahlian!id_kelompok_keahlian(id_kk, nama_kelompok),
+ dosen_pembimbing_1:dosen!pembimbing_1(id_dosen, nama_dosen),
+ dosen_pembimbing_2:dosen!pembimbing_2(id_dosen, nama_dosen)
`)
.order('nim');
@@ -48,8 +56,10 @@ export async function GET(request: NextRequest) {
// Transformasi data untuk meratakan field yang di-join
const transformedData = data.map(item => ({
...item,
- nama_kelompok_keahlian: item.kelompok_keahlian?.nama_kelompok || null
- })).map(({ kelompok_keahlian, ...rest }) => rest);
+ nama_kelompok_keahlian: item.kelompok_keahlian?.nama_kelompok || null,
+ nama_pembimbing_1: item.dosen_pembimbing_1?.nama_dosen || null,
+ nama_pembimbing_2: item.dosen_pembimbing_2?.nama_dosen || null
+ })).map(({ kelompok_keahlian, dosen_pembimbing_1, dosen_pembimbing_2, ...rest }) => rest);
return NextResponse.json(transformedData);
}
@@ -75,7 +85,10 @@ export async function POST(request: NextRequest) {
ipk,
id_kelompok_keahlian,
status_kuliah,
- semester
+ semester,
+ pembimbing_1,
+ pembimbing_2,
+ status_bimbingan
} = body;
// Validasi field yang wajib diisi
@@ -115,7 +128,10 @@ export async function POST(request: NextRequest) {
ipk: ipk || null,
id_kelompok_keahlian: id_kelompok_keahlian || null,
status_kuliah: status_kuliah || "Aktif",
- semester: semester || 1
+ semester: semester || 1,
+ pembimbing_1: pembimbing_1 || null,
+ pembimbing_2: pembimbing_2 || null,
+ status_bimbingan: status_bimbingan || "Belum Selesai"
})
.select()
.single();
@@ -157,7 +173,10 @@ export async function PUT(request: NextRequest) {
ipk,
id_kelompok_keahlian,
status_kuliah,
- semester
+ semester,
+ pembimbing_1,
+ pembimbing_2,
+ status_bimbingan
} = body;
// Cek apakah mahasiswa ada
@@ -185,7 +204,10 @@ export async function PUT(request: NextRequest) {
ipk: ipk || existing.ipk,
id_kelompok_keahlian: id_kelompok_keahlian || existing.id_kelompok_keahlian,
status_kuliah: status_kuliah || existing.status_kuliah,
- semester: semester || existing.semester
+ semester: semester || existing.semester,
+ pembimbing_1: pembimbing_1 !== undefined ? pembimbing_1 : existing.pembimbing_1,
+ pembimbing_2: pembimbing_2 !== undefined ? pembimbing_2 : existing.pembimbing_2,
+ status_bimbingan: status_bimbingan || existing.status_bimbingan
})
.eq('nim', nim);
diff --git a/app/api/keloladata/data-mahasiswa/upload/route.ts b/app/api/keloladata/data-mahasiswa/upload/route.ts
index c0bcace..bea7b0e 100644
--- a/app/api/keloladata/data-mahasiswa/upload/route.ts
+++ b/app/api/keloladata/data-mahasiswa/upload/route.ts
@@ -120,7 +120,10 @@ function processData(headers: string[], rows: any[][]) {
ipk: ['ipk', 'gpa'],
kelompok_keahlian: ['kelompok_keahlian', 'kk', 'keahlian', 'id_kk'],
status_kuliah: ['status_kuliah', 'status', 'status_mahasiswa'],
- semester: ['semester', 'sem']
+ semester: ['semester', 'sem'],
+ pembimbing_1_nip: ['pembimbing_1_nip', 'nip_pembimbing_1', 'pembimbing1_nip', 'dosen_pembimbing_1'],
+ pembimbing_2_nip: ['pembimbing_2_nip', 'nip_pembimbing_2', 'pembimbing2_nip', 'dosen_pembimbing_2'],
+ status_bimbingan: ['status_bimbingan', 'status_guidance', 'bimbingan_status']
};
// Map actual headers to expected headers
@@ -205,6 +208,22 @@ function processData(headers: string[], rows: any[][]) {
}
}
+ // Handle pembimbing NIP
+ const pembimbing_1_nip = headerMap.pembimbing_1_nip !== undefined ? String(values[headerMap.pembimbing_1_nip] || '') || null : null;
+ const pembimbing_2_nip = headerMap.pembimbing_2_nip !== undefined ? String(values[headerMap.pembimbing_2_nip] || '') || null : null;
+
+ // Handle status bimbingan
+ let status_bimbingan = 'Belum Selesai'; // Default value
+ if (headerMap.status_bimbingan !== undefined && values[headerMap.status_bimbingan] !== undefined) {
+ const statusValue = String(values[headerMap.status_bimbingan] || '').trim();
+ if (statusValue) {
+ const mappedStatus = mapStatusBimbingan(statusValue);
+ if (mappedStatus) {
+ status_bimbingan = mappedStatus;
+ }
+ }
+ }
+
// Validate required fields
if (!nim || !nama || !jenis_kelamin || !tahun_angkatan) {
errors.push(`Row ${i+1}: Missing required fields`);
@@ -241,14 +260,17 @@ function processData(headers: string[], rows: any[][]) {
nama,
jk: jenis_kelamin,
agama,
- kabupaten,
+ kabupaten,
provinsi,
jenis_pendaftaran,
tahun_angkatan,
ipk,
kelompok_keahlian_id,
status_kuliah,
- semester
+ semester,
+ pembimbing_1_nip,
+ pembimbing_2_nip,
+ status_bimbingan
});
} catch (error) {
@@ -301,6 +323,23 @@ function mapStatus(value: string): 'Aktif' | 'Cuti' | 'Lulus' | 'Non-Aktif' | nu
return null;
}
+// Function to map status bimbingan values to standardized format
+function mapStatusBimbingan(value: string): 'Selesai' | 'Belum Selesai' | null {
+ if (!value) return null;
+
+ const lowerValue = value.toLowerCase();
+
+ if (['selesai', 'completed', 'done', 'finished', 's', '1'].includes(lowerValue)) {
+ return 'Selesai';
+ }
+
+ if (['belum selesai', 'belum_selesai', 'not completed', 'incomplete', 'ongoing', 'in progress', 'b', '0'].includes(lowerValue)) {
+ return 'Belum Selesai';
+ }
+
+ return null;
+}
+
// Fungsi untuk insert data ke database
async function insertDataToDatabase(data: any[]) {
let insertedCount = 0;
@@ -315,6 +354,28 @@ async function insertDataToDatabase(data: any[]) {
.eq('nim', item.nim)
.single();
+ // Lookup pembimbing berdasarkan NIP
+ let pembimbing_1_id = null;
+ let pembimbing_2_id = null;
+
+ if (item.pembimbing_1_nip) {
+ const { data: dosen1 } = await supabase
+ .from('dosen')
+ .select('id_dosen')
+ .eq('nip', item.pembimbing_1_nip)
+ .single();
+ pembimbing_1_id = dosen1?.id_dosen || null;
+ }
+
+ if (item.pembimbing_2_nip) {
+ const { data: dosen2 } = await supabase
+ .from('dosen')
+ .select('id_dosen')
+ .eq('nip', item.pembimbing_2_nip)
+ .single();
+ pembimbing_2_id = dosen2?.id_dosen || null;
+ }
+
const mahasiswaData = {
nama: item.nama,
jk: item.jk,
@@ -326,7 +387,10 @@ async function insertDataToDatabase(data: any[]) {
ipk: item.ipk,
id_kelompok_keahlian: item.kelompok_keahlian_id,
status_kuliah: item.status_kuliah,
- semester: item.semester
+ semester: item.semester,
+ pembimbing_1: pembimbing_1_id,
+ pembimbing_2: pembimbing_2_id,
+ status_bimbingan: item.status_bimbingan
};
if (existingData) {
diff --git a/app/api/mahasiswa/bimbingan-dosen/route.ts b/app/api/mahasiswa/bimbingan-dosen/route.ts
new file mode 100644
index 0000000..ad74619
--- /dev/null
+++ b/app/api/mahasiswa/bimbingan-dosen/route.ts
@@ -0,0 +1,101 @@
+import { NextRequest, NextResponse } from 'next/server';
+import supabase from '@/lib/db';
+
+// GET - Ambil data bimbingan dosen berdasarkan status bimbingan
+export async function GET(request: NextRequest) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const tahun_angkatan = searchParams.get('tahun_angkatan');
+
+ let query = supabase
+ .from('mahasiswa')
+ .select(`
+ pembimbing_1,
+ pembimbing_2,
+ status_bimbingan,
+ tahun_angkatan,
+ dosen_pembimbing_1:dosen!pembimbing_1(id_dosen, nama_dosen),
+ dosen_pembimbing_2:dosen!pembimbing_2(id_dosen, nama_dosen)
+ `);
+
+ // Filter berdasarkan tahun angkatan jika diberikan
+ if (tahun_angkatan && tahun_angkatan !== 'all') {
+ query = query.eq('tahun_angkatan', tahun_angkatan);
+ }
+
+ const { data, error } = await query;
+
+ if (error) {
+ console.error('Error fetching bimbingan data:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+
+ // Proses data untuk menggabungkan pembimbing 1 dan 2
+ const bimbinganMap = new Map();
+
+ data.forEach(mahasiswa => {
+ // Proses pembimbing 1
+ if (mahasiswa.pembimbing_1 && mahasiswa.dosen_pembimbing_1) {
+ const dosenId = (mahasiswa.dosen_pembimbing_1 as any).id_dosen;
+ const dosenNama = (mahasiswa.dosen_pembimbing_1 as any).nama_dosen;
+ const status = mahasiswa.status_bimbingan;
+
+ if (!bimbinganMap.has(dosenId)) {
+ bimbinganMap.set(dosenId, {
+ id_dosen: dosenId,
+ nama_dosen: dosenNama,
+ selesai: 0,
+ belum_selesai: 0
+ });
+ }
+
+ const dosenData = bimbinganMap.get(dosenId);
+ if (dosenData) {
+ if (status === 'Selesai') {
+ dosenData.selesai++;
+ } else {
+ dosenData.belum_selesai++;
+ }
+ }
+ }
+
+ // Proses pembimbing 2
+ if (mahasiswa.pembimbing_2 && mahasiswa.dosen_pembimbing_2) {
+ const dosenId = (mahasiswa.dosen_pembimbing_2 as any).id_dosen;
+ const dosenNama = (mahasiswa.dosen_pembimbing_2 as any).nama_dosen;
+ const status = mahasiswa.status_bimbingan;
+
+ if (!bimbinganMap.has(dosenId)) {
+ bimbinganMap.set(dosenId, {
+ id_dosen: dosenId,
+ nama_dosen: dosenNama,
+ selesai: 0,
+ belum_selesai: 0
+ });
+ }
+
+ const dosenData = bimbinganMap.get(dosenId);
+ if (dosenData) {
+ if (status === 'Selesai') {
+ dosenData.selesai++;
+ } else {
+ dosenData.belum_selesai++;
+ }
+ }
+ }
+ });
+
+ // Konversi Map ke Array dan urutkan berdasarkan nama dosen A-Z
+ const result = Array.from(bimbinganMap.values())
+ .map(dosen => ({
+ ...dosen,
+ total: dosen.selesai + dosen.belum_selesai
+ }))
+ .sort((a, b) => a.nama_dosen.localeCompare(b.nama_dosen));
+
+ return NextResponse.json(result);
+ } catch (error) {
+ console.error('Error fetching bimbingan data:', error);
+ return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
+ }
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 5c00552..ec0230b 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -21,6 +21,8 @@ import TingkatPrestasiChart from "@/components/chartsDashboard/TingkatPrestasiDa
import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasiswaPieChart";
import TingkatPrestasiPieChartDash from "@/components/chartsDashboard/TingkatPrestasiPieChartDash";
import LulusTepatWaktuChart from "@/components/charts/LulusTepatWaktuChart";
+import BimbinganDosenChart from "@/components/charts/BimbinganDosenChart";
+import BimbinganDosenPerAngkatanChart from "@/components/charts/BimbinganDosenPerAngkatanChart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
@@ -82,7 +84,8 @@ export default function TotalMahasiswaPage() {
{ id: 'study-duration', label: 'Kelulusan Tepat Waktu & Masa Studi' },
{ id: 'expertise', label: 'Kelompok Keahlian' },
{ id: 'scholarship', label: 'Beasiswa & Prestasi' },
- { id: 'demographics', label: 'Asal Kabupaten & Provinsi' }
+ { id: 'demographics', label: 'Asal Kabupaten & Provinsi' },
+ { id: 'bimbingan-dosen', label: 'Bimbingan Dosen' }
];
// Navigation menu items for per year data
@@ -90,7 +93,8 @@ export default function TotalMahasiswaPage() {
{ id: 'overview-year', label: 'Jumlah & Status per Angkatan' },
{ id: 'status-year', label: 'Jenis Pendaftaran & Kelompok Keahlian' },
{ id: 'achievement-year', label: 'Beasiswa & Prestasi per Angkatan' },
- { id: 'demographics-year', label: 'Asal Kabupaten per Angkatan' }
+ { id: 'demographics-year', label: 'Asal Kabupaten per Angkatan' },
+ { id: 'bimbingan-dosen-year', label: 'Bimbingan Dosen' }
];
return (
@@ -177,6 +181,10 @@ export default function TotalMahasiswaPage() {
+
+
+
+
) : (
@@ -198,9 +206,13 @@ export default function TotalMahasiswaPage() {
{/* Demographics Section */}
-
)}
diff --git a/app/detail/bimbingan-dosen/page.tsx b/app/detail/bimbingan-dosen/page.tsx
new file mode 100644
index 0000000..01a2919
--- /dev/null
+++ b/app/detail/bimbingan-dosen/page.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { useState } from "react";
+import BimbinganDosenChart from "@/components/charts/BimbinganDosenChart";
+import BimbinganDosenPerAngkatanChart from "@/components/charts/BimbinganDosenPerAngkatanChart";
+import FilterTahunAngkatan from "@/components/FilterTahunAngkatan";
+
+export default function BimbinganDosenDetailPage() {
+ const [selectedYear, setSelectedYear] = useState("all");
+
+ return (
+
+
+ {/* Filter Section */}
+
+
+ {/* Chart Section - Enhanced Size */}
+
+ {/* Chart untuk semua data atau dual chart ketika tahun tertentu dipilih */}
+ {selectedYear === "all" ? (
+
+
+
+ ) : (
+ <>
+
+ >
+ )}
+
+
+ {/* Information Section */}
+
+
+ Informasi Visualisasi
+
+
+
+
+ Grafik Utama (Bimbingan Dosen)
+
+
+ - • Menampilkan statistik bimbingan mahasiswa per dosen pembimbing
+ - • Data terbagi menjadi dua kategori: "Selesai" dan "Belum Selesai"
+ - • Hijau menunjukkan bimbingan selesai, kuning untuk belum selesai
+
+
+ {selectedYear !== "all" && (
+
+
+ Grafik Per Angkatan ({selectedYear})
+
+
+ - • Menampilkan statistik bimbingan untuk angkatan {selectedYear}
+ - • Data spesifik beban bimbingan dosen per angkatan
+ - • Insight progress penyelesaian bimbingan per tahun angkatan
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/keloladata/dosen/page.tsx b/app/keloladata/dosen/page.tsx
new file mode 100644
index 0000000..dfda1c1
--- /dev/null
+++ b/app/keloladata/dosen/page.tsx
@@ -0,0 +1,9 @@
+import DataTableDosen from "@/components/datatable/data-table-dosen";
+
+export default function DosenPage() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index 7ee2f0c..d349b09 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -105,7 +105,7 @@ export default function DashboardPage() {
return (
-
Visualisasi Data Akademik Mahasiswa Informatika
+ {/*
Visualisasi Data Akademik Mahasiswa Informatika
*/}
{loading ? (
diff --git a/components/charts/AsalDaerahChart.tsx b/components/charts/AsalDaerahChart.tsx
index d86caa5..a4c8a1b 100644
--- a/components/charts/AsalDaerahChart.tsx
+++ b/components/charts/AsalDaerahChart.tsx
@@ -50,7 +50,7 @@ export default function AsalDaerahChart({
distributed: false,
barHeight: '90%',
dataLabels: {
- position: 'top',
+ position: 'center',
},
},
},
@@ -60,10 +60,9 @@ export default function AsalDaerahChart({
return val.toString();
},
style: {
- fontSize: '14px',
+ fontSize: '12px',
colors: [theme === 'dark' ? '#fff' : '#000']
},
- offsetX: 10,
},
stroke: {
show: true,
@@ -268,7 +267,7 @@ export default function AsalDaerahChart({
const calculateHeight = () => {
const minHeight = 100;
const barHeight = 15; // Tinggi per bar dalam piksel
- const padding = 50; // Ruang ekstra untuk judul, legenda, dll
+ const padding = 100; // Ruang ekstra untuk judul, legenda, dll
const dynamicHeight = Math.max(minHeight, (data.length * barHeight) + padding);
return `${dynamicHeight}px`;
};
@@ -292,7 +291,7 @@ export default function AsalDaerahChart({
{
const minHeight = 100;
const barHeight = 15; // Tinggi per bar dalam piksel
- const padding = 50; // Ruang ekstra untuk judul, legenda, dll
+ const padding = 100; // Ruang ekstra untuk judul, legenda, dll
const dynamicHeight = Math.max(minHeight, (data.length * barHeight) + padding);
return `${dynamicHeight}px`;
};
@@ -317,7 +316,7 @@ export default function AsalDaerahPerAngkatanChart({ tahunAngkatan }: Props) {
import('react-apexcharts'), { ssr: false });
+
+interface BimbinganDosenData {
+ id_dosen: number;
+ nama_dosen: string;
+ selesai: number;
+ belum_selesai: number;
+ total: number;
+}
+
+interface BimbinganDosenChartProps {
+ height?: string;
+ showDetailButton?: boolean;
+}
+
+export default function BimbinganDosenChart({
+ height = "h-[400px] sm:h-[450px] md:h-[500px] lg:h-[600px]",
+ showDetailButton = true
+}: BimbinganDosenChartProps = {}) {
+ const { theme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [data, setData] = useState([]);
+ const [series, setSeries] = useState([
+ {
+ name: 'Selesai',
+ data: []
+ },
+ {
+ name: 'Belum Selesai',
+ data: []
+ }
+ ]);
+ const [options, setOptions] = useState({
+ chart: {
+ type: 'bar',
+ stacked: true,
+ toolbar: {
+ show: true,
+ },
+ },
+ plotOptions: {
+ bar: {
+ horizontal: true,
+ columnWidth: '85%',
+ distributed: false,
+ barHeight: '90%',
+ dataLabels: {
+ position: 'center',
+ },
+ },
+ },
+ dataLabels: {
+ enabled: true,
+ formatter: function (val: number) {
+ return val > 0 ? val.toString() : '';
+ },
+ style: {
+ fontSize: '12px',
+ colors: [theme === 'dark' ? '#fff' : '#000']
+ },
+ },
+ stroke: {
+ show: true,
+ width: 1,
+ colors: [theme === 'dark' ? '#374151' : '#E5E7EB'],
+ },
+ xaxis: {
+ categories: [],
+ title: {
+ text: 'Jumlah Bimbingan',
+ style: {
+ fontSize: '14px',
+ fontWeight: 'bold',
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ style: {
+ fontSize: '12px',
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Nama Dosen',
+ style: {
+ fontSize: '14px',
+ fontWeight: 'bold',
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ style: {
+ fontSize: '14px',
+ colors: theme === 'dark' ? '#fff' : '#000'
+ },
+ maxWidth: 200,
+ },
+ },
+ legend: {
+ position: 'top',
+ fontSize: '14px',
+ markers: {
+ size: 12,
+ },
+ itemMargin: {
+ horizontal: 10,
+ },
+ horizontalAlign: 'center',
+ labels: {
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ fill: {
+ opacity: 1,
+ },
+ colors: ['#10B981', '#F59E0B'], // Hijau untuk Selesai, Kuning untuk Belum Selesai
+ tooltip: {
+ theme: theme === 'dark' ? 'dark' : 'light',
+ y: {
+ formatter: function (val: number) {
+ return val + " mahasiswa";
+ }
+ }
+ }
+ });
+
+ // Perbarui tema saat berubah
+ useEffect(() => {
+ setOptions(prev => ({
+ ...prev,
+ chart: {
+ ...prev.chart,
+ background: theme === 'dark' ? '#0F172B' : '#fff',
+ },
+ xaxis: {
+ ...prev.xaxis,
+ title: {
+ ...prev.xaxis?.title,
+ style: {
+ ...prev.xaxis?.title?.style,
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ ...prev.xaxis?.labels,
+ style: {
+ ...prev.xaxis?.labels?.style,
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ yaxis: {
+ ...prev.yaxis,
+ title: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title),
+ style: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style),
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels),
+ style: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style),
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ legend: {
+ ...prev.legend,
+ labels: {
+ ...prev.legend?.labels,
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ tooltip: {
+ ...prev.tooltip,
+ theme: theme === 'dark' ? 'dark' : 'light'
+ }
+ }));
+ }, [theme]);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch('/api/mahasiswa/bimbingan-dosen');
+
+ 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('Format data tidak valid diterima dari server');
+ }
+
+ // Urutkan data berdasarkan nama dosen A-Z
+ const sortedResult = result.sort((a, b) => a.nama_dosen.localeCompare(b.nama_dosen));
+ setData(sortedResult);
+
+ // Proses data untuk chart
+ const namaDosen = sortedResult.map(item => item.nama_dosen);
+ const selesaiData = sortedResult.map(item => item.selesai);
+ const belumSelesaiData = sortedResult.map(item => item.belum_selesai);
+
+ setSeries([
+ {
+ name: 'Selesai',
+ data: selesaiData
+ },
+ {
+ name: 'Belum Selesai',
+ data: belumSelesaiData
+ }
+ ]);
+
+ setOptions(prev => ({
+ ...prev,
+ xaxis: {
+ ...prev.xaxis,
+ categories: namaDosen,
+ },
+ }));
+ } catch (err) {
+ console.error('Error in fetchData:', err);
+ setError(err instanceof Error ? err.message : 'Terjadi kesalahan saat mengambil data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ if (!mounted) {
+ return null;
+ }
+
+ if (loading) {
+ return (
+
+
+
+ Loading...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ Error: {error}
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+ Tidak ada data bimbingan yang tersedia
+
+
+
+ );
+ }
+
+ // Hitung tinggi dinamis berdasarkan jumlah dosen
+ const calculateHeight = () => {
+ const minHeight = 200;
+ const barHeight = 25; // Tinggi per bar dalam piksel
+ const padding = 100; // Ruang ekstra untuk judul, legenda, dll
+ const dynamicHeight = Math.max(minHeight, (data.length * barHeight) + padding);
+ return `${dynamicHeight}px`;
+ };
+
+ return (
+
+
+
+
+ Bimbingan Dosen
+
+ {showDetailButton && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/charts/BimbinganDosenPerAngkatanChart.tsx b/components/charts/BimbinganDosenPerAngkatanChart.tsx
new file mode 100644
index 0000000..33f7ecd
--- /dev/null
+++ b/components/charts/BimbinganDosenPerAngkatanChart.tsx
@@ -0,0 +1,358 @@
+'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";
+
+// Import ApexCharts secara dinamis untuk menghindari masalah SSR
+const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
+
+interface BimbinganDosenData {
+ id_dosen: number;
+ nama_dosen: string;
+ selesai: number;
+ belum_selesai: number;
+ total: number;
+}
+
+interface Props {
+ tahunAngkatan: string;
+}
+
+export default function BimbinganDosenPerAngkatanChart({ tahunAngkatan }: Props) {
+ const { theme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [data, setData] = useState([]);
+ const [series, setSeries] = useState([
+ {
+ name: 'Selesai',
+ data: []
+ },
+ {
+ name: 'Belum Selesai',
+ data: []
+ }
+ ]);
+ const [options, setOptions] = useState({
+ chart: {
+ type: 'bar',
+ stacked: true,
+ toolbar: {
+ show: true,
+ tools: {
+ download: true,
+ selection: true,
+ zoom: true,
+ zoomin: true,
+ zoomout: true,
+ pan: true,
+ reset: true,
+ customIcons: []
+ },
+ export: {
+ csv: {
+ filename: `bimbingan-dosen-angkatan`,
+ columnDelimiter: ',',
+ headerCategory: 'Nama Dosen',
+ headerValue: 'Jumlah Bimbingan'
+ }
+ },
+ },
+ },
+ plotOptions: {
+ bar: {
+ horizontal: true,
+ columnWidth: '85%',
+ distributed: false,
+ barHeight: '90%',
+ dataLabels: {
+ position: 'center'
+ }
+ }
+ },
+ dataLabels: {
+ enabled: true,
+ formatter: function (val: number) {
+ return val > 0 ? val.toString() : '';
+ },
+ style: {
+ fontSize: '12px',
+ colors: [theme === 'dark' ? '#fff' : '#000']
+ },
+ },
+ stroke: {
+ show: true,
+ width: 1,
+ colors: [theme === 'dark' ? '#374151' : '#E5E7EB'],
+ },
+ xaxis: {
+ categories: [],
+ title: {
+ text: 'Jumlah Bimbingan',
+ style: {
+ fontSize: '14px',
+ fontWeight: 'bold',
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ style: {
+ fontSize: '12px',
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ yaxis: {
+ title: {
+ text: 'Nama Dosen',
+ style: {
+ fontSize: '14px',
+ fontWeight: 'bold',
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ style: {
+ fontSize: '14px',
+ colors: theme === 'dark' ? '#fff' : '#000'
+ },
+ maxWidth: 200,
+ },
+ },
+ legend: {
+ position: 'top',
+ horizontalAlign: 'center',
+ labels: {
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ grid: {
+ padding: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ }
+ },
+ fill: {
+ opacity: 1,
+ },
+ colors: ['#10B981', '#F59E0B'], // Hijau untuk Selesai, Kuning untuk Belum Selesai
+ tooltip: {
+ theme: theme === 'dark' ? 'dark' : 'light',
+ y: {
+ formatter: function (val: number) {
+ return val + " mahasiswa";
+ }
+ }
+ }
+ });
+
+ // Perbarui tema saat berubah
+ useEffect(() => {
+ setOptions(prev => ({
+ ...prev,
+ chart: {
+ ...prev.chart,
+ background: theme === 'dark' ? '#0F172B' : '#fff',
+ },
+ xaxis: {
+ ...prev.xaxis,
+ title: {
+ ...prev.xaxis?.title,
+ style: {
+ ...prev.xaxis?.title?.style,
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ ...prev.xaxis?.labels,
+ style: {
+ ...prev.xaxis?.labels?.style,
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ yaxis: {
+ ...prev.yaxis,
+ title: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title),
+ style: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style),
+ color: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ labels: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels),
+ style: {
+ ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style),
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ }
+ },
+ legend: {
+ ...prev.legend,
+ labels: {
+ ...prev.legend?.labels,
+ colors: theme === 'dark' ? '#fff' : '#000'
+ }
+ },
+ tooltip: {
+ ...prev.tooltip,
+ theme: theme === 'dark' ? 'dark' : 'light'
+ }
+ }));
+ }, [theme]);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch(`/api/mahasiswa/bimbingan-dosen?tahun_angkatan=${tahunAngkatan}`);
+
+ 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('Format data tidak valid diterima dari server');
+ }
+
+ // Urutkan data berdasarkan nama dosen A-Z
+ const sortedResult = result.sort((a, b) => a.nama_dosen.localeCompare(b.nama_dosen));
+ setData(sortedResult);
+
+ // Proses data untuk chart
+ const namaDosen = sortedResult.map(item => item.nama_dosen);
+ const selesaiData = sortedResult.map(item => item.selesai);
+ const belumSelesaiData = sortedResult.map(item => item.belum_selesai);
+
+ setSeries([
+ {
+ name: 'Selesai',
+ data: selesaiData
+ },
+ {
+ name: 'Belum Selesai',
+ data: belumSelesaiData
+ }
+ ]);
+
+ setOptions(prev => ({
+ ...prev,
+ xaxis: {
+ ...prev.xaxis,
+ categories: namaDosen,
+ },
+ chart: {
+ ...prev.chart,
+ toolbar: {
+ ...prev.chart?.toolbar,
+ export: {
+ ...prev.chart?.toolbar?.export,
+ csv: {
+ ...prev.chart?.toolbar?.export?.csv,
+ filename: `bimbingan-dosen-angkatan-${tahunAngkatan}`,
+ }
+ }
+ }
+ }
+ }));
+ } catch (err) {
+ console.error('Error in fetchData:', err);
+ setError(err instanceof Error ? err.message : 'Terjadi kesalahan saat mengambil data');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (tahunAngkatan) {
+ fetchData();
+ }
+ }, [tahunAngkatan]);
+
+ if (!mounted) {
+ return null;
+ }
+
+ if (loading) {
+ return (
+
+
+
+ Loading...
+
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ Error: {error}
+
+
+
+ );
+ }
+
+ if (data.length === 0) {
+ return (
+
+
+
+ Tidak ada data bimbingan yang tersedia untuk angkatan {tahunAngkatan}
+
+
+
+ );
+ }
+
+ // Hitung tinggi dinamis berdasarkan jumlah dosen
+ const calculateHeight = () => {
+ const minHeight = 200;
+ const barHeight = 25; // Tinggi per bar dalam piksel
+ const padding = 100; // Ruang ekstra untuk judul, legenda, dll
+ const dynamicHeight = Math.max(minHeight, (data.length * barHeight) + padding);
+ return `${dynamicHeight}px`;
+ };
+
+ return (
+
+
+
+ Bimbingan Dosen Angkatan {tahunAngkatan}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/datatable/data-table-dosen.tsx b/components/datatable/data-table-dosen.tsx
new file mode 100644
index 0000000..1c0c53f
--- /dev/null
+++ b/components/datatable/data-table-dosen.tsx
@@ -0,0 +1,617 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from "@/components/ui/table";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+ DialogClose
+} from "@/components/ui/dialog";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue
+} from "@/components/ui/select";
+import {
+ Pagination,
+ PaginationContent,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationLink,
+ PaginationNext,
+ PaginationPrevious,
+} from "@/components/ui/pagination";
+import {
+ PlusCircle,
+ Pencil,
+ Trash2,
+ Search,
+ X,
+ Loader2
+} from "lucide-react";
+import UploadExcelDosen from "@/components/datatable/upload-file-dosen";
+import { useToast } from "@/components/ui/toast-provider";
+
+// Define the Dosen type
+interface Dosen {
+ id_dosen: number;
+ nama_dosen: string;
+ nip: string;
+}
+
+export default function DataTableDosen() {
+ const { showSuccess, showError } = useToast();
+
+ // State for data
+ const [dosen, setDosen] = useState([]);
+ const [filteredData, setFilteredData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // State for filtering
+ const [searchTerm, setSearchTerm] = useState("");
+
+ // State for pagination
+ const [currentPage, setCurrentPage] = useState(1);
+ const [pageSize, setPageSize] = useState(10);
+ const [paginatedData, setPaginatedData] = useState([]);
+
+ // State for form
+ const [formMode, setFormMode] = useState<"add" | "edit">("add");
+ const [formData, setFormData] = useState>({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+
+ // State for delete confirmation
+ const [deleteId, setDeleteId] = useState(null);
+ const [isDeleting, setIsDeleting] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+
+ // Fetch data on component mount
+ useEffect(() => {
+ fetchDosen();
+ }, []);
+
+ // Filter data when search term changes
+ useEffect(() => {
+ filterData();
+ }, [searchTerm, dosen]);
+
+ // Update paginated data when filtered data or pagination settings change
+ useEffect(() => {
+ paginateData();
+ }, [filteredData, currentPage, pageSize]);
+
+ // Fetch dosen data from API
+ const fetchDosen = async () => {
+ try {
+ setLoading(true);
+ let url = "/api/keloladata/data-dosen";
+
+ // Add search to URL if it exists
+ const params = new URLSearchParams();
+ if (searchTerm) {
+ params.append("search", searchTerm);
+ }
+
+ if (params.toString()) {
+ url += `?${params.toString()}`;
+ }
+
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch data");
+ }
+
+ const data = await response.json();
+ setDosen(data);
+ setFilteredData(data);
+ setError(null);
+ } catch (err) {
+ setError("Error fetching data. Please try again later.");
+ console.error("Error fetching data:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Filter data based on search term
+ const filterData = () => {
+ let filtered = [...dosen];
+
+ // Filter by search term
+ if (searchTerm) {
+ filtered = filtered.filter(
+ (item) =>
+ (item.nama_dosen?.toLowerCase() || "").includes(searchTerm.toLowerCase()) ||
+ (item.nip?.toLowerCase() || "").includes(searchTerm.toLowerCase())
+ );
+ }
+
+ setFilteredData(filtered);
+ // Reset to first page when filters change
+ setCurrentPage(1);
+ };
+
+ // Paginate data
+ const paginateData = () => {
+ const startIndex = (currentPage - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ setPaginatedData(filteredData.slice(startIndex, endIndex));
+ };
+
+ // Get total number of pages
+ const getTotalPages = () => {
+ return Math.ceil(filteredData.length / pageSize);
+ };
+
+ // Handle page change
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ };
+
+ // Handle page size change
+ const handlePageSizeChange = (size: string) => {
+ setPageSize(Number(size));
+ setCurrentPage(1); // Reset to first page when changing page size
+ };
+
+ // Reset form data
+ const resetForm = () => {
+ setFormData({});
+ };
+
+ // Handle form input changes
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ // Open form dialog for adding new dosen
+ const handleAdd = () => {
+ setFormMode("add");
+ resetForm();
+ setIsDialogOpen(true);
+ };
+
+ // Open form dialog for editing dosen
+ const handleEdit = (data: Dosen) => {
+ setFormMode("edit");
+ setFormData(data);
+ setIsDialogOpen(true);
+ };
+
+ // Open delete confirmation dialog
+ const handleDeleteConfirm = (id: number) => {
+ setDeleteId(id);
+ setIsDeleteDialogOpen(true);
+ };
+
+ // Submit form for add/edit
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ try {
+ setIsSubmitting(true);
+
+ if (formMode === "add") {
+ // Add new dosen
+ const response = await fetch("/api/keloladata/data-dosen", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const responseData = await response.json();
+
+ if (!response.ok) {
+ // Handle specific errors
+ if (response.status === 409) {
+ showError("Gagal!", responseData.message);
+ throw new Error(responseData.message);
+ }
+ if (response.status === 400) {
+ showError("Gagal!", responseData.message);
+ throw new Error(responseData.message);
+ }
+ showError("Gagal!", "Gagal menambahkan dosen");
+ throw new Error(responseData.message || "Failed to add dosen");
+ }
+
+ showSuccess("Berhasil!", "Dosen berhasil ditambahkan");
+ } else {
+ // Edit existing dosen
+ const response = await fetch(`/api/keloladata/data-dosen?id=${formData.id_dosen}`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData),
+ });
+
+ const responseData = await response.json();
+
+ if (!response.ok) {
+ // Handle specific errors
+ if (response.status === 409) {
+ showError("Gagal!", responseData.message);
+ throw new Error(responseData.message);
+ }
+ if (response.status === 400) {
+ showError("Gagal!", responseData.message);
+ throw new Error(responseData.message);
+ }
+ showError("Gagal!", responseData.message || "Failed to update dosen");
+ throw new Error(responseData.message || "Failed to update dosen");
+ }
+
+ showSuccess("Berhasil!", "Dosen berhasil diperbarui");
+ }
+
+ // Refresh data after successful operation
+ await fetchDosen();
+ setIsDialogOpen(false);
+ resetForm();
+ } catch (err) {
+ console.error("Error submitting form:", err);
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ // Delete dosen
+ const handleDelete = async () => {
+ if (!deleteId) return;
+
+ try {
+ setIsDeleting(true);
+
+ const response = await fetch(`/api/keloladata/data-dosen?id=${deleteId}`, {
+ method: "DELETE",
+ });
+
+ const responseData = await response.json();
+
+ if (!response.ok) {
+ if (response.status === 409) {
+ showError("Gagal!", responseData.message);
+ throw new Error(responseData.message);
+ }
+ showError("Gagal!", responseData.message || "Failed to delete dosen");
+ throw new Error(responseData.message || "Failed to delete dosen");
+ }
+
+ // Refresh data after successful deletion
+ await fetchDosen();
+ setIsDeleteDialogOpen(false);
+ setDeleteId(null);
+ showSuccess("Berhasil!", "Dosen berhasil dihapus");
+ } catch (err) {
+ console.error("Error deleting dosen:", err);
+ } finally {
+ setIsDeleting(false);
+ }
+ };
+
+ // Generate pagination items
+ const renderPaginationItems = () => {
+ const totalPages = getTotalPages();
+ const items = [];
+
+ // Always show first page
+ items.push(
+
+ handlePageChange(1)}
+ >
+ 1
+
+
+ );
+
+ // Show ellipsis if needed
+ if (currentPage > 3) {
+ items.push(
+
+
+
+ );
+ }
+
+ // Show pages around current page
+ for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) {
+ if (i === 1 || i === totalPages) continue; // Skip first and last pages as they're always shown
+ items.push(
+
+ handlePageChange(i)}
+ >
+ {i}
+
+
+ );
+ }
+
+ // Show ellipsis if needed
+ if (currentPage < totalPages - 2) {
+ items.push(
+
+
+
+ );
+ }
+
+ // Always show last page if there's more than one page
+ if (totalPages > 1) {
+ items.push(
+
+ handlePageChange(totalPages)}
+ >
+ {totalPages}
+
+
+ );
+ }
+
+ return items;
+ };
+
+ // Calculate the range of entries being displayed
+ const getDisplayRange = () => {
+ if (filteredData.length === 0) return { start: 0, end: 0 };
+
+ const start = (currentPage - 1) * pageSize + 1;
+ const end = Math.min(currentPage * pageSize, filteredData.length);
+
+ return { start, end };
+ };
+
+ return (
+
+
+
+ {/* Search */}
+
+
+
+ setSearchTerm(e.target.value)}
+ />
+ {searchTerm && (
+ setSearchTerm("")}
+ />
+ )}
+
+
+
+ {/* Show entries selector */}
+
+ Show
+
+ entries
+
+
+ {/* Table */}
+ {loading ? (
+
+
+
+ ) : error ? (
+
+ {error}
+
+ ) : (
+
+
+
+
+ {/* ID */}
+ Nama Dosen
+ NIP
+ Aksi
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+ Tidak ada data yang sesuai dengan pencarian
+
+
+ ) : (
+ paginatedData.map((dosenItem) => (
+
+ {/* {dosenItem.id_dosen} */}
+ {dosenItem.nama_dosen}
+ {dosenItem.nip}
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ )}
+
+ {/* Pagination info and controls */}
+ {!loading && !error && filteredData.length > 0 && (
+
+
+ Showing {getDisplayRange().start} to {getDisplayRange().end} of {filteredData.length} entries
+
+
+
+
+ handlePageChange(Math.max(1, currentPage - 1))}
+ className={currentPage === 1 ? "pointer-events-none opacity-50" : ""}
+ />
+
+
+ {renderPaginationItems()}
+
+
+ handlePageChange(Math.min(getTotalPages(), currentPage + 1))}
+ className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : ""}
+ />
+
+
+
+
+ )}
+
+ {/* Add/Edit Dialog */}
+
+
+ {/* Delete Confirmation Dialog */}
+
+
+ );
+}
diff --git a/components/datatable/data-table-mahasiswa.tsx b/components/datatable/data-table-mahasiswa.tsx
index 753b7e6..566757e 100644
--- a/components/datatable/data-table-mahasiswa.tsx
+++ b/components/datatable/data-table-mahasiswa.tsx
@@ -66,6 +66,11 @@ interface Mahasiswa {
nama_kelompok_keahlian: string | null;
status_kuliah: "Aktif" | "Cuti" | "Lulus" | "Non-Aktif";
semester: number;
+ pembimbing_1: number | null;
+ nama_pembimbing_1: string | null;
+ pembimbing_2: number | null;
+ nama_pembimbing_2: string | null;
+ status_bimbingan: "Selesai" | "Belum Selesai";
created_at: string;
updated_at: string;
}
@@ -76,6 +81,13 @@ interface KelompokKeahlian {
nama_kelompok: string;
}
+// Define the Dosen type
+interface Dosen {
+ id_dosen: number;
+ nama_dosen: string;
+ nip: string;
+}
+
export default function DataTableMahasiswa() {
const { showSuccess, showError } = useToast();
// State for data
@@ -125,6 +137,9 @@ export default function DataTableMahasiswa() {
const [deleteKelompokKeahlianId, setDeleteKelompokKeahlianId] = useState(null);
const [isKelompokKeahlianDeleting, setIsKelompokKeahlianDeleting] = useState(false);
const [isKelompokKeahlianDeleteDialogOpen, setIsKelompokKeahlianDeleteDialogOpen] = useState(false);
+
+ // State for dosen data
+ const [dosenData, setDosenData] = useState([]);
// Fetch data on component mount
useEffect(() => {
@@ -132,6 +147,7 @@ export default function DataTableMahasiswa() {
fetchJenisPendaftaranOptions();
fetchKelompokKeahlianOptions();
fetchKelompokKeahlianData();
+ fetchDosenData();
}, []);
// Filter data when search term or filter changes
@@ -267,7 +283,8 @@ export default function DataTableMahasiswa() {
setFormData({
jk: "Pria",
status_kuliah: "Aktif",
- semester: 1
+ semester: 1,
+ status_bimbingan: "Belum Selesai"
});
};
@@ -291,6 +308,9 @@ export default function DataTableMahasiswa() {
} else if (name === "semester") {
const numValue = value === "" ? 1 : parseInt(value);
setFormData((prev) => ({ ...prev, [name]: numValue }));
+ } else if (name === "pembimbing_1" || name === "pembimbing_2") {
+ const numValue = value === "null" || value === "" ? null : parseInt(value);
+ setFormData((prev) => ({ ...prev, [name]: numValue }));
} else {
setFormData((prev) => ({ ...prev, [name]: value }));
}
@@ -329,6 +349,22 @@ export default function DataTableMahasiswa() {
}
};
+ // Fetch dosen data from API
+ const fetchDosenData = async () => {
+ try {
+ const response = await fetch("/api/keloladata/data-dosen");
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch dosen data");
+ }
+
+ const data = await response.json();
+ setDosenData(data);
+ } catch (err) {
+ console.error("Error fetching dosen data:", err);
+ }
+ };
+
// Open form dialog for adding new mahasiswa
const handleAdd = () => {
setFormMode("add");
@@ -337,6 +373,7 @@ export default function DataTableMahasiswa() {
// Make sure we have the latest options
fetchJenisPendaftaranOptions();
fetchKelompokKeahlianOptions();
+ fetchDosenData();
};
// Open form dialog for editing mahasiswa
@@ -347,6 +384,7 @@ export default function DataTableMahasiswa() {
// Make sure we have the latest options
fetchJenisPendaftaranOptions();
fetchKelompokKeahlianOptions();
+ fetchDosenData();
};
// Open delete confirmation dialog
@@ -748,13 +786,16 @@ export default function DataTableMahasiswa() {
IPK
Status Kuliah
Kelompok Keahlian
+ Pembimbing 1
+ Pembimbing 2
+ Status Bimbingan
Aksi
{paginatedData.length === 0 ? (
-
+
Tidak ada data yang sesuai dengan filter
@@ -787,6 +828,19 @@ export default function DataTableMahasiswa() {
{mhs.nama_kelompok_keahlian || "-"}
+ {mhs.nama_pembimbing_1 || "-"}
+ {mhs.nama_pembimbing_2 || "-"}
+
+
+ {mhs.status_bimbingan}
+
+
@@ -1026,6 +1080,65 @@ export default function DataTableMahasiswa() {
required
/>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/datatable/upload-file-dosen.tsx b/components/datatable/upload-file-dosen.tsx
new file mode 100644
index 0000000..78b0f88
--- /dev/null
+++ b/components/datatable/upload-file-dosen.tsx
@@ -0,0 +1,225 @@
+"use client";
+
+import { useState, useRef } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogFooter,
+ DialogClose
+} from "@/components/ui/dialog";
+import { Upload, FileText, Loader2, X, CheckCircle, AlertCircle } from "lucide-react";
+import { useToast } from "@/components/ui/toast-provider";
+
+interface UploadExcelDosenProps {
+ onUploadSuccess: () => void;
+}
+
+export default function UploadExcelDosen({ onUploadSuccess }: UploadExcelDosenProps) {
+ const { showSuccess, showError } = useToast();
+ const fileInputRef = useRef(null);
+
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [selectedFile, setSelectedFile] = useState(null);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadResult, setUploadResult] = useState(null);
+
+ // Handle file selection
+ const handleFileSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ // Validate file type
+ const allowedTypes = [
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel',
+ 'text/csv'
+ ];
+
+ if (allowedTypes.includes(file.type)) {
+ setSelectedFile(file);
+ } else {
+ showError("File tidak valid", "Silakan pilih file Excel (.xlsx, .xls) atau CSV (.csv)");
+ e.target.value = '';
+ }
+ }
+ };
+
+ // Handle file upload
+ const handleUpload = async () => {
+ if (!selectedFile) {
+ showError("Tidak ada file", "Silakan pilih file terlebih dahulu");
+ return;
+ }
+
+ try {
+ setIsUploading(true);
+
+ const formData = new FormData();
+ formData.append('file', selectedFile);
+
+ const response = await fetch('/api/keloladata/data-dosen/upload', {
+ method: 'POST',
+ body: formData,
+ });
+
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error(result.message || 'Upload failed');
+ }
+
+ setUploadResult(result);
+
+ if (result.details.successCount > 0) {
+ showSuccess("Upload berhasil!", result.message);
+ onUploadSuccess(); // Refresh the main table
+ } else {
+ showError("Upload gagal", "Tidak ada data yang berhasil diupload");
+ }
+
+ } catch (error) {
+ console.error('Upload error:', error);
+ showError("Error", error instanceof Error ? error.message : "Terjadi kesalahan saat upload");
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ // Reset dialog
+ const resetDialog = () => {
+ setSelectedFile(null);
+ setUploadResult(null);
+ if (fileInputRef.current) {
+ fileInputRef.current.value = '';
+ }
+ };
+
+ // Close dialog
+ const handleDialogClose = () => {
+ setIsDialogOpen(false);
+ resetDialog();
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx
index 73e4d8e..9a2baac 100644
--- a/components/ui/Navbar.tsx
+++ b/components/ui/Navbar.tsx
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { ThemeToggle } from '@/components/theme-toggle';
-import { Menu, ChevronDown, BarChart, Database, CircleCheck, School, GraduationCap, Clock, BookOpen, Award, Home, LogOut, User } from 'lucide-react';
+import { Menu, ChevronDown, BarChart, Database, CircleCheck, School, GraduationCap, Clock, BookOpen, Award, Home, LogOut, User, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import {
@@ -194,6 +194,12 @@ const Navbar = () => {
Prestasi
+
+
+
+ Dosen
+
+
)}