From 4585f6a3463aca4905517a18b2b498fe400294fb Mon Sep 17 00:00:00 2001 From: Randa Firman Putra Date: Tue, 15 Jul 2025 14:46:34 +0700 Subject: [PATCH] Add Kelola Data --- .../data-beasiswa-mahasiswa/route.ts | 28 +- .../data-beasiswa-mahasiswa/upload/route.ts | 109 +-- app/dashboard/layout.tsx | 9 - app/dashboard/mahasiswa/beasiswa/page.tsx | 94 --- app/dashboard/mahasiswa/berprestasi/page.tsx | 80 -- .../mahasiswa/lulustepatwaktu/page.tsx | 61 -- app/dashboard/mahasiswa/profile/page.tsx | 11 - app/dashboard/mahasiswa/status/page.tsx | 86 -- app/dashboard/mahasiswa/total/page.tsx | 59 -- app/dashboard/page.tsx | 238 ------ app/keloladata/beasiswa/page.tsx | 6 +- app/keloladata/prestasi/page.tsx | 6 +- app/layout.tsx | 5 +- app/page.tsx | 8 +- .../data-table-beasiswa-mahasiswa.tsx | 685 +++++++++++++++ components/datatable/data-table-mahasiswa.tsx | 18 +- .../data-table-prestasi-mahasiswa.tsx | 797 ++++++++++++++++++ .../datatable/edit-jenis-pendaftaran.tsx | 7 +- .../datatable/upload-excel-mahasiswa.tsx | 7 +- .../upload-file-beasiswa-mahasiswa.tsx | 160 ++++ .../upload-file-prestasi-mahasiswa.tsx | 161 ++++ components/ui/Navbar.tsx | 33 +- components/ui/login-dialog.tsx | 39 +- components/ui/toast-provider.tsx | 135 +++ components/ui/toast.tsx | 126 ++- components/ui/toast/README.md | 164 ++++ components/ui/toast/index.ts | 2 + components/ui/use-toast.tsx | 4 +- 28 files changed, 2251 insertions(+), 887 deletions(-) delete mode 100644 app/dashboard/layout.tsx delete mode 100644 app/dashboard/mahasiswa/beasiswa/page.tsx delete mode 100644 app/dashboard/mahasiswa/berprestasi/page.tsx delete mode 100644 app/dashboard/mahasiswa/lulustepatwaktu/page.tsx delete mode 100644 app/dashboard/mahasiswa/profile/page.tsx delete mode 100644 app/dashboard/mahasiswa/status/page.tsx delete mode 100644 app/dashboard/mahasiswa/total/page.tsx delete mode 100644 app/dashboard/page.tsx create mode 100644 components/datatable/data-table-beasiswa-mahasiswa.tsx create mode 100644 components/datatable/data-table-prestasi-mahasiswa.tsx create mode 100644 components/datatable/upload-file-beasiswa-mahasiswa.tsx create mode 100644 components/datatable/upload-file-prestasi-mahasiswa.tsx create mode 100644 components/ui/toast-provider.tsx create mode 100644 components/ui/toast/README.md create mode 100644 components/ui/toast/index.ts diff --git a/app/api/keloladata/data-beasiswa-mahasiswa/route.ts b/app/api/keloladata/data-beasiswa-mahasiswa/route.ts index 9f11517..0dd1563 100644 --- a/app/api/keloladata/data-beasiswa-mahasiswa/route.ts +++ b/app/api/keloladata/data-beasiswa-mahasiswa/route.ts @@ -111,14 +111,13 @@ export async function POST(request: NextRequest) { nim, nama_beasiswa, sumber_beasiswa, - beasiswa_status, jenis_beasiswa } = body; // Validate required fields - if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) { + if (!nim || !nama_beasiswa || !sumber_beasiswa || !jenis_beasiswa) { return NextResponse.json( - { message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, beasiswa_status, jenis_beasiswa' }, + { message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, jenis_beasiswa' }, { status: 400 } ); } @@ -138,16 +137,8 @@ export async function POST(request: NextRequest) { } // Validate enum values - const validStatus = ['Aktif', 'Selesai', 'Dibatalkan']; const validJenisBeasiswa = ['Pemerintah', 'Non-Pemerintah']; - if (!validStatus.includes(beasiswa_status)) { - return NextResponse.json( - { message: 'Invalid beasiswa_status value. Must be one of: Aktif, Selesai, Dibatalkan' }, - { status: 400 } - ); - } - if (!validJenisBeasiswa.includes(jenis_beasiswa)) { return NextResponse.json( { message: 'Invalid jenis_beasiswa value. Must be one of: Pemerintah, Non-Pemerintah' }, @@ -162,7 +153,6 @@ export async function POST(request: NextRequest) { id_mahasiswa: mahasiswaExists.id_mahasiswa, nama_beasiswa, sumber_beasiswa, - beasiswa_status, jenis_beasiswa }) .select() @@ -201,14 +191,13 @@ export async function PUT(request: NextRequest) { nim, nama_beasiswa, sumber_beasiswa, - beasiswa_status, jenis_beasiswa } = body; // Validate required fields - if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) { + if (!nim || !nama_beasiswa || !sumber_beasiswa || !jenis_beasiswa) { return NextResponse.json( - { message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, beasiswa_status, jenis_beasiswa' }, + { message: 'Missing required fields: nim, nama_beasiswa, sumber_beasiswa, jenis_beasiswa' }, { status: 400 } ); } @@ -239,16 +228,8 @@ export async function PUT(request: NextRequest) { } // Validate enum values - const validStatus = ['Aktif', 'Selesai', 'Dibatalkan']; const validJenisBeasiswa = ['Pemerintah', 'Non-Pemerintah']; - if (!validStatus.includes(beasiswa_status)) { - return NextResponse.json( - { message: 'Invalid beasiswa_status value. Must be one of: Aktif, Selesai, Dibatalkan' }, - { status: 400 } - ); - } - if (!validJenisBeasiswa.includes(jenis_beasiswa)) { return NextResponse.json( { message: 'Invalid jenis_beasiswa value. Must be one of: Pemerintah, Non-Pemerintah' }, @@ -263,7 +244,6 @@ export async function PUT(request: NextRequest) { id_mahasiswa: mahasiswaExists.id_mahasiswa, nama_beasiswa, sumber_beasiswa, - beasiswa_status, jenis_beasiswa }) .eq('id_beasiswa', id); diff --git a/app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts b/app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts index 97dcd19..b19eccc 100644 --- a/app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts +++ b/app/api/keloladata/data-beasiswa-mahasiswa/upload/route.ts @@ -2,6 +2,11 @@ import { NextRequest, NextResponse } from 'next/server'; import * as XLSX from 'xlsx'; import supabase from '@/lib/db'; +// GET method for testing the route +export async function GET() { + return NextResponse.json({ message: 'Upload route is working' }); +} + export async function POST(request: NextRequest) { try { // Get form data from request @@ -12,6 +17,11 @@ export async function POST(request: NextRequest) { return NextResponse.json({ message: 'File tidak ditemukan' }, { status: 400 }); } + // Validate file size (max 10MB) + if (file.size > 10 * 1024 * 1024) { + return NextResponse.json({ message: 'File terlalu besar. Maksimal 10MB' }, { status: 400 }); + } + // Process file data based on file type let validData = []; let errors: string[] = []; @@ -65,10 +75,18 @@ async function processExcelData(fileBuffer: ArrayBuffer) { // Parse Excel file const workbook = XLSX.read(fileBuffer, { type: 'array' }); + if (!workbook.SheetNames || workbook.SheetNames.length === 0) { + return { validData: [], errors: ['File Excel tidak memiliki sheet'] }; + } + // Get first sheet const sheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[sheetName]; + if (!worksheet) { + return { validData: [], errors: ['Sheet pertama tidak ditemukan'] }; + } + // Convert to JSON with proper typing const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][]; @@ -76,6 +94,10 @@ async function processExcelData(fileBuffer: ArrayBuffer) { return { validData: [], errors: ['File Excel kosong'] }; } + if (jsonData.length < 2) { + return { validData: [], errors: ['File Excel hanya memiliki header, tidak ada data'] }; + } + // Convert Excel data to CSV-like format for processing const headers = jsonData[0].map(h => String(h).toLowerCase()); const rows = jsonData.slice(1); @@ -84,7 +106,7 @@ async function processExcelData(fileBuffer: ArrayBuffer) { return processData(headers, rows); } catch (error) { console.error('Error processing Excel data:', error); - return { validData: [], errors: [(error as Error).message] }; + return { validData: [], errors: [`Error memproses file Excel: ${(error as Error).message}`] }; } } @@ -113,7 +135,6 @@ function processData(headers: string[], rows: any[][]) { nim: ['nim', 'nomor induk', 'nomor mahasiswa'], nama_beasiswa: ['nama_beasiswa', 'nama beasiswa', 'namabeasiswa', 'beasiswa', 'nama'], sumber_beasiswa: ['sumber_beasiswa', 'sumber beasiswa', 'sumberbeasiswa', 'sumber'], - beasiswa_status: ['beasiswa_status', 'status beasiswa', 'statusbeasiswa', 'status'], jenis_beasiswa: ['jenis_beasiswa', 'jenis beasiswa', 'jenisbeasiswa', 'jenis'] }; @@ -129,19 +150,18 @@ function processData(headers: string[], rows: any[][]) { } // Check required headers - const requiredHeaders = ['nim', 'nama_beasiswa', 'sumber_beasiswa', 'beasiswa_status', 'jenis_beasiswa']; + const requiredHeaders = ['nim', 'nama_beasiswa', 'sumber_beasiswa', 'jenis_beasiswa']; const missingHeaders = requiredHeaders.filter(h => headerMap[h] === undefined); if (missingHeaders.length > 0) { return { validData: [], - errors: [`Kolom berikut tidak ditemukan: ${missingHeaders.join(', ')}. Pastikan file memiliki kolom: NIM, Nama Beasiswa, Sumber Beasiswa, Status Beasiswa, dan Jenis Beasiswa.`] + errors: [`Kolom berikut tidak ditemukan: ${missingHeaders.join(', ')}. Pastikan file memiliki kolom: NIM, Nama Beasiswa, Sumber Beasiswa, dan Jenis Beasiswa.`] }; } const validData = []; const errors = []; - const validStatuses = ['Aktif', 'Selesai', 'Dibatalkan']; const validJenis = ['Pemerintah', 'Non-Pemerintah']; // Process data rows @@ -154,24 +174,14 @@ function processData(headers: string[], rows: any[][]) { const nim = String(values[headerMap.nim] || '').trim(); const nama_beasiswa = String(values[headerMap.nama_beasiswa] || '').trim(); const sumber_beasiswa = String(values[headerMap.sumber_beasiswa] || '').trim(); - let beasiswa_status = String(values[headerMap.beasiswa_status] || '').trim(); let jenis_beasiswa = String(values[headerMap.jenis_beasiswa] || '').trim(); // Validate required fields - if (!nim || !nama_beasiswa || !sumber_beasiswa || !beasiswa_status || !jenis_beasiswa) { + if (!nim || !nama_beasiswa || !sumber_beasiswa || !jenis_beasiswa) { errors.push(`Baris ${i+2}: Data tidak lengkap (NIM: ${nim || 'kosong'})`); continue; } - // Normalize status beasiswa - beasiswa_status = normalizeBeasiswaStatus(beasiswa_status); - - // Validate status beasiswa - if (!validStatuses.includes(beasiswa_status)) { - errors.push(`Baris ${i+2}: Status beasiswa tidak valid "${beasiswa_status}" untuk NIM ${nim}. Harus salah satu dari: ${validStatuses.join(', ')}`); - continue; - } - // Normalize jenis beasiswa jenis_beasiswa = normalizeJenisBeasiswa(jenis_beasiswa); @@ -186,7 +196,6 @@ function processData(headers: string[], rows: any[][]) { nim, nama_beasiswa, sumber_beasiswa, - beasiswa_status, jenis_beasiswa }); @@ -198,25 +207,6 @@ function processData(headers: string[], rows: any[][]) { return { validData, errors }; } -// Function to normalize beasiswa status values -function normalizeBeasiswaStatus(value: string): string { - const lowerValue = value.toLowerCase(); - - if (['aktif', 'active', 'a'].includes(lowerValue)) { - return 'Aktif'; - } - - if (['selesai', 'complete', 'completed', 's', 'finish', 'finished'].includes(lowerValue)) { - return 'Selesai'; - } - - if (['dibatalkan', 'cancel', 'cancelled', 'canceled', 'batal', 'd', 'c'].includes(lowerValue)) { - return 'Dibatalkan'; - } - - return value; // Return original if no match -} - // Function to normalize jenis beasiswa values function normalizeJenisBeasiswa(value: string): string { const lowerValue = value.toLowerCase(); @@ -238,22 +228,13 @@ async function insertDataToDatabase(data: any[]) { let errorCount = 0; const errorMessages: string[] = []; - console.log('=== DEBUG: Starting beasiswa data insertion process ==='); - console.log(`Total data items to process: ${data.length}`); - console.log('Sample data items:', data.slice(0, 3)); - // First, validate all NIMs exist before processing const uniqueNims = [...new Set(data.map(item => item.nim))]; - console.log(`Unique NIMs found: ${uniqueNims.length}`); - console.log('Unique NIMs:', uniqueNims); - const nimValidationMap = new Map(); // Batch check all NIMs for existence - console.log('=== DEBUG: Starting NIM validation ==='); for (const nim of uniqueNims) { try { - console.log(`Checking NIM: ${nim}`); const { data: mahasiswaData, error: checkError } = await supabase .from('mahasiswa') .select('id_mahasiswa, nama') @@ -261,45 +242,28 @@ async function insertDataToDatabase(data: any[]) { .single(); if (checkError || !mahasiswaData) { - console.log(`❌ NIM ${nim}: NOT FOUND in database`); - console.log(`Error details:`, checkError); nimValidationMap.set(nim, { exists: false, error: 'Mahasiswa dengan NIM ini tidak ditemukan dalam database' }); } else { - console.log(`✅ NIM ${nim}: FOUND - ID: ${mahasiswaData.id_mahasiswa}, Nama: ${mahasiswaData.nama}`); nimValidationMap.set(nim, { exists: true, id_mahasiswa: mahasiswaData.id_mahasiswa, nama: mahasiswaData.nama }); } } catch (error) { - console.log(`❌ NIM ${nim}: ERROR during validation`); - console.log(`Error details:`, error); nimValidationMap.set(nim, { exists: false, error: `Error checking NIM: ${(error as Error).message}` }); } } - console.log('=== DEBUG: NIM validation results ==='); - console.log('Validation map:', Object.fromEntries(nimValidationMap)); - // Process each data item - console.log('=== DEBUG: Starting beasiswa data processing ==='); for (const item of data) { try { - console.log(`\n--- Processing beasiswa item: NIM ${item.nim} ---`); - console.log('Item data:', item); - const nimValidation = nimValidationMap.get(item.nim); - console.log('NIM validation result:', nimValidation); if (!nimValidation || !nimValidation.exists) { errorCount++; const errorMsg = nimValidation?.error || `NIM ${item.nim}: Mahasiswa dengan NIM ini tidak ditemukan dalam database`; - console.log(`❌ Skipping item - ${errorMsg}`); errorMessages.push(errorMsg); continue; } - console.log(`✅ NIM ${item.nim} is valid, proceeding with beasiswa check/insert`); - // Check if beasiswa already exists for this mahasiswa and nama_beasiswa - console.log(`Checking existing beasiswa for mahasiswa ID: ${nimValidation.id_mahasiswa}, nama_beasiswa: ${item.nama_beasiswa}`); const { data: existingBeasiswa, error: beasiswaCheckError } = await supabase .from('beasiswa_mahasiswa') .select('id_beasiswa') @@ -307,18 +271,12 @@ async function insertDataToDatabase(data: any[]) { .eq('nama_beasiswa', item.nama_beasiswa) .single(); - if (beasiswaCheckError && beasiswaCheckError.code !== 'PGRST116') { - console.log(`❌ Error checking existing beasiswa:`, beasiswaCheckError); - } - if (existingBeasiswa) { - console.log(`📝 Updating existing beasiswa (ID: ${existingBeasiswa.id_beasiswa})`); // Update existing beasiswa const { error: updateError } = await supabase .from('beasiswa_mahasiswa') .update({ sumber_beasiswa: item.sumber_beasiswa, - beasiswa_status: item.beasiswa_status, jenis_beasiswa: item.jenis_beasiswa }) .eq('id_beasiswa', existingBeasiswa.id_beasiswa); @@ -326,14 +284,10 @@ async function insertDataToDatabase(data: any[]) { if (updateError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal memperbarui beasiswa: ${updateError.message}`; - console.log(`❌ Update failed: ${errorMsg}`); errorMessages.push(errorMsg); continue; - } else { - console.log(`✅ Beasiswa updated successfully`); } } else { - console.log(`📝 Inserting new beasiswa for mahasiswa ID: ${nimValidation.id_mahasiswa}`); // Insert new beasiswa const { error: insertError } = await supabase .from('beasiswa_mahasiswa') @@ -341,34 +295,23 @@ async function insertDataToDatabase(data: any[]) { id_mahasiswa: nimValidation.id_mahasiswa, nama_beasiswa: item.nama_beasiswa, sumber_beasiswa: item.sumber_beasiswa, - beasiswa_status: item.beasiswa_status, jenis_beasiswa: item.jenis_beasiswa }); if (insertError) { errorCount++; const errorMsg = `NIM ${item.nim} (${nimValidation.nama}): Gagal menyimpan beasiswa: ${insertError.message}`; - console.log(`❌ Insert failed: ${errorMsg}`); errorMessages.push(errorMsg); continue; - } else { - console.log(`✅ Beasiswa inserted successfully`); } } imported++; - console.log(`✅ Item processed successfully. Imported count: ${imported}`); } catch (error) { - console.error(`❌ Error processing record for NIM ${item.nim}:`, error); errorCount++; errorMessages.push(`NIM ${item.nim}: Terjadi kesalahan: ${(error as Error).message}`); } } - console.log('=== DEBUG: Final results ==='); - console.log(`Total imported: ${imported}`); - console.log(`Total errors: ${errorCount}`); - console.log(`Error messages:`, errorMessages); - return { imported, errorCount, errorMessages }; } \ No newline at end of file diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx deleted file mode 100644 index 3039a63..0000000 --- a/app/dashboard/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ClientLayout from '@/components/ClientLayout'; - -export default function DashboardLayout({ - children, -}: { - children: React.ReactNode; -}) { - return {children}; -} \ No newline at end of file diff --git a/app/dashboard/mahasiswa/beasiswa/page.tsx b/app/dashboard/mahasiswa/beasiswa/page.tsx deleted file mode 100644 index 7f24132..0000000 --- a/app/dashboard/mahasiswa/beasiswa/page.tsx +++ /dev/null @@ -1,94 +0,0 @@ -'use client'; - -import { useState } from "react"; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import FilterJenisBeasiswa from "@/components/FilterJenisBeasiswa"; -import TotalBeasiswaChart from "@/components/charts/TotalBeasiswaChart"; -import TotalBeasiswaPieChart from "@/components/charts/TotalBeasiswaPieChart"; -import NamaBeasiswaChart from "@/components/charts/NamaBeasiswaChart"; -import NamaBeasiswaPieChart from "@/components/charts/NamaBeasiswaPieChart"; -import JenisPendaftaranBeasiswaChart from "@/components/charts/JenisPendaftaranBeasiswaChart"; -import JenisPendaftaranBeasiswaPieChart from "@/components/charts/JenisPendaftaranBeasiswaPieChart"; -import AsalDaerahBeasiswaChart from "@/components/charts/AsalDaerahBeasiswaChart"; -import IPKBeasiswaChart from "@/components/charts/IPKBeasiswaChart"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function BeasiswaMahasiswaPage() { - const [selectedYear, setSelectedYear] = useState("all"); - const [selectedJenisBeasiswa, setSelectedJenisBeasiswa] = useState("Pemerintah"); - - return ( -
-

