diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 497c4c8..ab384eb 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -16,7 +16,7 @@ export async function POST(request: NextRequest) { } // Validate role - if (!['ketuajurusan', 'admin'].includes(role)) { + if (!['ketuajurusan', 'ketuaprodi', 'pimpinan', 'admin'].includes(role)) { return NextResponse.json( { message: 'Role tidak valid' }, { status: 400 } @@ -25,18 +25,22 @@ export async function POST(request: NextRequest) { let query = supabase .from('user_app') - .select('*') - .eq('role_user', role); + .select('*'); // Add specific field filter based on role - if (role === 'ketuajurusan') { + if (role === 'pimpinan' || role === 'ketuajurusan' || role === 'ketuaprodi') { if (!nip) { return NextResponse.json( - { message: 'NIP diperlukan untuk Ketua Jurusan' }, + { message: 'NIP diperlukan untuk Pimpinan' }, { status: 400 } ); } - query = query.eq('nip', nip); + // For pimpinan, accept both ketuajurusan and ketuaprodi + if (role === 'pimpinan') { + query = query.in('role_user', ['ketuajurusan', 'ketuaprodi']).eq('nip', nip); + } else { + query = query.eq('role_user', role).eq('nip', nip); + } } else if (role === 'admin') { if (!username) { return NextResponse.json( @@ -44,8 +48,8 @@ export async function POST(request: NextRequest) { { status: 400 } ); } - query = query.eq('username', username); - } + query = query.eq('role_user', role).eq('username', username); + } const { data: users, error } = await query; diff --git a/app/api/keloladata/data-akun/route.ts b/app/api/keloladata/data-akun/route.ts new file mode 100644 index 0000000..be7fc91 --- /dev/null +++ b/app/api/keloladata/data-akun/route.ts @@ -0,0 +1,328 @@ +import { NextRequest, NextResponse } from 'next/server'; +import supabase from '@/lib/db'; +import bcrypt from 'bcryptjs'; + +// GET - Ambil semua data user +export async function GET() { + try { + const { data, error } = await supabase + .from('user_app') + .select('id_user, username, nip, role_user, created_at, updated_at') + .order('id_user', { ascending: true }); + + if (error) { + console.error('Error fetching users:', error); + return NextResponse.json( + { error: 'Failed to fetch users' }, + { status: 500 } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching users:', error); + return NextResponse.json( + { error: 'Failed to fetch users' }, + { status: 500 } + ); + } +} + +// POST - Buat user baru +export async function POST(request: NextRequest) { + try { + const { username, nip, password, role_user } = await request.json(); + + // Validasi input + if (!username || username.trim() === '') { + return NextResponse.json( + { error: 'Username is required' }, + { status: 400 } + ); + } + + if (!password || password.trim() === '') { + return NextResponse.json( + { error: 'Password is required' }, + { status: 400 } + ); + } + + if (!role_user || !['admin', 'ketuajurusan', 'ketuaprodi'].includes(role_user)) { + return NextResponse.json( + { error: 'Role user must be either admin, ketuajurusan, or ketuaprodi' }, + { status: 400 } + ); + } + + // Cek apakah username sudah ada + const { data: existingUsername, error: existingUsernameError } = await supabase + .from('user_app') + .select('id_user') + .ilike('username', username.trim()); + + if (existingUsernameError) { + console.error('Error checking existing username:', existingUsernameError); + return NextResponse.json( + { error: 'Failed to check existing username' }, + { status: 500 } + ); + } + + if (existingUsername && existingUsername.length > 0) { + return NextResponse.json( + { error: 'Username sudah digunakan' }, + { status: 409 } + ); + } + + // Cek apakah NIP sudah ada (jika NIP diisi) + if (nip && nip.trim() !== '') { + const { data: existingNip, error: existingNipError } = await supabase + .from('user_app') + .select('id_user') + .eq('nip', nip.trim()); + + if (existingNipError) { + console.error('Error checking existing NIP:', existingNipError); + return NextResponse.json( + { error: 'Failed to check existing NIP' }, + { status: 500 } + ); + } + + if (existingNip && existingNip.length > 0) { + return NextResponse.json( + { error: 'NIP sudah digunakan' }, + { status: 409 } + ); + } + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Insert user baru + const { data, error } = await supabase + .from('user_app') + .insert([{ + username: username.trim(), + nip: nip && nip.trim() !== '' ? nip.trim() : null, + password: hashedPassword, + role_user: role_user + }]) + .select('id_user, username, nip, role_user, created_at, updated_at') + .single(); + + if (error) { + console.error('Error creating user:', error); + return NextResponse.json( + { error: 'Failed to create user' }, + { status: 500 } + ); + } + + return NextResponse.json(data, { status: 201 }); + } catch (error) { + console.error('Error creating user:', error); + return NextResponse.json( + { error: 'Failed to create user' }, + { status: 500 } + ); + } +} + +// PUT - Update data user (termasuk reset password) +export async function PUT(request: NextRequest) { + try { + const { id_user, username, nip, password, role_user } = await request.json(); + + // Validasi input + if (!id_user) { + return NextResponse.json( + { error: 'ID user is required' }, + { status: 400 } + ); + } + + if (!username || username.trim() === '') { + return NextResponse.json( + { error: 'Username is required' }, + { status: 400 } + ); + } + + if (!role_user || !['admin', 'ketuajurusan', 'ketuaprodi'].includes(role_user)) { + return NextResponse.json( + { error: 'Role user must be either admin, ketuajurusan, or ketuaprodi' }, + { status: 400 } + ); + } + + // Check if user exists + const { data: existingData, error: existingError } = await supabase + .from('user_app') + .select('id_user') + .eq('id_user', id_user) + .single(); + + if (existingError || !existingData) { + return NextResponse.json( + { error: 'User tidak ditemukan' }, + { status: 404 } + ); + } + + // Cek apakah username sudah ada untuk user lain + const { data: duplicateUsername, error: duplicateUsernameError } = await supabase + .from('user_app') + .select('id_user') + .ilike('username', username.trim()) + .neq('id_user', id_user); + + if (duplicateUsernameError) { + console.error('Error checking duplicate username:', duplicateUsernameError); + return NextResponse.json( + { error: 'Failed to check duplicate username' }, + { status: 500 } + ); + } + + if (duplicateUsername && duplicateUsername.length > 0) { + return NextResponse.json( + { error: 'Username sudah digunakan oleh user lain' }, + { status: 409 } + ); + } + + // Cek apakah NIP sudah ada untuk user lain (jika NIP diisi) + if (nip && nip.trim() !== '') { + const { data: duplicateNip, error: duplicateNipError } = await supabase + .from('user_app') + .select('id_user') + .eq('nip', nip.trim()) + .neq('id_user', id_user); + + if (duplicateNipError) { + console.error('Error checking duplicate NIP:', duplicateNipError); + return NextResponse.json( + { error: 'Failed to check duplicate NIP' }, + { status: 500 } + ); + } + + if (duplicateNip && duplicateNip.length > 0) { + return NextResponse.json( + { error: 'NIP sudah digunakan oleh user lain' }, + { status: 409 } + ); + } + } + + // Siapkan data update + const updateData: any = { + username: username.trim(), + nip: nip && nip.trim() !== '' ? nip.trim() : null, + role_user: role_user, + updated_at: new Date().toISOString() + }; + + // Jika password diisi, hash dan update password + if (password && password.trim() !== '') { + const hashedPassword = await bcrypt.hash(password, 10); + updateData.password = hashedPassword; + } + + const { data, error } = await supabase + .from('user_app') + .update(updateData) + .eq('id_user', id_user) + .select('id_user, username, nip, role_user, created_at, updated_at') + .single(); + + if (error) { + console.error('Error updating user:', error); + return NextResponse.json( + { error: 'Failed to update user' }, + { status: 500 } + ); + } + + return NextResponse.json(data); + } catch (error) { + console.error('Error updating user:', error); + return NextResponse.json( + { error: 'Failed to update user' }, + { status: 500 } + ); + } +} + +// DELETE - Hapus user +export async function DELETE(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const id_user = searchParams.get('id_user'); + + if (!id_user) { + return NextResponse.json( + { error: 'ID user is required' }, + { status: 400 } + ); + } + + // Check if user exists + const { data: existingData, error: existingError } = await supabase + .from('user_app') + .select('id_user, username') + .eq('id_user', id_user) + .single(); + + if (existingError || !existingData) { + return NextResponse.json( + { error: 'User tidak ditemukan' }, + { status: 404 } + ); + } + + // Prevent deleting admin account (optional safety check) + // Uncomment jika ingin mencegah penghapusan admin + // const { data: userData } = await supabase + // .from('user_app') + // .select('role_user') + // .eq('id_user', id_user) + // .single(); + // + // if (userData && userData.role_user === 'admin') { + // return NextResponse.json( + // { error: 'Tidak dapat menghapus akun admin' }, + // { status: 403 } + // ); + // } + + const { error } = await supabase + .from('user_app') + .delete() + .eq('id_user', id_user); + + if (error) { + console.error('Error deleting user:', error); + return NextResponse.json( + { error: 'Failed to delete user' }, + { status: 500 } + ); + } + + return NextResponse.json( + { message: 'User berhasil dihapus' }, + { status: 200 } + ); + } catch (error) { + console.error('Error deleting user:', error); + return NextResponse.json( + { error: 'Failed to delete user' }, + { status: 500 } + ); + } +} + diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 49dfaaa..31ce0e0 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -133,15 +133,13 @@ export default function TotalMahasiswaPage() { { id: 'expertise', label: 'Kelompok Keahlian' }, { id: 'dropout', label: 'Terancam Drop Out & Distribusi IPK' }, { id: 'scholarship', label: 'Beasiswa & Prestasi' }, - { id: 'demographics', label: 'Asal Kabupaten & Provinsi' }, - { id: 'bimbingan-dosen', label: 'Bimbingan Dosen' }, - { id: 'mk-belum-diambil', label: 'Mata Kuliah Belum Diambil' } + { id: 'mk-belum-diambil', label: 'Mata Kuliah Belum Diambil & Provinsi Mahasiswa' }, + { id: 'demographics', label: 'Asal Kabupaten & Bimbingan Dosen' }, ]; // Navigation menu items for per year data const perYearNavItems = [ { 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: 'academic-year', label: 'Distribusi IPK per Angkatan' }, @@ -244,6 +242,7 @@ export default function TotalMahasiswaPage() { {/* MK Belum Diambil Section */}
+
{/* Demographics Section */} @@ -252,13 +251,9 @@ export default function TotalMahasiswaPage() {
- +
- -
- -
) : (
diff --git a/app/keloladata/akun/page.tsx b/app/keloladata/akun/page.tsx new file mode 100644 index 0000000..2375739 --- /dev/null +++ b/app/keloladata/akun/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import DataTableAkun from "@/components/datatable/kelolaakun/data-table-akun"; + +export default function KelolaAkunPage() { + return ( +
+ +
+ ); +} + diff --git a/app/page.tsx b/app/page.tsx index 1bb0222..b13d7c9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -32,7 +32,7 @@ export default function HomePage() { const data = await response.json(); setUser(data.user); // Redirect based on user role - if (data.user.role_user === 'ketuajurusan') { + if (data.user.role_user === 'ketuajurusan' || data.user.role_user === 'ketuaprodi') { router.push('/dashboard'); } else if (data.user.role_user === 'admin') { router.push('/keloladata/mahasiswa'); @@ -53,12 +53,22 @@ export default function HomePage() { setUser(userData.user); setShowLoginDialog(false); - // Redirect based on user role + // Get role label + let roleLabel = ''; if (userData.user.role_user === 'ketuajurusan') { - showSuccess("Berhasil!", "Selamat datang, Ketua Jurusan!"); + roleLabel = 'Ketua Jurusan'; + } else if (userData.user.role_user === 'ketuaprodi') { + roleLabel = 'Ketua Prodi'; + } else if (userData.user.role_user === 'admin') { + roleLabel = 'Admin'; + } + + showSuccess("Berhasil!", `Selamat datang, ${roleLabel}`); + + // Redirect based on user role + if (userData.user.role_user === 'ketuajurusan' || userData.user.role_user === 'ketuaprodi') { router.push('/dashboard'); } else if (userData.user.role_user === 'admin') { - showSuccess("Berhasil!", "Selamat datang, Admin!"); router.push('/keloladata/mahasiswa'); } }; @@ -133,7 +143,7 @@ function AutoLoginDialog({ onLoginSuccess }: AutoLoginDialogProps) { body: JSON.stringify({ nip: ketuaForm.nip, password: ketuaForm.password, - role: "ketuajurusan", + role: "pimpinan", // Will accept both ketuajurusan and ketuaprodi }), }); @@ -210,7 +220,7 @@ function AutoLoginDialog({ onLoginSuccess }: AutoLoginDialogProps) { : "text-slate-300 hover:text-white hover:bg-slate-600/50" }`} > - Ketua Jurusan + Pimpinan
- {/* Ketua Jurusan form */} + {/* Pimpinan form (Ketua Jurusan / Ketua Prodi) */} {activeTab === "ketua" && (
@@ -266,7 +276,7 @@ function AutoLoginDialog({ onLoginSuccess }: AutoLoginDialogProps) { Loading...
) : ( - "Login sebagai Ketua Jurusan" + "Login sebagai Pimpinan" )}
diff --git a/components/chartsDashboard/MKBelumDiambilChart.tsx b/components/chartsDashboard/MKBelumDiambilChart.tsx index 5b48216..97a6421 100644 --- a/components/chartsDashboard/MKBelumDiambilChart.tsx +++ b/components/chartsDashboard/MKBelumDiambilChart.tsx @@ -419,7 +419,7 @@ export default function MKBelumDiambilChart({ - Memuat data mata kuliah belum diambil... + Loading... diff --git a/components/datatable/kelolaakun/data-table-akun.tsx b/components/datatable/kelolaakun/data-table-akun.tsx new file mode 100644 index 0000000..c9158a6 --- /dev/null +++ b/components/datatable/kelolaakun/data-table-akun.tsx @@ -0,0 +1,802 @@ +"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, + Eye, + EyeOff, + KeyRound +} from "lucide-react"; +import { useToast } from "@/components/ui/toast-provider"; +import { Badge } from "@/components/ui/badge"; + +// Define the User type +interface UserApp { + id_user: number; + username: string; + nip: string | null; + role_user: 'admin' | 'ketuajurusan' | 'ketuaprodi'; + created_at: string; + updated_at: string; +} + +export default function DataTableAkun() { + const { showSuccess, showError } = useToast(); + + // State for data + const [users, setUsers] = useState([]); + const [filteredData, setFilteredData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // State for filtering + const [searchTerm, setSearchTerm] = useState(""); + const [roleFilter, setRoleFilter] = useState("all"); + + // 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" | "reset-password">("add"); + const [formData, setFormData] = useState>({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [showPassword, setShowPassword] = 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(() => { + fetchUsers(); + }, []); + + // Filter data when search term or role filter changes + useEffect(() => { + filterData(); + }, [searchTerm, roleFilter, users]); + + // Update paginated data when filtered data or pagination settings change + useEffect(() => { + paginateData(); + }, [filteredData, currentPage, pageSize]); + + // Fetch users data from API + const fetchUsers = async () => { + try { + setLoading(true); + const response = await fetch("/api/keloladata/data-akun"); + + if (!response.ok) { + throw new Error("Failed to fetch data"); + } + + const data = await response.json(); + setUsers(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 role + const filterData = () => { + let filtered = [...users]; + + // Filter by search term + if (searchTerm) { + filtered = filtered.filter( + (item) => + (item.username?.toLowerCase() || "").includes(searchTerm.toLowerCase()) || + (item.nip?.toLowerCase() || "").includes(searchTerm.toLowerCase()) + ); + } + + // Filter by role + if (roleFilter && roleFilter !== "all") { + filtered = filtered.filter((item) => item.role_user === roleFilter); + } + + 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 form data + const resetForm = () => { + setFormData({}); + setShowPassword(false); + }; + + // Handle form input changes + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + // Handle role select change + const handleRoleChange = (value: string) => { + setFormData((prev) => ({ ...prev, role_user: value as 'admin' | 'ketuajurusan' | 'ketuaprodi' })); + }; + + // Open form dialog for adding new user + const handleAdd = () => { + setFormMode("add"); + resetForm(); + setIsDialogOpen(true); + }; + + // Open form dialog for editing user + const handleEdit = (data: UserApp) => { + setFormMode("edit"); + setFormData({ ...data, password: "" }); // Don't include password in edit form initially + setIsDialogOpen(true); + }; + + // Open form dialog for resetting password + const handleResetPassword = (data: UserApp) => { + setFormMode("reset-password"); + setFormData({ + id_user: data.id_user, + username: data.username, + nip: data.nip, + role_user: data.role_user, + password: "" + }); + setIsDialogOpen(true); + }; + + // Open delete confirmation dialog + const handleDeleteConfirm = (id: number) => { + setDeleteId(id); + setIsDeleteDialogOpen(true); + }; + + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // Validation + if (formMode !== "reset-password") { + if (!formData.username?.trim()) { + showError("Username harus diisi"); + return; + } + + if (!formData.role_user) { + showError("Role user harus dipilih"); + return; + } + } + + if (formMode === "add" && !formData.password?.trim()) { + showError("Password harus diisi untuk user baru"); + return; + } + + if (formMode === "reset-password" && !formData.password?.trim()) { + showError("Password baru harus diisi"); + return; + } + + setIsSubmitting(true); + + try { + let response; + + if (formMode === "add") { + // Create new user + response = await fetch("/api/keloladata/data-akun", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username: formData.username?.trim(), + nip: formData.nip?.trim() || null, + password: formData.password, + role_user: formData.role_user, + }), + }); + } else { + // Update existing user (edit or reset password) + const updateData: any = { + id_user: formData.id_user, + username: formData.username?.trim(), + nip: formData.nip?.trim() || null, + role_user: formData.role_user, + }; + + // Include password if it's provided + if (formData.password?.trim()) { + updateData.password = formData.password; + } + + response = await fetch("/api/keloladata/data-akun", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(updateData), + }); + } + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to save data"); + } + + await fetchUsers(); + setIsDialogOpen(false); + resetForm(); + + if (formMode === "add") { + showSuccess("User berhasil ditambahkan"); + } else if (formMode === "reset-password") { + showSuccess("Password berhasil direset"); + } else { + showSuccess("User berhasil diperbarui"); + } + } catch (err) { + console.error("Error saving data:", err); + showError(err instanceof Error ? err.message : "Gagal menyimpan data"); + } finally { + setIsSubmitting(false); + } + }; + + // Handle delete + const handleDelete = async () => { + if (deleteId === null) return; + + setIsDeleting(true); + + try { + const response = await fetch(`/api/keloladata/data-akun?id_user=${deleteId}`, { + method: "DELETE", + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || "Failed to delete user"); + } + + await fetchUsers(); + setIsDeleteDialogOpen(false); + setDeleteId(null); + showSuccess("User berhasil dihapus"); + } catch (err) { + console.error("Error deleting data:", err); + showError(err instanceof Error ? err.message : "Gagal menghapus user"); + } finally { + setIsDeleting(false); + } + }; + + // Get role badge color + const getRoleBadgeColor = (role: string) => { + switch (role) { + case 'admin': + return 'bg-red-500 hover:bg-red-600'; + case 'ketuajurusan': + return 'bg-blue-500 hover:bg-blue-600'; + case 'ketuaprodi': + return 'bg-green-500 hover:bg-green-600'; + default: + return 'bg-gray-500 hover:bg-gray-600'; + } + }; + + // Get role label + const getRoleLabel = (role: string) => { + switch (role) { + case 'admin': + return 'Admin'; + case 'ketuajurusan': + return 'Ketua Jurusan'; + case 'ketuaprodi': + return 'Ketua Prodi'; + default: + return role; + } + }; + + // Calculate the range of entries being displayed + function 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 }; + } + + // Generate pagination items + function renderPaginationItems() { + const totalPages = getTotalPages(); + const items = []; + + // Always show first page + items.push( + + handlePageChange(1)} + className="cursor-pointer" + > + 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; + items.push( + + handlePageChange(i)} + className="cursor-pointer" + > + {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)} + className="cursor-pointer" + > + {totalPages} + + + ); + } + + return items; + } + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + return ( +
+ {/* Header and Actions */} +
+
+

Kelola Akun

+

+ Kelola akun pengguna sistem PODIF +

+
+ +
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> + {searchTerm && ( + + )} +
+ +
+ + {/* Table Controls */} +
+ Show + + entries +
+ + {/* Table */} +
+ + + + No + Username + NIP + Role + Aksi + + + + {paginatedData.length === 0 ? ( + + + Tidak ada data yang tersedia + + + ) : ( + paginatedData.map((user, index) => ( + + + {(currentPage - 1) * pageSize + index + 1} + + + {user.username} + + + {user.nip || '-'} + + + + {getRoleLabel(user.role_user)} + + + +
+ + + +
+
+
+ )) + )} +
+
+
+ + {/* Pagination */} + {!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" : "cursor-pointer"} + /> + + + {renderPaginationItems()} + + + handlePageChange(Math.min(getTotalPages(), currentPage + 1))} + className={currentPage === getTotalPages() ? "pointer-events-none opacity-50" : "cursor-pointer"} + /> + + + +
+ )} + + {/* Form Dialog */} + + + + + {formMode === "add" ? "Tambah Akun Baru" : formMode === "reset-password" ? "Reset Password" : "Edit Akun"} + + +
+
+ {formMode !== "reset-password" && ( + <> +
+ + +
+
+ + +
+
+ + +
+ + )} + {formMode === "reset-password" && ( +
+

+ Reset password untuk user: {formData.username} +

+
+ )} + {(formMode === "add" || formMode === "reset-password" || formMode === "edit") && ( +
+ +
+ + +
+
+ )} +
+ + + + + + +
+
+
+ + {/* Delete Confirmation Dialog */} + + + + Konfirmasi Hapus + +

+ Apakah Anda yakin ingin menghapus akun ini? Tindakan ini tidak dapat dibatalkan. +

+ + + + + + +
+
+
+ ); +} + diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx index deb4585..255b6b3 100644 --- a/components/ui/Navbar.tsx +++ b/components/ui/Navbar.tsx @@ -129,7 +129,7 @@ const Navbar = () => { {/* Desktop Navigation - Centered */}
{/* Dashboard - Only for Ketua Jurusan */} - {user && user.role_user === 'ketuajurusan' && ( + {user && (user.role_user === 'ketuajurusan' || user.role_user === 'ketuaprodi') && ( <> @@ -232,6 +232,12 @@ const Navbar = () => { Nilai Mahasiswa + + + + Akun + + )} @@ -245,13 +251,17 @@ const Navbar = () => { - {user.role_user === 'ketuajurusan' ? user.username : user.username} + {user.username} @@ -300,7 +310,7 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => {

Menu Utama

{/* Dashboard - Only for Ketua Jurusan */} - {user.role_user === 'ketuajurusan' && ( + {(user.role_user === 'ketuajurusan' || user.role_user === 'ketuaprodi') && ( Dashboard @@ -359,6 +369,10 @@ const MobileNavContent = ({ user, onLogout }: MobileNavContentProps) => { Nilai Mahasiswa + + + Akun +
)}