Coba terus

This commit is contained in:
Randa Firman Putra
2025-08-23 23:30:40 +07:00
parent 7da0df7af0
commit 78d76c8f4f
2 changed files with 490 additions and 13 deletions

View File

@@ -0,0 +1,214 @@
import { NextRequest, NextResponse } from 'next/server';
import supabase from '@/lib/db';
// GET - Get all kelompok keahlian
export async function GET() {
try {
const { data, error } = await supabase
.from('kelompok_keahlian')
.select('id_kk, nama_kelompok')
.order('id_kk', { ascending: true });
if (error) {
console.error('Error fetching kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to fetch kelompok keahlian' },
{ status: 500 }
);
}
return NextResponse.json(data);
} catch (error) {
console.error('Error fetching kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to fetch kelompok keahlian' },
{ status: 500 }
);
}
}
// POST - Create new kelompok keahlian
export async function POST(request: NextRequest) {
try {
const { nama_kelompok } = await request.json();
if (!nama_kelompok || nama_kelompok.trim() === '') {
return NextResponse.json(
{ error: 'Nama kelompok keahlian is required' },
{ status: 400 }
);
}
// Check if nama_kelompok already exists
const { data: existingData, error: existingError } = await supabase
.from('kelompok_keahlian')
.select('id_kk')
.ilike('nama_kelompok', nama_kelompok.trim());
if (existingError) {
console.error('Error checking existing kelompok keahlian:', existingError);
return NextResponse.json(
{ error: 'Failed to check existing kelompok keahlian' },
{ status: 500 }
);
}
if (existingData && existingData.length > 0) {
return NextResponse.json(
{ error: 'Kelompok keahlian dengan nama tersebut sudah ada' },
{ status: 409 }
);
}
const { data, error } = await supabase
.from('kelompok_keahlian')
.insert([{ nama_kelompok: nama_kelompok.trim() }])
.select('id_kk, nama_kelompok')
.single();
if (error) {
console.error('Error creating kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to create kelompok keahlian' },
{ status: 500 }
);
}
return NextResponse.json(data, { status: 201 });
} catch (error) {
console.error('Error creating kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to create kelompok keahlian' },
{ status: 500 }
);
}
}
// PUT - Update kelompok keahlian
export async function PUT(request: NextRequest) {
try {
const { id_kk, nama_kelompok } = await request.json();
if (!id_kk || !nama_kelompok || nama_kelompok.trim() === '') {
return NextResponse.json(
{ error: 'ID dan nama kelompok keahlian are required' },
{ status: 400 }
);
}
// Check if kelompok keahlian exists
const { data: existingData, error: existingError } = await supabase
.from('kelompok_keahlian')
.select('id_kk')
.eq('id_kk', id_kk)
.single();
if (existingError || !existingData) {
return NextResponse.json(
{ error: 'Kelompok keahlian tidak ditemukan' },
{ status: 404 }
);
}
// Check if nama_kelompok already exists for other records
const { data: duplicateData, error: duplicateError } = await supabase
.from('kelompok_keahlian')
.select('id_kk')
.ilike('nama_kelompok', nama_kelompok.trim())
.neq('id_kk', id_kk);
if (duplicateError) {
console.error('Error checking duplicate kelompok keahlian:', duplicateError);
return NextResponse.json(
{ error: 'Failed to check duplicate kelompok keahlian' },
{ status: 500 }
);
}
if (duplicateData && duplicateData.length > 0) {
return NextResponse.json(
{ error: 'Kelompok keahlian dengan nama tersebut sudah ada' },
{ status: 409 }
);
}
const { data, error } = await supabase
.from('kelompok_keahlian')
.update({ nama_kelompok: nama_kelompok.trim() })
.eq('id_kk', id_kk)
.select('id_kk, nama_kelompok')
.single();
if (error) {
console.error('Error updating kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to update kelompok keahlian' },
{ status: 500 }
);
}
return NextResponse.json(data);
} catch (error) {
console.error('Error updating kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to update kelompok keahlian' },
{ status: 500 }
);
}
}
// DELETE - Delete kelompok keahlian
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const id_kk = searchParams.get('id_kk');
if (!id_kk) {
return NextResponse.json(
{ error: 'ID kelompok keahlian is required' },
{ status: 400 }
);
}
// Check if kelompok keahlian exists
const { data: existingData, error: existingError } = await supabase
.from('kelompok_keahlian')
.select('id_kk')
.eq('id_kk', id_kk)
.single();
if (existingError || !existingData) {
return NextResponse.json(
{ error: 'Kelompok keahlian tidak ditemukan' },
{ status: 404 }
);
}
// Check if kelompok keahlian is being used in other tables
// You might want to add foreign key checks here depending on your database structure
const { error } = await supabase
.from('kelompok_keahlian')
.delete()
.eq('id_kk', id_kk);
if (error) {
console.error('Error deleting kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to delete kelompok keahlian' },
{ status: 500 }
);
}
return NextResponse.json(
{ message: 'Kelompok keahlian berhasil dihapus' },
{ status: 200 }
);
} catch (error) {
console.error('Error deleting kelompok keahlian:', error);
return NextResponse.json(
{ error: 'Failed to delete kelompok keahlian' },
{ status: 500 }
);
}
}