Mahasiswa Beasiswa

- -
-

- Mahasiswa yang mendapatkan beasiswa di program studi Informatika Fakultas Teknik Universitas Tanjungpura. -

-
- - - - - Filter Data - - - -
- - -
-
-
- - {selectedYear === "all" ? ( - <> - - - - - ) : ( - <> - - - - - )} - - - - {selectedYear === "all" && ( - - )} -
- ); - } \ No newline at end of file diff --git a/app/dashboard/mahasiswa/berprestasi/page.tsx b/app/dashboard/mahasiswa/berprestasi/page.tsx deleted file mode 100644 index 4601074..0000000 --- a/app/dashboard/mahasiswa/berprestasi/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -'use client'; - -import { useState } from "react"; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import FilterJenisPrestasi from "@/components/FilterJenisPrestasi"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import TotalPrestasiChart from "@/components/charts/TotalPrestasiChart"; -import TotalPrestasiPieChart from "@/components/charts/TotalPrestasiPieChart"; -import TingkatPrestasiChart from "@/components/charts/TingkatPrestasiChart"; -import TingkatPrestasiPieChart from "@/components/charts/TingkatPrestasiPieChart"; -import JenisPendaftaranPrestasiChart from "@/components/charts/JenisPendaftaranPrestasiChart"; -import JenisPendaftaranPrestasiPieChart from "@/components/charts/JenisPendaftaranPrestasiPieChart"; -import AsalDaerahPrestasiChart from "@/components/charts/AsalDaerahPrestasiChart"; -import IPKPrestasiChart from "@/components/charts/IPKPrestasiChart"; - -export default function BerprestasiMahasiswaPage() { - const [selectedYear, setSelectedYear] = useState("all"); - const [selectedJenisPrestasi, setSelectedJenisPrestasi] = useState("Akademik"); - - return ( -
-

Mahasiswa Berprestasi

- -
-

- Mahasiswa yang mendapatkan prestasi akademik dan non akademik di program studi Informatika Fakultas Teknik Universitas Tanjungpura. -

-
- - - - - Filter Data - - - -
- - -
-
-
- - {selectedYear === "all" ? ( - <> - - - - - ) : ( - <> - - - - - )} - - - - {selectedYear === "all" && ( - - )} -
- ); - } \ No newline at end of file diff --git a/app/dashboard/mahasiswa/lulustepatwaktu/page.tsx b/app/dashboard/mahasiswa/lulustepatwaktu/page.tsx deleted file mode 100644 index 543ad20..0000000 --- a/app/dashboard/mahasiswa/lulustepatwaktu/page.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { useState } from "react"; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import LulusTepatWaktuChart from "@/components/charts/LulusTepatWaktuChart"; -import LulusTepatWaktuPieChart from "@/components/charts/LulusTepatWaktuPieChart"; -import JenisPendaftaranLulusChart from "@/components/charts/JenisPendaftaranLulusChart"; -import JenisPendaftaranLulusPieChart from "@/components/charts/JenisPendaftaranLulusPieChart"; -import AsalDaerahLulusChart from "@/components/charts/AsalDaerahLulusChart"; -import IPKLulusTepatChart from "@/components/charts/IPKLulusTepatChart"; - -export default function LulusTepatWaktuPage() { - const [selectedYear, setSelectedYear] = useState("all"); - - return ( -
-

Mahasiswa Lulus Tepat Waktu

- -
-

- Mahasiswa yang lulus tepat waktu sesuai dengan masa studi ≤ 4 tahun program studi Informatika Fakultas Teknik Universitas Tanjungpura. -

-
- - - - - Filter Data - - - -
- -
-
-
- - {selectedYear === "all" ? ( - <> - - - - ) : ( - <> - - - - )} - - - {selectedYear === "all" && ( - - )} - -
- ); - } \ No newline at end of file diff --git a/app/dashboard/mahasiswa/profile/page.tsx b/app/dashboard/mahasiswa/profile/page.tsx deleted file mode 100644 index 1741e35..0000000 --- a/app/dashboard/mahasiswa/profile/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -export default function ProfilePage() { - return ( -
-
- Sedang dalam pengembangan :) -
-
- ); -} diff --git a/app/dashboard/mahasiswa/status/page.tsx b/app/dashboard/mahasiswa/status/page.tsx deleted file mode 100644 index 8b1054c..0000000 --- a/app/dashboard/mahasiswa/status/page.tsx +++ /dev/null @@ -1,86 +0,0 @@ -'use client'; - -import { useState } from "react"; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import FilterStatusKuliah from "@/components/FilterStatusKuliah"; -import StatusMahasiswaFilterChart from "@/components/charts/StatusMahasiswaFilterChart"; -import StatusMahasiswaFilterPieChart from "@/components/charts/StatusMahasiswaFilterPieChart"; -import JenisPendaftaranStatusChart from "@/components/charts/JenisPendaftaranStatusChart"; -import JenisPendaftaranStatusPieChart from "@/components/charts/JenisPendaftaranStatusPieChart"; -import AsalDaerahStatusChart from '@/components/charts/AsalDaerahStatusChart'; -import IpkStatusChart from '@/components/charts/IpkStatusChart'; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function StatusMahasiswaPage() { - const [selectedYear, setSelectedYear] = useState("all"); - const [selectedStatus, setSelectedStatus] = useState("Aktif"); - - return ( -
-

Status Mahasiswa

- -
-

- Mahasiswa status adalah status kuliah mahasiswa program studi Informatika Fakultas Teknik Universitas Tanjungpura. -

-
- - - - - Filter Data - - - -
- - -
-
-
- - {selectedYear === "all" ? ( - <> - - - - ) : ( - <> - - - - )} - - - - {selectedYear === "all" && ( - - )} - -
- ); - } \ No newline at end of file diff --git a/app/dashboard/mahasiswa/total/page.tsx b/app/dashboard/mahasiswa/total/page.tsx deleted file mode 100644 index 12064c9..0000000 --- a/app/dashboard/mahasiswa/total/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; - -import { useState } from "react"; -import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart"; -import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart"; -import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart"; -import AsalDaerahChart from "@/components/charts/AsalDaerahChart"; -import IPKChart from "@/components/charts/IPKChart"; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; -import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -export default function TotalMahasiswaPage() { - const [selectedYear, setSelectedYear] = useState("all"); - - return ( -
-

