add new feature

This commit is contained in:
Randa Firman Putra
2025-09-11 13:51:01 +07:00
parent 61a08dc212
commit d6d8fc3b32
2 changed files with 413 additions and 0 deletions

View File

@@ -0,0 +1,411 @@
"use client";
import { useState, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
User,
Loader2,
Calendar,
MapPin,
GraduationCap,
Trophy,
DollarSign,
Mail,
Phone,
BookOpen,
Award
} from "lucide-react";
// Interface definitions
interface MahasiswaData {
nim: string;
nama: string;
jk: "Pria" | "Wanita";
agama: string | null;
kabupaten: string | null;
provinsi: string | null;
jenis_pendaftaran: string | null;
tahun_angkatan: string;
ipk: number | null;
id_kelompok_keahlian: number | null;
nama_kelompok_keahlian: string | null;
status_kuliah: "Aktif" | "Cuti" | "Lulus" | "Non-Aktif";
semester: number;
created_at: string;
updated_at: string;
}
interface BeasiswaData {
id_beasiswa: number;
nim: string;
nama: string;
nama_beasiswa: string;
sumber_beasiswa: string;
jenis_beasiswa: "Pemerintah" | "Non-Pemerintah";
created_at: string;
}
interface PrestasiData {
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;
}
interface BiodataMahasiswaDialogProps {
nim: string;
nama: string;
}
export default function BiodataMahasiswaDialog({ nim, nama }: BiodataMahasiswaDialogProps) {
const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Data states
const [mahasiswaData, setMahasiswaData] = useState<MahasiswaData | null>(null);
const [beasiswaData, setBeasiswaData] = useState<BeasiswaData[]>([]);
const [prestasiData, setPrestasiData] = useState<PrestasiData[]>([]);
// Fetch all data when dialog opens
const fetchBiodataData = async () => {
try {
setLoading(true);
setError(null);
// Fetch mahasiswa data
const mahasiswaResponse = await fetch(`/api/keloladata/data-mahasiswa`);
if (!mahasiswaResponse.ok) {
throw new Error("Failed to fetch mahasiswa data");
}
const mahasiswaResult = await mahasiswaResponse.json();
// Find specific student data
const studentData = Array.isArray(mahasiswaResult)
? mahasiswaResult.find((m: MahasiswaData) => m.nim === nim)
: null;
setMahasiswaData(studentData);
// Fetch beasiswa data
try {
const beasiswaResponse = await fetch(`/api/keloladata/data-beasiswa-mahasiswa`);
if (beasiswaResponse.ok) {
const beasiswaResult = await beasiswaResponse.json();
const filteredBeasiswa = Array.isArray(beasiswaResult)
? beasiswaResult.filter((b: BeasiswaData) => b.nim === nim)
: [];
setBeasiswaData(filteredBeasiswa);
} else {
setBeasiswaData([]);
}
} catch (err) {
console.warn("Beasiswa data not available:", err);
setBeasiswaData([]);
}
// Fetch prestasi data
try {
const prestasiResponse = await fetch(`/api/keloladata/data-prestasi-mahasiswa`);
if (prestasiResponse.ok) {
const prestasiResult = await prestasiResponse.json();
const filteredPrestasi = Array.isArray(prestasiResult)
? prestasiResult.filter((p: PrestasiData) => p.nim === nim)
: [];
setPrestasiData(filteredPrestasi);
} else {
setPrestasiData([]);
}
} catch (err) {
console.warn("Prestasi data not available:", err);
setPrestasiData([]);
}
} catch (err) {
console.error("Error fetching biodata:", err);
setError(err instanceof Error ? err.message : "Failed to fetch data");
} finally {
setLoading(false);
}
};
// Handle dialog open
const handleOpenChange = (open: boolean) => {
setIsOpen(open);
if (open) {
fetchBiodataData();
} else {
// Reset data when closing
setMahasiswaData(null);
setBeasiswaData([]);
setPrestasiData([]);
setError(null);
}
};
// Get status badge color
const getStatusColor = (status: string) => {
switch (status?.toLowerCase()) {
case "aktif":
return "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100";
case "cuti":
return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100";
case "lulus":
return "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100";
case "non-aktif":
return "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100";
default:
return "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-100";
}
};
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button size="sm" variant="outline" className="flex items-center gap-1">
<User className="h-4 w-4" />
<span className="hidden sm:inline">Detail</span>
</Button>
</DialogTrigger>
<DialogContent className="w-[95vw] max-w-4xl max-h-[90vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Profil Mahasiswa - {nama}
</DialogTitle>
</DialogHeader>
{loading ? (
<div className="flex justify-center items-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-2">Memuat biodata...</span>
</div>
) : error ? (
<div className="bg-destructive/10 p-4 rounded-md text-destructive text-center">
Error: {error}
</div>
) : (
<div className="space-y-2">
{/* Data Pribadi */}
{mahasiswaData && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<User className="h-5 w-5" />
Data Pribadi
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">NIM</label>
<p className="font-semi">{mahasiswaData.nim}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Nama Lengkap</label>
<p className="font-semi">{mahasiswaData.nama}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Jenis Kelamin</label>
<p>{mahasiswaData.jk}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Agama</label>
<p>{mahasiswaData.agama || "-"}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Kabupaten</label>
<p className="flex items-center gap-1">
{mahasiswaData.kabupaten || "-"}
</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Provinsi</label>
<p className="flex items-center gap-1">
{mahasiswaData.provinsi || "-"}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Data Akademik */}
{mahasiswaData && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
Data Akademik
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-4">
<div>
<label className="text-sm font-medium text-muted-foreground">Tahun Angkatan</label>
<p className="flex items-center gap-1">
{mahasiswaData.tahun_angkatan}
</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Jenis Pendaftaran</label>
<p>{mahasiswaData.jenis_pendaftaran || "-"}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Semester</label>
<p className="flex items-center gap-1">
{mahasiswaData.semester}
</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">IPK</label>
<p className="font-semi text-lg">
{mahasiswaData.ipk ? Number(mahasiswaData.ipk).toFixed(2) : "-"}
</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Status Kuliah</label>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(mahasiswaData.status_kuliah)}`}>
{mahasiswaData.status_kuliah}
</span>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Kelompok Keahlian</label>
<p>{mahasiswaData.nama_kelompok_keahlian || "-"}</p>
</div>
</div>
</CardContent>
</Card>
)}
<div className="border-t border-gray-200 dark:border-gray-700"></div>
{/* Data Beasiswa */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BookOpen className="h-5 w-5" />
Riwayat Beasiswa
</CardTitle>
</CardHeader>
<CardContent>
{beasiswaData.length === 0 ? (
<p className="text-muted-foreground text-center py-4">
Tidak ada riwayat beasiswa
</p>
) : (
<div className="space-y-3">
{beasiswaData.map((beasiswa) => (
<div key={beasiswa.id_beasiswa} className="border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label className="text-sm font-medium text-muted-foreground">Nama Beasiswa</label>
<p className="font-semibold">{beasiswa.nama_beasiswa}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Sumber Beasiswa</label>
<p>{beasiswa.sumber_beasiswa}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Jenis Beasiswa</label>
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
beasiswa.jenis_beasiswa === "Pemerintah"
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100"
: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-100"
}`}>
{beasiswa.jenis_beasiswa}
</span>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Data Prestasi */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Trophy className="h-5 w-5" />
Riwayat Prestasi
</CardTitle>
</CardHeader>
<CardContent>
{prestasiData.length === 0 ? (
<p className="text-muted-foreground text-center py-4">
Tidak ada riwayat prestasi
</p>
) : (
<div className="space-y-3">
{prestasiData.map((prestasi) => (
<div key={prestasi.id_prestasi} className="border rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div>
<label className="text-sm font-medium text-muted-foreground">Nama Prestasi</label>
<p className="font-semibold">{prestasi.nama_prestasi}</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Jenis Prestasi</label>
<span className={`inline-flex items-center px-2 py-1 rounded-md text-xs font-medium ${
prestasi.jenis_prestasi === "Akademik"
? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100"
: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100"
}`}>
{prestasi.jenis_prestasi}
</span>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Tingkat Prestasi</label>
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100">
<Award className="h-3 w-3 mr-1" />
{prestasi.tingkat_prestasi}
</span>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Peringkat</label>
<p className="flex items-center gap-1 font-semibold text-orange-600">
<Trophy className="h-4 w-4" />
{prestasi.peringkat}
</p>
</div>
<div>
<label className="text-sm font-medium text-muted-foreground">Tanggal Prestasi</label>
<p className="flex items-center gap-1">
<Calendar className="h-4 w-4 text-muted-foreground" />
{new Date(prestasi.tanggal_prestasi).toLocaleDateString('id-ID', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
</p>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
)}
</DialogContent>
</Dialog>
);
}

View File

@@ -48,6 +48,7 @@ import {
} from "lucide-react";
import EditJenisPendaftaran from "@/components/datatable/edit-jenis-pendaftaran";
import UploadExcelMahasiswa from "@/components/datatable/upload-excel-mahasiswa";
import BiodataMahasiswaDialog from "@/components/datatable/biodata-mahasiswa-dialog";
import { useToast } from "@/components/ui/toast-provider";
// Define the Mahasiswa type based on API route structure
@@ -788,6 +789,7 @@ export default function DataTableMahasiswa() {
<TableCell>{mhs.nama_kelompok_keahlian || "-"}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<BiodataMahasiswaDialog nim={mhs.nim} nama={mhs.nama} />
<Button
size="sm"
variant="outline"