meti-frontend/app/admin/members/page.tsx.backup
2025-08-15 23:03:15 +07:00

1398 lines
59 KiB
Plaintext

"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Users,
UserCheck,
Clock,
Search,
Filter,
ArrowLeft,
CheckCircle,
XCircle,
AlertCircle,
Plus,
Upload,
FileText,
Download,
Trash2,
} from "lucide-react"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import Link from "next/link"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { AuthGuard } from "@/components/auth-guard"
import { useAuth } from "@/hooks/use-auth"
import { Checkbox } from "@/components/ui/checkbox"
import Papa from "papaparse"
import * as XLSX from "xlsx"
interface User {
id: string
username: string
name: string
memberId?: string
memberStatus: "verified" | "pending" | "rejected"
department?: string
joinDate?: string
lastLogin?: string
}
interface MemberStats {
totalMembers: number
verifiedMembers: number
pendingMembers: number
rejectedMembers: number
votedMembers: number
participationRate: number
}
interface BulkUserData {
name: string
email: string
password: string
role: string
selected?: boolean
rowIndex?: number
validationError?: string
}
interface ParsedFileData {
data: BulkUserData[]
errors: string[]
fileName: string
}
function MembersPageContent() {
const { user } = useAuth("admin")
const [members, setMembers] = useState<User[]>([])
const [stats, setStats] = useState<MemberStats | null>(null)
const [searchTerm, setSearchTerm] = useState("")
const [statusFilter, setStatusFilter] = useState<string>("all")
const [loading, setLoading] = useState(false)
const [debugInfo, setDebugInfo] = useState<string>("")
const [showCreateUser, setShowCreateUser] = useState(false)
const [createUserForm, setCreateUserForm] = useState({
username: "",
password: "",
name: "",
email: "",
memberId: "",
department: "",
joinDate: new Date().toISOString().split("T")[0],
})
// Bulk upload states
const [showBulkUpload, setShowBulkUpload] = useState(false)
const [parsedData, setParsedData] = useState<ParsedFileData | null>(null)
const [bulkLoading, setBulkLoading] = useState(false)
const [selectedUsers, setSelectedUsers] = useState<Set<number>>(new Set())
const [selectAll, setSelectAll] = useState(false)
const router = useRouter()
useEffect(() => {
if (user) {
loadMembers()
loadStats()
}
}, [user])
const loadMembers = async () => {
try {
console.log("=== LOADING MEMBERS ===")
const response = await fetch("/api/members")
const data = await response.json()
console.log("Members API response:", data)
if (data.success) {
setMembers(data.members)
console.log("Members loaded:", data.members.length)
// Debug info
const statusCounts = data.members.reduce((acc: any, member: User) => {
acc[member.memberStatus] = (acc[member.memberStatus] || 0) + 1
return acc
}, {})
setDebugInfo(`Total: ${data.members.length}, Status: ${JSON.stringify(statusCounts)}`)
// Log each member's status
data.members.forEach((member: User, index: number) => {
console.log(
`Member ${index + 1}: ${member.name} - Status: "${member.memberStatus}" (${typeof member.memberStatus})`,
)
})
}
} catch (error) {
console.error("Error loading members:", error)
setDebugInfo("Error loading members: " + error.message)
}
}
const loadStats = async () => {
try {
const response = await fetch("/api/members/stats")
const data = await response.json()
if (data.success) {
setStats(data.stats)
}
} catch (error) {
console.error("Error loading stats:", error)
}
}
const handleStatusChange = async (memberId: string, newStatus: string) => {
console.log(`=== CHANGING STATUS ===`)
console.log(`Member ID: ${memberId}, New Status: ${newStatus}`)
setLoading(true)
try {
const response = await fetch(`/api/members/${memberId}/status`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: newStatus }),
})
const data = await response.json()
console.log("Status change response:", data)
if (response.ok) {
await loadMembers()
await loadStats()
alert(`Status anggota berhasil diubah menjadi ${newStatus}`)
} else {
alert("Gagal mengubah status: " + data.message)
}
} catch (error) {
console.error("Error updating member status:", error)
alert("Gagal mengubah status anggota")
} finally {
setLoading(false)
}
}
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
console.log("=== CREATING USER ===")
console.log("Form data:", createUserForm)
try {
const response = await fetch("/api/members", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...createUserForm,
role: "voter",
memberStatus: "pending",
}),
})
const data = await response.json()
console.log("API response:", data)
if (data.success) {
console.log("User created successfully:", data.user)
// Reload data
await loadMembers()
await loadStats()
// Reset form
setCreateUserForm({
username: "",
password: "",
name: "",
email: "",
memberId: "",
department: "",
joinDate: new Date().toISOString().split("T")[0],
})
setShowCreateUser(false)
alert("Anggota baru berhasil dibuat!")
} else {
console.error("Failed to create user:", data.message)
alert("Gagal membuat anggota: " + data.message)
}
} catch (error) {
console.error("Error creating user:", error)
alert("Terjadi kesalahan sistem")
} finally {
setLoading(false)
}
}
// File parsing functions
const validateUserData = (userData: BulkUserData, rowIndex: number): string | null => {
if (!userData.name || userData.name.trim().length === 0) {
return "Name is required"
}
if (userData.name.length > 255) {
return "Name must be less than 255 characters"
}
if (!userData.email || !userData.email.includes("@")) {
return "Valid email is required"
}
if (!userData.password || userData.password.length < 6) {
return "Password must be at least 6 characters"
}
if (!userData.role || userData.role.trim().length === 0) {
return "Role is required"
}
return null
}
const parseCSVFile = (file: File): Promise<ParsedFileData> => {
return new Promise((resolve) => {
Papa.parse(file, {
header: true,
skipEmptyLines: true,
complete: (result) => {
const errors: string[] = []
const data: BulkUserData[] = []
result.data.forEach((row: any, index: number) => {
const userData: BulkUserData = {
name: row.name || row.Name || "",
email: row.email || row.Email || "",
password: row.password || row.Password || "",
role: row.role || row.Role || "voter",
selected: true,
rowIndex: index
}
const validationError = validateUserData(userData, index)
if (validationError) {
userData.validationError = validationError
errors.push(`Row ${index + 2}: ${validationError}`)
}
data.push(userData)
})
resolve({
data,
errors,
fileName: file.name
})
},
error: (error) => {
resolve({
data: [],
errors: [error.message],
fileName: file.name
})
}
})
})
}
const parseExcelFile = (file: File): Promise<ParsedFileData> => {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target?.result as ArrayBuffer)
const workbook = XLSX.read(data, { type: 'array' })
const sheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[sheetName]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
const errors: string[] = []
const users: BulkUserData[] = []
jsonData.forEach((row: any, index: number) => {
const userData: BulkUserData = {
name: row.name || row.Name || "",
email: row.email || row.Email || "",
password: row.password || row.Password || "",
role: row.role || row.Role || "voter",
selected: true,
rowIndex: index
}
const validationError = validateUserData(userData, index)
if (validationError) {
userData.validationError = validationError
errors.push(`Row ${index + 2}: ${validationError}`)
}
users.push(userData)
})
resolve({
data: users,
errors,
fileName: file.name
})
} catch (error) {
resolve({
data: [],
errors: [error.message],
fileName: file.name
})
}
}
reader.readAsArrayBuffer(file)
})
}
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
setBulkLoading(true)
try {
let parsed: ParsedFileData
if (file.name.toLowerCase().endsWith('.csv')) {
parsed = await parseCSVFile(file)
} else if (file.name.toLowerCase().endsWith('.xlsx') || file.name.toLowerCase().endsWith('.xls')) {
parsed = await parseExcelFile(file)
} else {
alert('Please upload a CSV or Excel file')
setBulkLoading(false)
return
}
setParsedData(parsed)
setSelectedUsers(new Set(parsed.data.map((_, index) => index)))
setSelectAll(true)
} catch (error) {
console.error('Error parsing file:', error)
alert('Error parsing file: ' + error.message)
} finally {
setBulkLoading(false)
}
}
const handleSelectAll = (checked: boolean) => {
if (checked && parsedData) {
setSelectedUsers(new Set(parsedData.data.map((_, index) => index)))
} else {
setSelectedUsers(new Set())
}
setSelectAll(checked)
}
const handleSelectUser = (index: number, checked: boolean) => {
const newSelected = new Set(selectedUsers)
if (checked) {
newSelected.add(index)
} else {
newSelected.delete(index)
}
setSelectedUsers(newSelected)
setSelectAll(parsedData ? newSelected.size === parsedData.data.length : false)
}
const handleBulkCreateUsers = async () => {
if (!parsedData || selectedUsers.size === 0) {
alert('Please select at least one user to create')
return
}
setBulkLoading(true)
try {
const selectedUsersData = parsedData.data
.filter((_, index) => selectedUsers.has(index))
.filter(user => !user.validationError) // Only include valid users
.map(user => ({
name: user.name,
email: user.email,
password: user.password,
role: user.role
}))
if (selectedUsersData.length === 0) {
alert('No valid users selected for creation')
setBulkLoading(false)
return
}
const response = await fetch('/api/v1/users/bulk', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
users: selectedUsersData
})
})
const result = await response.json()
if (response.ok && result.success) {
alert(`Successfully created ${selectedUsersData.length} users!`)
setParsedData(null)
setSelectedUsers(new Set())
setSelectAll(false)
setShowBulkUpload(false)
// Reload members list
await loadMembers()
await loadStats()
} else {
alert(`Failed to create users: ${result.message || 'Unknown error'}`)
}
} catch (error) {
console.error('Error creating bulk users:', error)
alert('Error creating users: ' + error.message)
} finally {
setBulkLoading(false)
}
}
const downloadTemplate = () => {
const csvContent = "name,email,password,role\nJohn Doe,john@example.com,password123,voter\nJane Smith,jane@example.com,password456,admin"
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
link.setAttribute('href', url)
link.setAttribute('download', 'user_template.csv')
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
const filteredMembers = members.filter((member) => {
const matchesSearch =
member.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.memberId?.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.department?.toLowerCase().includes(searchTerm.toLowerCase())
const matchesStatus = statusFilter === "all" || member.memberStatus === statusFilter
return matchesSearch && matchesStatus
})
const getStatusBadge = (status: string) => {
switch (status) {
case "verified":
return (
<Badge className="bg-green-100 text-green-800 border-green-200">
<CheckCircle className="h-3 w-3 mr-1" />
Terverifikasi
</Badge>
)
case "pending":
return (
<Badge className="bg-yellow-100 text-yellow-800 border-yellow-200">
<Clock className="h-3 w-3 mr-1" />
Menunggu
</Badge>
)
case "rejected":
return (
<Badge className="bg-red-100 text-red-800 border-red-200">
<XCircle className="h-3 w-3 mr-1" />
Ditolak
</Badge>
)
default:
return (
<Badge variant="secondary" className="bg-gray-100 text-gray-800">
<AlertCircle className="h-3 w-3 mr-1" />
Status Tidak Valid: {status}
</Badge>
)
}
}
if (!user) return null
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="container mx-auto px-4 py-4">
<div className="flex items-center gap-2 lg:gap-4">
<Link href="/admin" className="text-blue-600 hover:text-blue-800">
<ArrowLeft className="h-5 w-5" />
</Link>
<img src="/images/meti-logo.png" alt="METI - New & Renewable Energy" className="h-8 lg:h-10 w-auto" />
<h1 className="text-lg lg:text-2xl font-bold text-gray-900">Verifikasi Anggota</h1>
</div>
</div>
</header>
<div className="container mx-auto px-4 py-8">
{/* Debug Info */}
{debugInfo && (
<Alert className="mb-6">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>Debug Info:</strong> {debugInfo}
</AlertDescription>
</Alert>
)}
{/* Statistics Cards */}
{stats && (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6 mb-8">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs lg:text-sm font-medium">Total Anggota</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold">{stats.totalMembers}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs lg:text-sm font-medium">Terverifikasi</CardTitle>
<UserCheck className="h-4 w-4 text-green-600" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-green-600">{stats.verifiedMembers}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs lg:text-sm font-medium">Menunggu</CardTitle>
<Clock className="h-4 w-4 text-yellow-600" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-yellow-600">{stats.pendingMembers}</div>
</CardContent>
</Card>
<Card className="col-span-2 lg:col-span-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-xs lg:text-sm font-medium">Partisipasi</CardTitle>
<AlertCircle className="h-4 w-4 text-blue-600" />
</CardHeader>
<CardContent>
<div className="text-xl lg:text-2xl font-bold text-blue-600">{stats.participationRate}%</div>
<p className="text-xs text-muted-foreground">
{stats.votedMembers} dari {stats.verifiedMembers}
</p>
</CardContent>
</Card>
</div>
)}
{/* Create User & Filters */}
<Card className="mb-6">
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>Kelola Anggota</CardTitle>
<div className="flex gap-2">
<Dialog open={showCreateUser} onOpenChange={setShowCreateUser}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Tambah Anggota Baru
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Tambah Anggota Baru</DialogTitle>
<DialogDescription>Buat akun voter baru untuk anggota METI</DialogDescription>
</DialogHeader>
<form onSubmit={handleCreateUser} className="space-y-4">
<div>
<Label htmlFor="username">Username</Label>
<Input
id="username"
value={createUserForm.username}
onChange={(e) => setCreateUserForm({ ...createUserForm, username: e.target.value })}
placeholder="username_anggota"
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={createUserForm.password}
onChange={(e) => setCreateUserForm({ ...createUserForm, password: e.target.value })}
placeholder="Password untuk login"
required
/>
</div>
<div>
<Label htmlFor="name">Nama Lengkap</Label>
<Input
id="name"
value={createUserForm.name}
onChange={(e) => setCreateUserForm({ ...createUserForm, name: e.target.value })}
placeholder="Nama lengkap anggota"
required
/>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={createUserForm.email}
onChange={(e) => setCreateUserForm({ ...createUserForm, email: e.target.value })}
placeholder="email@example.com"
required
/>
</div>
<div>
<Label htmlFor="memberId">ID Anggota</Label>
<Input
id="memberId"
value={createUserForm.memberId}
onChange={(e) => setCreateUserForm({ ...createUserForm, memberId: e.target.value })}
placeholder="METI-XXX"
required
/>
</div>
<div>
<Label htmlFor="department">Departemen</Label>
<Input
id="department"
value={createUserForm.department}
onChange={(e) => setCreateUserForm({ ...createUserForm, department: e.target.value })}
placeholder="Renewable Energy, Solar Energy, dll"
required
/>
</div>
<div>
<Label htmlFor="joinDate">Tanggal Bergabung</Label>
<Input
id="joinDate"
type="date"
value={createUserForm.joinDate}
onChange={(e) => setCreateUserForm({ ...createUserForm, joinDate: e.target.value })}
required
/>
</div>
<Button type="submit" disabled={loading} className="w-full">
{loading ? "Membuat..." : "Buat Anggota"}
</Button>
</form>
</DialogContent>
</Dialog>
<Dialog open={showBulkUpload} onOpenChange={setShowBulkUpload}>
<DialogTrigger asChild>
<Button variant="outline">
<Upload className="h-4 w-4 mr-2" />
Upload Bulk Users
</Button>
</DialogTrigger>
<DialogContent className="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Upload Bulk Users</DialogTitle>
<DialogDescription>
Upload CSV or Excel file to create multiple users at once
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-auto">
{!parsedData ? (
<div className="space-y-6">
{/* File Upload Section */}
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8">
<div className="text-center">
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">Upload File</h3>
<p className="text-gray-600 mb-4">
Choose a CSV or Excel file containing user data
</p>
<input
type="file"
accept=".csv,.xlsx,.xls"
onChange={handleFileUpload}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button className="cursor-pointer" disabled={bulkLoading}>
{bulkLoading ? "Processing..." : "Choose File"}
</Button>
</label>
</div>
</div>
{/* Template Download */}
<div className="bg-blue-50 p-4 rounded-lg">
<h4 className="font-medium mb-2">Need a template?</h4>
<p className="text-sm text-gray-600 mb-3">
Download our CSV template to get started. Required columns: name, email, password, role
</p>
<Button variant="outline" size="sm" onClick={downloadTemplate}>
<Download className="h-4 w-4 mr-2" />
Download Template
</Button>
</div>
{/* Format Info */}
<div className="bg-gray-50 p-4 rounded-lg">
<h4 className="font-medium mb-2">File Format Requirements:</h4>
<ul className="text-sm text-gray-600 space-y-1">
<li>• <strong>name</strong>: Full name (required, max 255 chars)</li>
<li>• <strong>email</strong>: Valid email address (required)</li>
<li>• <strong>password</strong>: Password (required, min 6 chars)</li>
<li>• <strong>role</strong>: User role (required, e.g., 'voter', 'admin')</li>
</ul>
</div>
</div>
) : (
<div className="space-y-4">
{/* File Info & Actions */}
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Preview: {parsedData.fileName}</h3>
<p className="text-sm text-gray-600">
Found {parsedData.data.length} users, {selectedUsers.size} selected
</p>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setParsedData(null)}
>
<Trash2 className="h-4 w-4 mr-2" />
Clear
</Button>
<Button
onClick={handleBulkCreateUsers}
disabled={bulkLoading || selectedUsers.size === 0}
>
{bulkLoading ? "Creating..." : `Create ${selectedUsers.size} Users`}
</Button>
</div>
</div>
{/* Errors Display */}
{parsedData.errors.length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>Validation Errors:</strong>
<ul className="mt-2 space-y-1">
{parsedData.errors.slice(0, 5).map((error, index) => (
<li key={index} className="text-sm">• {error}</li>
))}
{parsedData.errors.length > 5 && (
<li className="text-sm">• ... and {parsedData.errors.length - 5} more errors</li>
)}
</ul>
</AlertDescription>
</Alert>
)}
{/* Data Table */}
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectAll}
onCheckedChange={(checked) => handleSelectAll(checked || false)}
/>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>Role</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{parsedData.data.map((user, index) => (
<TableRow key={index}>
<TableCell>
<Checkbox
checked={selectedUsers.has(index)}
onCheckedChange={(checked) => handleSelectUser(index, checked || false)}
disabled={!!user.validationError}
/>
</TableCell>
<TableCell className="font-medium">
{user.name || <span className="text-gray-400">Missing</span>}
</TableCell>
<TableCell>
{user.email || <span className="text-gray-400">Missing</span>}
</TableCell>
<TableCell>
{user.role || <span className="text-gray-400">Missing</span>}
</TableCell>
<TableCell>
{user.validationError ? (
<Badge variant="destructive" className="text-xs">
{user.validationError}
</Badge>
) : (
<Badge className="bg-green-100 text-green-800 text-xs">
Valid
</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-3 h-4 w-4 text-gray-400" />
<Input
placeholder="Cari nama, ID anggota, atau departemen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="w-full md:w-48">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger>
<Filter className="h-4 w-4 mr-2" />
<SelectValue placeholder="Filter Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Semua Status</SelectItem>
<SelectItem value="verified">Terverifikasi</SelectItem>
<SelectItem value="pending">Menunggu</SelectItem>
<SelectItem value="rejected">Ditolak</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Members Table */}
<Card>
<CardHeader>
<CardTitle>Daftar Anggota ({filteredMembers.length})</CardTitle>
<CardDescription>Kelola verifikasi dan status keanggotaan METI</CardDescription>
</CardHeader>
<CardContent>
{/* Desktop Table View */}
<div className="hidden lg:block overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[120px]">ID Anggota</TableHead>
<TableHead className="min-w-[120px]">Username</TableHead>
<TableHead className="min-w-[150px]">Nama</TableHead>
<TableHead className="min-w-[120px]">Departemen</TableHead>
<TableHead className="min-w-[120px]">Tanggal Bergabung</TableHead>
<TableHead className="min-w-[140px]">Status</TableHead>
<TableHead className="min-w-[120px]">Login Terakhir</TableHead>
<TableHead className="min-w-[200px]">Aksi</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredMembers.map((member) => (
<TableRow key={member.id}>
<TableCell className="font-medium">{member.memberId}</TableCell>
<TableCell className="font-mono text-sm bg-gray-50 px-2 py-1 rounded">
{member.username}
</TableCell>
<TableCell className="font-medium">{member.name}</TableCell>
<TableCell>{member.department}</TableCell>
<TableCell>
{member.joinDate ? new Date(member.joinDate).toLocaleDateString("id-ID") : "-"}
</TableCell>
<TableCell>{getStatusBadge(member.memberStatus)}</TableCell>
<TableCell>
{member.lastLogin
? new Date(member.lastLogin).toLocaleDateString("id-ID")
: "Belum pernah login"}
</TableCell>
<TableCell>
<div className="flex gap-2">
{/* Existing button logic remains the same */}
{member.memberStatus === "pending" && (
<>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="bg-green-600 hover:bg-green-700 text-white">
<CheckCircle className="h-3 w-3 mr-1" />
Verifikasi
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Verifikasi</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin memverifikasi anggota berikut?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Departemen:</strong> {member.department}
</p>
<p>
<strong>Status Saat Ini:</strong> Menunggu Verifikasi
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "verified")}
disabled={loading}
className="bg-green-600 hover:bg-green-700"
>
{loading ? "Memverifikasi..." : "Ya, Verifikasi"}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="destructive">
<XCircle className="h-3 w-3 mr-1" />
Tolak
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Penolakan</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menolak verifikasi anggota berikut?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Departemen:</strong> {member.department}
</p>
<p className="text-red-600">
<strong>Aksi:</strong> Anggota akan ditolak dan tidak dapat login
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "rejected")}
disabled={loading}
variant="destructive"
>
{loading ? "Menolak..." : "Ya, Tolak"}
</Button>
</div>
</DialogContent>
</Dialog>
</>
)}
{/* Other status buttons remain the same */}
{member.memberStatus === "rejected" && (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="bg-green-600 hover:bg-green-700 text-white">
<CheckCircle className="h-3 w-3 mr-1" />
Verifikasi
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Verifikasi</DialogTitle>
<DialogDescription>
Anggota ini sebelumnya ditolak. Apakah Anda yakin ingin memverifikasinya sekarang?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Status Saat Ini:</strong> <span className="text-red-600">Ditolak</span>
</p>
<p className="text-green-600">
<strong>Aksi:</strong> Anggota akan dapat login dan voting
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "verified")}
disabled={loading}
className="bg-green-600 hover:bg-green-700"
>
{loading ? "Memverifikasi..." : "Ya, Verifikasi"}
</Button>
</div>
</DialogContent>
</Dialog>
)}
{/* Button untuk status verified */}
{member.memberStatus === "verified" && (
<Dialog>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="border-yellow-300 text-yellow-700 hover:bg-yellow-50 bg-transparent"
>
<Clock className="h-3 w-3 mr-1" />
Tunda
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Penangguhan</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menangguhkan verifikasi anggota ini?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Status Saat Ini:</strong>{" "}
<span className="text-green-600">Terverifikasi</span>
</p>
<p className="text-yellow-600">
<strong>Aksi:</strong> Anggota tidak akan dapat login sementara
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "pending")}
disabled={loading}
className="bg-yellow-600 hover:bg-yellow-700 text-white"
>
{loading ? "Menangguhkan..." : "Ya, Tunda"}
</Button>
</div>
</DialogContent>
</Dialog>
)}
{/* Debug info - hapus ini setelah testing */}
<span className="text-xs text-gray-500 ml-2">Status: {member.memberStatus}</span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{/* Mobile Card View */}
<div className="lg:hidden space-y-4">
{filteredMembers.map((member) => (
<Card key={member.id} className="p-4">
<div className="space-y-3">
<div className="flex justify-between items-start">
<div>
<h3 className="font-semibold text-lg">{member.name}</h3>
<p className="text-sm text-gray-600">{member.memberId}</p>
<p className="text-xs text-gray-500 font-mono bg-gray-100 px-2 py-1 rounded inline-block mt-1">
@{member.username}
</p>
</div>
{getStatusBadge(member.memberStatus)}
</div>
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-gray-500">Departemen:</span>
<p className="font-medium">{member.department}</p>
</div>
<div>
<span className="text-gray-500">Bergabung:</span>
<p className="font-medium">
{member.joinDate ? new Date(member.joinDate).toLocaleDateString("id-ID") : "-"}
</p>
</div>
</div>
<div className="text-sm">
<span className="text-gray-500">Login Terakhir:</span>
<p className="font-medium">
{member.lastLogin
? new Date(member.lastLogin).toLocaleDateString("id-ID")
: "Belum pernah login"}
</p>
</div>
{/* Mobile Action Buttons */}
<div className="flex gap-2 pt-2">
{member.memberStatus === "pending" && (
<>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="bg-green-600 hover:bg-green-700 text-white flex-1">
<CheckCircle className="h-3 w-3 mr-1" />
Verifikasi
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Verifikasi</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin memverifikasi anggota berikut?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Departemen:</strong> {member.department}
</p>
<p>
<strong>Status Saat Ini:</strong> Menunggu Verifikasi
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "verified")}
disabled={loading}
className="bg-green-600 hover:bg-green-700"
>
{loading ? "Memverifikasi..." : "Ya, Verifikasi"}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="destructive" className="flex-1">
<XCircle className="h-3 w-3 mr-1" />
Tolak
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Penolakan</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menolak verifikasi anggota berikut?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Departemen:</strong> {member.department}
</p>
<p className="text-red-600">
<strong>Aksi:</strong> Anggota akan ditolak dan tidak dapat login
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "rejected")}
disabled={loading}
variant="destructive"
>
{loading ? "Menolak..." : "Ya, Tolak"}
</Button>
</div>
</DialogContent>
</Dialog>
</>
)}
{member.memberStatus === "rejected" && (
<Dialog>
<DialogTrigger asChild>
<Button size="sm" className="bg-green-600 hover:bg-green-700 text-white w-full">
<CheckCircle className="h-3 w-3 mr-1" />
Verifikasi
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Verifikasi</DialogTitle>
<DialogDescription>
Anggota ini sebelumnya ditolak. Apakah Anda yakin ingin memverifikasinya sekarang?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Status Saat Ini:</strong> <span className="text-red-600">Ditolak</span>
</p>
<p className="text-green-600">
<strong>Aksi:</strong> Anggota akan dapat login dan voting
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "verified")}
disabled={loading}
className="bg-green-600 hover:bg-green-700"
>
{loading ? "Memverifikasi..." : "Ya, Verifikasi"}
</Button>
</div>
</DialogContent>
</Dialog>
)}
{member.memberStatus === "verified" && (
<Dialog>
<DialogTrigger asChild>
<Button
size="sm"
variant="outline"
className="border-yellow-300 text-yellow-700 hover:bg-yellow-50 bg-transparent w-full"
>
<Clock className="h-3 w-3 mr-1" />
Tunda
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Konfirmasi Penangguhan</DialogTitle>
<DialogDescription>
Apakah Anda yakin ingin menangguhkan verifikasi anggota ini?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="space-y-2">
<p>
<strong>Nama:</strong> {member.name}
</p>
<p>
<strong>ID Anggota:</strong> {member.memberId}
</p>
<p>
<strong>Status Saat Ini:</strong>{" "}
<span className="text-green-600">Terverifikasi</span>
</p>
<p className="text-yellow-600">
<strong>Aksi:</strong> Anggota tidak akan dapat login sementara
</p>
</div>
</div>
<div className="flex justify-end gap-3">
<DialogTrigger asChild>
<Button variant="outline">Batal</Button>
</DialogTrigger>
<Button
onClick={() => handleStatusChange(member.id, "pending")}
disabled={loading}
className="bg-yellow-600 hover:bg-yellow-700 text-white"
>
{loading ? "Menangguhkan..." : "Ya, Tunda"}
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
</div>
</Card>
))}
</div>
{filteredMembers.length === 0 && (
<div className="text-center py-8">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">Tidak ada anggota ditemukan</h3>
<p className="text-gray-600">Coba ubah filter atau kata kunci pencarian</p>
</div>
)}
</CardContent>
</Card>
</div>
</div>
)
}
export default function MembersPage() {
return (
<AuthGuard requiredRole="admin">
<MembersPageContent />
</AuthGuard>
)
}