Total Mahasiswa

- -
-

- Mahasiswa total adalah jumlah mahasiswa program studi Informatika Fakultas Teknik Universitas Tanjungpura. -

-
- - - - - Filter Data - - - -
- -
-
-
- - {selectedYear === "all" ? ( - <> - - - - - - ) : ( - <> - - - - - )} -
- ); -} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx deleted file mode 100644 index da8bb69..0000000 --- a/app/dashboard/page.tsx +++ /dev/null @@ -1,238 +0,0 @@ -'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 { useTheme } from "next-themes"; -import StatusMahasiswaChart from "@/components/charts/StatusMahasiswaChart"; -import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart"; -import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart"; -import AsalDaerahChart from "@/components/charts/AsalDaerahChart"; -import IPKChart from '@/components/charts/IPKChart'; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart"; -import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; -import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; - -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; - ipk_rata_rata_aktif: number; - ipk_rata_rata_lulus: number; - total_mahasiswa_aktif_lulus: number; -} - -// Skeleton loading component -const CardSkeleton = () => ( - - -
-
-
- -
-
-
-
-
-
-
-); - -export default function DashboardPage() { - const { theme } = useTheme(); - const [mahasiswaData, setMahasiswaData] = useState({ - total_mahasiswa: 0, - mahasiswa_aktif: 0, - total_lulus: 0, - pria_lulus: 0, - wanita_lulus: 0, - total_berprestasi: 0, - prestasi_akademik: 0, - prestasi_non_akademik: 0, - ipk_rata_rata_aktif: 0, - ipk_rata_rata_lulus: 0, - total_mahasiswa_aktif_lulus: 0 - }); - const [selectedYear, setSelectedYear] = useState("all"); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - // Menggunakan cache API untuk mempercepat loading - const cacheKey = 'mahasiswa-total-data'; - const cachedData = sessionStorage.getItem(cacheKey); - const cachedTimestamp = sessionStorage.getItem(`${cacheKey}-timestamp`); - - // Cek apakah data cache masih valid (kurang dari 60 detik) - const isCacheValid = cachedTimestamp && - (Date.now() - parseInt(cachedTimestamp)) < 60000; - - if (cachedData && isCacheValid) { - setMahasiswaData(JSON.parse(cachedData)); - } - - // Fetch data total mahasiswa - const totalResponse = await fetch('/api/mahasiswa/total', { - cache: 'no-store', - }); - - if (!totalResponse.ok) { - throw new Error('Failed to fetch total data'); - } - - const totalData = await totalResponse.json(); - setMahasiswaData(totalData); - - // Menyimpan data dan timestamp ke sessionStorage - sessionStorage.setItem(cacheKey, JSON.stringify(totalData)); - sessionStorage.setItem(`${cacheKey}-timestamp`, Date.now().toString()); - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - console.error('Error fetching data:', err); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - return ( -
-

Dashboard Portal Data Informatika

- - {loading ? ( -
- - - - -
- ) : error ? ( -
-
-
- - - -
-
-

{error}

-
-
-
- ) : ( - <> -
- {/* Kartu Total Mahasiswa */} - - - - Total Mahasiswa - - - - -
{mahasiswaData.total_mahasiswa}
-
- Aktif: {mahasiswaData.mahasiswa_aktif} -
-
-
- - {/* Kartu Total Kelulusan */} - - - - Total Kelulusan - - - - -
{mahasiswaData.total_lulus}
-
- Laki-laki: {mahasiswaData.pria_lulus} - Perempuan: {mahasiswaData.wanita_lulus} -
-
-
- - {/* Kartu Total Prestasi */} - - - - Mahasiswa Berprestasi - - - - -
{mahasiswaData.total_berprestasi}
-
- Akademik: {mahasiswaData.prestasi_akademik} - Non-Akademik: {mahasiswaData.prestasi_non_akademik} -
-
-
- - {/* Kartu Rata-rata IPK */} - - - - Rata-rata IPK - - - - -
{mahasiswaData.mahasiswa_aktif}
-
- Aktif: {mahasiswaData.ipk_rata_rata_aktif} -
-
-
-
- - - - - Filter Data - - - -
- -
-
-
- - {selectedYear === "all" ? ( -
- - - - - -
- ) : ( -
- - - -
- )} - - )} -
- ); -} \ No newline at end of file diff --git a/app/keloladata/beasiswa/page.tsx b/app/keloladata/beasiswa/page.tsx index 9667b5a..99083f7 100644 --- a/app/keloladata/beasiswa/page.tsx +++ b/app/keloladata/beasiswa/page.tsx @@ -1,7 +1,9 @@ +import DataTableBeasiswaMahasiswa from "@/components/datatable/data-table-beasiswa-mahasiswa"; + export default function BeasiswaPage() { return ( -
-

Beasiswa

+
+
); } \ No newline at end of file diff --git a/app/keloladata/prestasi/page.tsx b/app/keloladata/prestasi/page.tsx index dc8efc0..80ea0f0 100644 --- a/app/keloladata/prestasi/page.tsx +++ b/app/keloladata/prestasi/page.tsx @@ -1,7 +1,9 @@ +import DataTablePrestasiMahasiswa from "@/components/datatable/data-table-prestasi-mahasiswa"; + export default function PrestasiPage() { return ( -
-

Prestasi

+
+
); } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index ba6607c..d17ad9e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next'; import { Geist, Geist_Mono } from 'next/font/google'; import './globals.css'; import ClientLayout from '@/components/ClientLayout'; +import { ToastProvider } from "@/components/ui/toast-provider"; const geistSans = Geist({ variable: '--font-geist-sans', @@ -33,7 +34,9 @@ export default function RootLayout({ - {children} + + {children} + ); diff --git a/app/page.tsx b/app/page.tsx index d86ade8..839093c 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -4,15 +4,9 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Users, GraduationCap, Trophy, BookOpen } from "lucide-react"; import { useEffect, useState } from "react"; import { useTheme } from "next-themes"; -import StatusMahasiswaChart from "@/components/charts/StatusMahasiswaChart"; import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart"; import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart"; import AsalDaerahChart from "@/components/charts/AsalDaerahChart"; -import IPKChart from '@/components/charts/IPKChart'; -import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; -import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart"; -import JenisPendaftaranPerAngkatanChart from "@/components/charts/JenisPendaftaranPerAngkatanChart"; -import AsalDaerahPerAngkatanChart from "@/components/charts/AsalDaerahPerAngkatanChart"; interface MahasiswaTotal { total_mahasiswa: number; @@ -108,7 +102,7 @@ export default function DashboardPage() { return (
-

