'use client' import { Eye, Pencil, Wifi, WifiOff } from 'lucide-react' import Link from 'next/link' import { useState } from 'react' import { toast } from 'sonner' import { bulkBan, bulkGiveBadge, bulkGiveCurrency, bulkUnban } from '@/actions/bulk-users' import { DataTable } from '@/components/admin/data-table' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { getAvatarUrl } from '@/lib/imager' import type { DataTableColumn, PaginatedResult } from '@/types' interface UserRow { id: number username: string mail: string rank: number credits: number pixels: number online: string motto: string look: string ipRegister: string accountCreated: number lastLogin: number | null } const RANK_LABELS: Record = { 1: 'User', 2: 'Bronze', 3: 'Silver', 4: 'Gold', 5: 'Moderator', 6: 'Admin', 7: 'Super Admin', } function getColumns(canEdit: boolean): DataTableColumn[] { const cols: DataTableColumn[] = [ { key: 'id', label: 'ID', sortable: true }, { key: 'username', label: 'User', sortable: true, render: (_, row) => (
{row.username} {row.motto && ( {row.motto} )}
), }, { key: 'mail', label: 'Email', sortable: true }, { key: 'rank', label: 'Rank', sortable: true, filterKey: 'filter_rank', filterOptions: [ { label: 'User (1)', value: '1' }, { label: 'Bronze (2)', value: '2' }, { label: 'Silver (3)', value: '3' }, { label: 'Gold (4)', value: '4' }, { label: 'Moderator (5)', value: '5' }, { label: 'Admin (6)', value: '6' }, { label: 'Super Admin (7+)', value: '7' }, ], render: (value) => { const r = Number(value) return ( = 7 ? 'default' : r >= 5 ? 'secondary' : 'outline'}> {RANK_LABELS[r] || `Rank ${r}`} ) }, }, { key: 'credits', label: 'Credits', sortable: true, render: (value) => {Number(value).toLocaleString()}, }, { key: 'online', label: 'Status', filterKey: 'filter_online', filterOptions: [ { label: 'Online', value: '1' }, { label: 'Offline', value: '0' }, ], render: (value) => value === '1' ? ( Online ) : ( Offline ), }, { key: 'accountCreated', label: 'Registered', sortable: true, render: (value) => { const ts = Number(value) if (!ts) return '-' return ( {new Date(ts * 1000).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric', })} ) }, }, { key: 'actions', label: '', render: (_, row) => (
{canEdit && ( )}
), }, ] return cols } interface UsersTableProps { data: PaginatedResult canEdit: boolean canBan: boolean } type PendingDialog = | { kind: 'ban'; ids: number[] } | { kind: 'give-currency'; ids: number[] } | { kind: 'give-badge'; ids: number[] } | null export function UsersTable({ data, canEdit, canBan }: UsersTableProps) { const [pending, setPending] = useState(null) const [busy, setBusy] = useState(false) const [banReason, setBanReason] = useState('Bulk ban') const [banDuration, setBanDuration] = useState('-1') const [currencyType, setCurrencyType] = useState<'credits' | 'pixels' | 'points'>('credits') const [currencyAmount, setCurrencyAmount] = useState('100') const [badgeCode, setBadgeCode] = useState('') function reportResult(label: string, ok: number, total: number, failures = 0) { if (ok > 0) toast.success(`${label}: ${ok}/${total}${failures ? ` · ${failures} failed` : ''}`) if (failures > 0 && ok === 0) toast.error(`${label} failed for ${failures} users`) } async function handleBulkAction(action: string, ids: (number | string)[]) { const numericIds = ids.map((i) => Number(i)) if (action === 'unban') { setBusy(true) try { const result = await bulkUnban({ userIds: numericIds }) if (result.ok) { const data = result.data as { unbanned: number; total: number } reportResult('Unbanned', data.unbanned, data.total) } else toast.error(result.error) } finally { setBusy(false) } return } if (action === 'ban') { setPending({ kind: 'ban', ids: numericIds }) return } if (action === 'give-currency') { setPending({ kind: 'give-currency', ids: numericIds }) return } if (action === 'give-badge') { setPending({ kind: 'give-badge', ids: numericIds }) return } } async function confirmBan() { if (pending?.kind !== 'ban') return setBusy(true) try { const result = await bulkBan({ userIds: pending.ids, reason: banReason, duration: Number(banDuration), }) if (result.ok) { const data = result.data as { banned: number } reportResult('Banned', data.banned, pending.ids.length) } else toast.error(result.error) setPending(null) } finally { setBusy(false) } } async function confirmGiveCurrency() { if (pending?.kind !== 'give-currency') return setBusy(true) try { const result = await bulkGiveCurrency({ userIds: pending.ids, amount: Number(currencyAmount), type: currencyType, }) if (result.ok) { const data = result.data as { given: number total: number failedIds: { userId: number; reason: string }[] } reportResult( `Given ${currencyAmount} ${currencyType}`, data.given, data.total, data.failedIds.length, ) } else toast.error(result.error) setPending(null) } finally { setBusy(false) } } async function confirmGiveBadge() { if (pending?.kind !== 'give-badge') return setBusy(true) try { const result = await bulkGiveBadge({ userIds: pending.ids, badgeCode: badgeCode.trim().toUpperCase(), }) if (result.ok) { const data = result.data as { given: number total: number failedIds: { userId: number; reason: string }[] } reportResult(`Given badge ${badgeCode}`, data.given, data.total, data.failedIds.length) } else toast.error(result.error) setPending(null) } finally { setBusy(false) } } const bulkActions: { label: string; value: string; variant?: 'destructive' | 'default' }[] = [] if (canBan) { bulkActions.push({ label: 'Ban', value: 'ban', variant: 'destructive' }) bulkActions.push({ label: 'Unban', value: 'unban' }) } if (canEdit) { bulkActions.push({ label: 'Give credits', value: 'give-currency' }) bulkActions.push({ label: 'Give badge', value: 'give-badge' }) } return ( <> 0 ? bulkActions : undefined} onBulkAction={bulkActions.length > 0 ? handleBulkAction : undefined} exportUrl={canEdit ? '/api/admin/export?type=users' : undefined} /> !o && setPending(null)}> Ban {pending?.kind === 'ban' ? pending.ids.length : 0} users Permanent ban: leave duration at -1.
setBanReason(e.target.value)} />
setBanDuration(e.target.value)} />
!o && setPending(null)}> Give currency to {pending?.kind === 'give-currency' ? pending.ids.length : 0} users
setCurrencyAmount(e.target.value)} />
!o && setPending(null)}> Give badge to {pending?.kind === 'give-badge' ? pending.ids.length : 0} users
setBadgeCode(e.target.value)} placeholder="ACH_*, etc." />
) }