View File

@@ -43,11 +43,13 @@ import {
Search,
X,
Loader2,
RefreshCw
RefreshCw,
Users
} 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;
@@ -67,6 +69,12 @@ interface Mahasiswa {
updated_at: string;
}
// Define the KelompokKeahlian type
interface KelompokKeahlian {
id_kk: number;
nama_kelompok: string;
}
export default function DataTableMahasiswa() {
const { showSuccess, showError } = useToast();
// State for data
@@ -107,11 +115,22 @@ export default function DataTableMahasiswa() {
// State for kelompok keahlian options
const [kelompokKeahlianOptions, setKelompokKeahlianOptions] = useState<Array<{id_kk: number, nama_kelompok: string}>>([]);
// State for kelompok keahlian CRUD
const [kelompokKeahlianData, setKelompokKeahlianData] = useState<KelompokKeahlian[]>([]);
const [kelompokKeahlianFormMode, setKelompokKeahlianFormMode] = useState<"add" | "edit">("add");
const [kelompokKeahlianFormData, setKelompokKeahlianFormData] = useState<Partial<KelompokKeahlian>>({});
const [isKelompokKeahlianSubmitting, setIsKelompokKeahlianSubmitting] = useState(false);
const [isKelompokKeahlianDialogOpen, setIsKelompokKeahlianDialogOpen] = useState(false);
const [deleteKelompokKeahlianId, setDeleteKelompokKeahlianId] = useState<number | null>(null);
const [isKelompokKeahlianDeleting, setIsKelompokKeahlianDeleting] = useState(false);
const [isKelompokKeahlianDeleteDialogOpen, setIsKelompokKeahlianDeleteDialogOpen] = useState(false);
// Fetch data on component mount
useEffect(() => {
fetchMahasiswa();
fetchJenisPendaftaranOptions();
fetchKelompokKeahlianOptions();
fetchKelompokKeahlianData();
}, []);
// Filter data when search term or filter changes
@@ -146,6 +165,22 @@ export default function DataTableMahasiswa() {
}
};
// Fetch kelompok keahlian data
const fetchKelompokKeahlianData = async () => {
try {
const response = await fetch("/api/keloladata/kelompok-keahlian");
if (!response.ok) {
throw new Error("Failed to fetch kelompok keahlian data");
}
const data = await response.json();
setKelompokKeahlianData(data);
} catch (err) {
console.error("Error fetching kelompok keahlian data:", err);
}
};
// Update semester for active students
const handleUpdateSemester = async () => {
try {
@@ -400,6 +435,104 @@ export default function DataTableMahasiswa() {
}
};
// Kelompok Keahlian CRUD functions
const handleKelompokKeahlianAdd = () => {
setKelompokKeahlianFormMode("add");
setKelompokKeahlianFormData({});
setIsKelompokKeahlianDialogOpen(true);
};
const handleKelompokKeahlianEdit = (data: KelompokKeahlian) => {
setKelompokKeahlianFormMode("edit");
setKelompokKeahlianFormData(data);
setIsKelompokKeahlianDialogOpen(true);
};
const handleKelompokKeahlianDeleteConfirm = (id: number) => {
setDeleteKelompokKeahlianId(id);
setIsKelompokKeahlianDeleteDialogOpen(true);
};
const handleKelompokKeahlianSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setIsKelompokKeahlianSubmitting(true);
if (kelompokKeahlianFormMode === "add") {
// Add new kelompok keahlian
const response = await fetch("/api/keloladata/kelompok-keahlian", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(kelompokKeahlianFormData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to add kelompok keahlian");
}
showSuccess("Kelompok keahlian berhasil ditambahkan!");
} else {
// Edit existing kelompok keahlian
const response = await fetch("/api/keloladata/kelompok-keahlian", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(kelompokKeahlianFormData),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to update kelompok keahlian");
}
showSuccess("Kelompok keahlian berhasil diperbarui!");
}
// Refresh data after successful operation
await fetchKelompokKeahlianData();
await fetchKelompokKeahlianOptions();
setIsKelompokKeahlianDialogOpen(false);
setKelompokKeahlianFormData({});
} catch (err) {
console.error("Error submitting kelompok keahlian form:", err);
showError(`Gagal ${kelompokKeahlianFormMode === "add" ? "menambahkan" : "memperbarui"} kelompok keahlian.`);
} finally {
setIsKelompokKeahlianSubmitting(false);
}
};
const handleKelompokKeahlianDelete = async () => {
if (!deleteKelompokKeahlianId) return;
try {
setIsKelompokKeahlianDeleting(true);
const response = await fetch(`/api/keloladata/kelompok-keahlian?id_kk=${deleteKelompokKeahlianId}`, {
method: "DELETE",
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to delete kelompok keahlian");
}
// Refresh data after successful deletion
await fetchKelompokKeahlianData();
await fetchKelompokKeahlianOptions();
setIsKelompokKeahlianDeleteDialogOpen(false);
setDeleteKelompokKeahlianId(null);
showSuccess("Kelompok keahlian berhasil dihapus!");
} catch (err) {
console.error("Error deleting kelompok keahlian:", err);
showError("Gagal menghapus kelompok keahlian.");
} finally {
setIsKelompokKeahlianDeleting(false);
}
};
// Get unique angkatan years for filter
const getUniqueAngkatan = () => {
const years = new Set<string>();
@@ -505,6 +638,13 @@ export default function DataTableMahasiswa() {
)}
Update Semester Mahasiswa Aktif
</Button>
<Button
variant="outline"
onClick={handleKelompokKeahlianAdd}
>
<Users className="mr-2 h-4 w-4" />
Kelola Kelompok Keahlian
</Button>
<EditJenisPendaftaran onUpdateSuccess={fetchMahasiswa} />
<UploadExcelMahasiswa onUploadSuccess={fetchMahasiswa} />
</div>
@@ -701,17 +841,17 @@ export default function DataTableMahasiswa() {
</div>
)}
{/* Add/Edit Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="sm:max-w-[900px] max-h-[90vh] overflow-y-auto">
{/* Add/Edit Dialog */}
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="w-[95vw] max-w-[1200px] max-h-[90vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>
{formMode === "add" ? "Tambah Mahasiswa" : "Edit Mahasiswa"}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="grid gap-6 py-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="grid gap-4 sm:gap-6 py-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4">
<div className="space-y-2">
<label htmlFor="nim" className="text-sm font-medium">
NIM <span className="text-destructive">*</span>
@@ -838,7 +978,7 @@ export default function DataTableMahasiswa() {
value={formData.id_kelompok_keahlian?.toString() || ""}
onValueChange={(value) => handleSelectChange("id_kelompok_keahlian", value)}
>
<SelectTrigger>
<SelectTrigger className="w-full">
<SelectValue placeholder="Pilih Kelompok Keahlian" />
</SelectTrigger>
<SelectContent>
@@ -886,13 +1026,13 @@ export default function DataTableMahasiswa() {
</div>
</div>
</div>
<DialogFooter>
<DialogFooter className="flex flex-col sm:flex-row gap-2 sm:gap-0">
<DialogClose asChild>
<Button type="button" variant="outline">
<Button type="button" variant="outline" className="w-full sm:w-auto">
Batal
</Button>
</DialogClose>
<Button type="submit" disabled={isSubmitting}>
<Button type="submit" disabled={isSubmitting} className="w-full sm:w-auto">
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{formMode === "add" ? "Tambah" : "Simpan"}
</Button>
@@ -903,7 +1043,7 @@ export default function DataTableMahasiswa() {
{/* Delete Confirmation Dialog */}
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent className="sm:max-w-[400px]">
<DialogContent className="w-[95vw] max-w-[400px] p-4 sm:p-6">
<DialogHeader>
<DialogTitle>Konfirmasi Hapus</DialogTitle>
</DialogHeader>
@@ -913,9 +1053,9 @@ export default function DataTableMahasiswa() {
Tindakan ini tidak dapat dibatalkan.
</p>
</div>
<DialogFooter>
<DialogFooter className="flex flex-col sm:flex-row gap-2 sm:gap-0">
<DialogClose asChild>
<Button type="button" variant="outline">
<Button type="button" variant="outline" className="w-full sm:w-auto">
Batal
</Button>
</DialogClose>
@@ -923,6 +1063,7 @@ export default function DataTableMahasiswa() {
variant="destructive"
onClick={handleDelete}
disabled={isDeleting}
className="w-full sm:w-auto"
>
{isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Hapus
@@ -930,6 +1071,128 @@ export default function DataTableMahasiswa() {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Kelompok Keahlian CRUD Dialog */}
<Dialog open={isKelompokKeahlianDialogOpen} onOpenChange={setIsKelompokKeahlianDialogOpen}>
<DialogContent className="w-[95vw] max-w-[600px] max-h-[90vh] overflow-y-auto p-4 sm:p-6">
<DialogHeader>
<DialogTitle>
{kelompokKeahlianFormMode === "add" ? "Tambah Kelompok Keahlian" : "Edit Kelompok Keahlian"}
</DialogTitle>
</DialogHeader>
{/* Kelompok Keahlian Table */}
<div className="border rounded-md mb-4 overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[60px]">ID</TableHead>
<TableHead className="min-w-[200px]">Nama Kelompok</TableHead>
<TableHead className="text-right min-w-[100px]">Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{kelompokKeahlianData.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-center py-4">
Tidak ada data kelompok keahlian
</TableCell>
</TableRow>
) : (
kelompokKeahlianData.map((kelompok) => (
<TableRow key={kelompok.id_kk}>
<TableCell className="font-medium">{kelompok.id_kk}</TableCell>
<TableCell className="break-words">{kelompok.nama_kelompok}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1 sm:gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleKelompokKeahlianEdit(kelompok)}
className="h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
>
<Pencil className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
<Button
size="sm"
variant="outline"
className="text-destructive hover:bg-destructive/10 h-8 w-8 p-0 sm:h-9 sm:w-auto sm:px-3"
onClick={() => handleKelompokKeahlianDeleteConfirm(kelompok.id_kk)}
>
<Trash2 className="h-3 w-3 sm:h-4 sm:w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Kelompok Keahlian Form */}
<form onSubmit={handleKelompokKeahlianSubmit}>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label htmlFor="nama_kelompok" className="text-sm font-medium">
Nama Kelompok Keahlian <span className="text-destructive">*</span>
</label>
<Input
id="nama_kelompok"
name="nama_kelompok"
value={kelompokKeahlianFormData.nama_kelompok || ""}
onChange={(e) => setKelompokKeahlianFormData(prev => ({ ...prev, nama_kelompok: e.target.value }))}
required
placeholder="Masukkan nama kelompok keahlian"
className="w-full"
/>
</div>
</div>
<DialogFooter className="flex flex-col sm:flex-row gap-2 sm:gap-0">
<DialogClose asChild>
<Button type="button" variant="outline" className="w-full sm:w-auto">
Tutup
</Button>
</DialogClose>
<Button type="submit" disabled={isKelompokKeahlianSubmitting} className="w-full sm:w-auto">
{isKelompokKeahlianSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{kelompokKeahlianFormMode === "add" ? "Tambah" : "Simpan"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Kelompok Keahlian Delete Confirmation Dialog */}
<Dialog open={isKelompokKeahlianDeleteDialogOpen} onOpenChange={setIsKelompokKeahlianDeleteDialogOpen}>
<DialogContent className="w-[95vw] max-w-[400px] p-4 sm:p-6">
<DialogHeader>
<DialogTitle>Konfirmasi Hapus Kelompok Keahlian</DialogTitle>
</DialogHeader>
<div className="py-4">
<p>Apakah Anda yakin ingin menghapus kelompok keahlian ini?</p>
<p className="text-sm text-muted-foreground mt-1">
Tindakan ini tidak dapat dibatalkan.
</p>
</div>
<DialogFooter className="flex flex-col sm:flex-row gap-2 sm:gap-0">
<DialogClose asChild>
<Button type="button" variant="outline" className="w-full sm:w-auto">
Batal
</Button>
</DialogClose>
<Button
variant="destructive"
onClick={handleKelompokKeahlianDelete}
disabled={isKelompokKeahlianDeleting}
className="w-full sm:w-auto"
>
{isKelompokKeahlianDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Hapus
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}