Dashboard Portal Data Informatika

+

Visualisasi Data Mahasiswa Informatika

{loading ? (
diff --git a/components/datatable/data-table-beasiswa-mahasiswa.tsx b/components/datatable/data-table-beasiswa-mahasiswa.tsx new file mode 100644 index 0000000..eab1ea9 --- /dev/null +++ b/components/datatable/data-table-beasiswa-mahasiswa.tsx @@ -0,0 +1,685 @@ +"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, + Filter +} from "lucide-react"; +import UploadExcelBeasiswaMahasiswa from "@/components/datatable/upload-file-beasiswa-mahasiswa"; +import { useToast } from "@/components/ui/toast-provider"; + +// Define the BeasiswaMahasiswa type +interface BeasiswaMahasiswa { + id_beasiswa: number; + nim: string; + nama: string; + nama_beasiswa: string; + sumber_beasiswa: string; + jenis_beasiswa: "Pemerintah" | "Non-Pemerintah"; + created_at: string; +} + +export default function DataTableBeasiswaMahasiswa() { + const { showSuccess, showError } = useToast(); + + // State for data + const [beasiswaMahasiswa, setBeasiswaMahasiswa] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for filtering + const [searchTerm, setSearchTerm] = useState(""); + const [filterJenisBeasiswa, setFilterJenisBeasiswa] = 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>({ + jenis_beasiswa: "Pemerintah" + }); + 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(() => { + fetchBeasiswaMahasiswa(); + }, []); + + // Filter data when search term or filter changes + useEffect(() => { + filterData(); + }, [searchTerm, filterJenisBeasiswa, beasiswaMahasiswa]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // Fetch beasiswa mahasiswa data from API + const fetchBeasiswaMahasiswa = async () => { + try { + setLoading(true); + let url = "/api/keloladata/data-beasiswa-mahasiswa"; + + // Add filters to URL if they exist + const params = new URLSearchParams(); + if (searchTerm) { + params.append("search", searchTerm); + } + if (filterJenisBeasiswa && filterJenisBeasiswa !== "all") { + params.append("jenis_beasiswa", filterJenisBeasiswa); + } + + 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(); + setBeasiswaMahasiswa(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 and filters + const filterData = () => { + let filtered = [...beasiswaMahasiswa]; + + // Filter by search term + if (searchTerm) { + filtered = filtered.filter( + (item) => + (item.nim?.toLowerCase() || "").includes(searchTerm.toLowerCase()) || + (item.nama?.toLowerCase() || "").includes(searchTerm.toLowerCase()) || + (item.nama_beasiswa?.toLowerCase() || "").includes(searchTerm.toLowerCase()) || + (item.sumber_beasiswa?.toLowerCase() || "").includes(searchTerm.toLowerCase()) + ); + } + + // Filter by jenis beasiswa + if (filterJenisBeasiswa && filterJenisBeasiswa !== "all") { + filtered = filtered.filter((item) => item.jenis_beasiswa === filterJenisBeasiswa); + } + + 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({ + jenis_beasiswa: "Pemerintah" + }); + }; + + // Handle form input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Handle select input changes + const handleSelectChange = (name: string, value: string) => { + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Open form dialog for adding new beasiswa + const handleAdd = () => { + setFormMode("add"); + resetForm(); + setIsDialogOpen(true); + }; + + // Open form dialog for editing beasiswa + const handleEdit = (data: BeasiswaMahasiswa) => { + 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 beasiswa + const response = await fetch("/api/keloladata/data-beasiswa-mahasiswa", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + showError("Gagal!", `NIM ${formData.nim} tidak terdaftar dalam database. Silakan cek kembali NIM yang dimasukkan.`); + throw new Error(`NIM ${formData.nim} tidak terdaftar. Silakan cek kembali NIM yang dimasukkan.`); + } + showError("Gagal!", "Gagal menambahkan beasiswa"); + throw new Error(responseData.message || "Failed to add beasiswa"); + } + + // Show success message with student info + showSuccess("Berhasil!", "Beasiswa mahasiswa berhasil ditambahkan"); + } else { + // Edit existing beasiswa + const response = await fetch(`/api/keloladata/data-beasiswa-mahasiswa?id=${formData.id_beasiswa}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + throw new Error(`NIM ${formData.nim} tidak terdaftar. Silakan cek kembali NIM yang dimasukkan.`); + } + showError("Gagal!", responseData.message || "Failed to update beasiswa"); + throw new Error(responseData.message || "Failed to update beasiswa"); + } + + showSuccess("Berhasil!", "Beasiswa mahasiswa berhasil diperbarui"); + } + + // Refresh data after successful operation + await fetchBeasiswaMahasiswa(); + setIsDialogOpen(false); + resetForm(); + } catch (err) { + console.error("Error submitting form:", err); + } finally { + setIsSubmitting(false); + } + }; + + // Delete beasiswa + const handleDelete = async () => { + if (!deleteId) return; + + try { + setIsDeleting(true); + + const response = await fetch(`/api/keloladata/data-beasiswa-mahasiswa?id=${deleteId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to delete beasiswa"); + } + + // Refresh data after successful deletion + await fetchBeasiswaMahasiswa(); + setIsDeleteDialogOpen(false); + setDeleteId(null); + showSuccess("Berhasil!", "Beasiswa mahasiswa berhasil dihapus"); + } catch (err) { + console.error("Error deleting beasiswa:", 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 ( +
+
+

Data Beasiswa Mahasiswa

+
+ + +
+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + setSearchTerm("")} + /> + )} +
+ +
+ + {/* Show entries selector */} +
+ Show + + entries +
+ + {/* Table */} + {loading ? ( +
+ +
+ ) : error ? ( +
+ {error} +
+ ) : ( +
+ + + + {/* ID */} + NIM + Nama + Nama Beasiswa + Sumber Beasiswa + Jenis Beasiswa + {/* Tanggal */} + Aksi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang sesuai dengan filter + + + ) : ( + paginatedData.map((beasiswa) => ( + + {/* {beasiswa.id_beasiswa} */} + {beasiswa.nim} + {beasiswa.nama} + {beasiswa.nama_beasiswa} + {beasiswa.sumber_beasiswa} + + + {beasiswa.jenis_beasiswa} + + + +
+ + +
+
+
+ )) + )} +
+
+
+ )} + + {/* 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 */} + + + + + {formMode === "add" ? "Tambah Beasiswa" : "Edit Beasiswa"} + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + + +
+
+
+ + {/* Delete Confirmation Dialog */} + + + + Konfirmasi Hapus + +
+

Apakah Anda yakin ingin menghapus data beasiswa ini?

+

+ Tindakan ini tidak dapat dibatalkan. +

+
+ + + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/components/datatable/data-table-mahasiswa.tsx b/components/datatable/data-table-mahasiswa.tsx index 3b9244d..2b463e4 100644 --- a/components/datatable/data-table-mahasiswa.tsx +++ b/components/datatable/data-table-mahasiswa.tsx @@ -47,7 +47,7 @@ import { } from "lucide-react"; import EditJenisPendaftaran from "@/components/datatable/edit-jenis-pendaftaran"; import UploadExcelMahasiswa from "@/components/datatable/upload-excel-mahasiswa"; - +import { useToast } from "@/components/ui/toast-provider"; // Define the Mahasiswa type based on API route structure interface Mahasiswa { nim: string; @@ -68,6 +68,7 @@ interface Mahasiswa { } export default function DataTableMahasiswa() { + const { showSuccess, showError } = useToast(); // State for data const [mahasiswa, setMahasiswa] = useState([]); const [filteredData, setFilteredData] = useState([]); @@ -161,12 +162,14 @@ export default function DataTableMahasiswa() { const errorData = await response.json(); throw new Error(errorData.message || "Failed to update semesters"); } - - + + showSuccess("Berhasil!", "Semester mahasiswa aktif berhasil diperbarui"); + // Refresh data after successful update await fetchMahasiswa(); } catch (err) { console.error("Error updating semesters:", err); + showError("Gagal!", (err as Error).message); } finally { setIsUpdatingSemester(false); } @@ -337,7 +340,7 @@ export default function DataTableMahasiswa() { const errorData = await response.json(); throw new Error(errorData.message || "Failed to add mahasiswa"); } - + showSuccess("Data mahasiswa berhasil ditambahkan!"); } else { // Edit existing mahasiswa const response = await fetch(`/api/keloladata/data-mahasiswa?nim=${formData.nim}`, { @@ -352,8 +355,8 @@ export default function DataTableMahasiswa() { const errorData = await response.json(); throw new Error(errorData.message || "Failed to update mahasiswa"); } - - } + showSuccess("Data mahasiswa berhasil diperbarui!"); + } // Refresh data after successful operation await fetchMahasiswa(); @@ -361,6 +364,7 @@ export default function DataTableMahasiswa() { resetForm(); } catch (err) { console.error("Error submitting form:", err); + showError(`Gagal ${formMode === "add" ? "menambahkan" : "memperbarui"} data mahasiswa.`); } finally { setIsSubmitting(false); } @@ -387,8 +391,10 @@ export default function DataTableMahasiswa() { await fetchMahasiswa(); setIsDeleteDialogOpen(false); setDeleteNim(null); + showSuccess("Data mahasiswa berhasil dihapus!"); } catch (err) { console.error("Error deleting mahasiswa:", err); + showError("Gagal menghapus data mahasiswa."); } finally { setIsDeleting(false); } diff --git a/components/datatable/data-table-prestasi-mahasiswa.tsx b/components/datatable/data-table-prestasi-mahasiswa.tsx new file mode 100644 index 0000000..780f6a0 --- /dev/null +++ b/components/datatable/data-table-prestasi-mahasiswa.tsx @@ -0,0 +1,797 @@ +"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, + Filter, + Trophy +} from "lucide-react"; +import UploadFilePrestasiMahasiswa from "@/components/datatable/upload-file-prestasi-mahasiswa"; +import { useToast } from "@/components/ui/toast-provider"; + +// Define the PrestasiMahasiswa type +interface PrestasiMahasiswa { + id_prestasi: number; + nim: string; + nama: string; + jenis_prestasi: "Akademik" | "Non-Akademik"; + nama_prestasi: string; + tingkat_prestasi: "Kabupaten" | "Provinsi" | "Nasional" | "Internasional"; + peringkat: string; + tanggal_prestasi: string; + keterangan: string | null; + created_at: string; +} + +export default function DataTablePrestasiMahasiswa() { + const { showSuccess, showError } = useToast(); + // State for data + const [prestasiMahasiswa, setPrestasiMahasiswa] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for filtering + const [searchTerm, setSearchTerm] = useState(""); + const [filterJenisPrestasi, setFilterJenisPrestasi] = useState(""); + const [filterTingkat, setFilterTingkat] = 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>({ + jenis_prestasi: "Akademik", + tingkat_prestasi: "Nasional" + }); + 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(() => { + fetchPrestasiMahasiswa(); + }, []); + + // Filter data when search term or filter changes + useEffect(() => { + filterData(); + }, [searchTerm, filterJenisPrestasi, filterTingkat, prestasiMahasiswa]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // Fetch prestasi mahasiswa data from API + const fetchPrestasiMahasiswa = async () => { + try { + setLoading(true); + let url = "/api/keloladata/data-prestasi-mahasiswa"; + + // Add filters to URL if they exist + const params = new URLSearchParams(); + if (searchTerm) { + params.append("search", searchTerm); + } + if (filterJenisPrestasi && filterJenisPrestasi !== "all") { + params.append("jenis_prestasi", filterJenisPrestasi); + } + if (filterTingkat && filterTingkat !== "all") { + params.append("tingkat_prestasi", filterTingkat); + } + + 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(); + setPrestasiMahasiswa(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 and filters + const filterData = () => { + let filtered = [...prestasiMahasiswa]; + + // Filter by search term + if (searchTerm) { + filtered = filtered.filter( + (item) => + (item.nim && item.nim.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.nama && item.nama.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.nama_prestasi && item.nama_prestasi.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.peringkat && item.peringkat.toLowerCase().includes(searchTerm.toLowerCase())) || + (item.keterangan && item.keterangan.toLowerCase().includes(searchTerm.toLowerCase())) + ); + } + + // Filter by jenis prestasi + if (filterJenisPrestasi && filterJenisPrestasi !== "all") { + filtered = filtered.filter((item) => item.jenis_prestasi === filterJenisPrestasi); + } + + // Filter by tingkat + if (filterTingkat && filterTingkat !== "all") { + filtered = filtered.filter((item) => item.tingkat_prestasi === filterTingkat); + } + + 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 + }; + + // Format date for display + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("id-ID", { + day: "numeric", + month: "long", + year: "numeric", + }); + }; + + // Reset form data + const resetForm = () => { + setFormData({ + jenis_prestasi: "Akademik", + tingkat_prestasi: "Nasional" + }); + }; + + // Handle form input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Handle select input changes + const handleSelectChange = (name: string, value: string) => { + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Open form dialog for adding new prestasi + const handleAdd = () => { + setFormMode("add"); + resetForm(); + setIsDialogOpen(true); + }; + + // Open form dialog for editing prestasi + const handleEdit = (data: PrestasiMahasiswa) => { + setFormMode("edit"); + // Format the date for the input field (YYYY-MM-DD) + const formattedData = { + ...data, + tanggal_prestasi: new Date(data.tanggal_prestasi).toISOString().split('T')[0] + }; + setFormData(formattedData); + 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 prestasi + const response = await fetch("/api/keloladata/data-prestasi-mahasiswa", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + throw new Error(`NIM ${formData.nim} tidak terdaftar dalam database. Silakan cek kembali NIM yang dimasukkan.`); + } + throw new Error(responseData.message || "Failed to add prestasi"); + } + + // Show success message with student info + showSuccess("Berhasil!", "Prestasi mahasiswa berhasil ditambahkan"); + } else { + // Edit existing prestasi + const response = await fetch(`/api/keloladata/data-prestasi-mahasiswa?id=${formData.id_prestasi}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + const responseData = await response.json(); + + if (!response.ok) { + // Handle specific NIM not found error + if (response.status === 404 && responseData.message.includes("tidak terdaftar")) { + throw new Error(`NIM ${formData.nim} tidak terdaftar dalam database. Silakan cek kembali NIM yang dimasukkan.`); + } + throw new Error(responseData.message || "Failed to update prestasi"); + } + + showSuccess("Berhasil!", "Prestasi mahasiswa berhasil diperbarui"); + } + + // Refresh data after successful operation + await fetchPrestasiMahasiswa(); + setIsDialogOpen(false); + resetForm(); + } catch (err) { + console.error("Error submitting form:", err); + } finally { + setIsSubmitting(false); + } + }; + + // Delete prestasi + const handleDelete = async () => { + if (!deleteId) return; + + try { + setIsDeleting(true); + + const response = await fetch(`/api/keloladata/data-prestasi-mahasiswa?id=${deleteId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to delete prestasi"); + } + + // Refresh data after successful deletion + await fetchPrestasiMahasiswa(); + setIsDeleteDialogOpen(false); + setDeleteId(null); + showSuccess("Berhasil!", "Prestasi mahasiswa berhasil dihapus"); + } catch (err) { + console.error("Error deleting prestasi:", 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 }; + }; + + // Get badge color based on tingkat + const getTingkatBadgeColor = (tingkat: string) => { + switch (tingkat) { + case "Kabupaten": + return "bg-green-100 text-green-800"; + case "Provinsi": + return "bg-blue-100 text-blue-800"; + case "Nasional": + return "bg-purple-100 text-purple-800"; + case "Internasional": + return "bg-red-100 text-red-800"; + default: + return "bg-gray-100 text-gray-800"; + } + }; + + return ( +
+
+

Data Prestasi Mahasiswa

+
+ + +
+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + /> + {searchTerm && ( + setSearchTerm("")} + /> + )} +
+ + +
+ + {/* Show entries selector */} +
+ Show + + entries +
+ + {/* Table */} + {loading ? ( +
+ +
+ ) : error ? ( +
+ {error} +
+ ) : ( +
+ + + + {/* ID */} + NIM + Nama + Jenis + Nama Prestasi + Tingkat + Peringkat + Tanggal + Aksi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang sesuai dengan filter + + + ) : ( + paginatedData.map((prestasi) => ( + + {/* {prestasi.id_prestasi} */} + {prestasi.nim} + {prestasi.nama} + + + {prestasi.jenis_prestasi} + + + {prestasi.nama_prestasi} + + + {prestasi.tingkat_prestasi} + + + {prestasi.peringkat} + {formatDate(prestasi.tanggal_prestasi)} + +
+ + +
+
+
+ )) + )} +
+
+
+ )} + + {/* 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 */} + + + + + {formMode === "add" ? "Tambah Prestasi" : "Edit Prestasi"} + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +