'use client' import { useEffect, useMemo, useState } from 'react' import { useRouter } from 'next/navigation' import AdminShell from '../../ui/AdminShell' import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth' type ProfileOption = { id: number name: string } type Invite = { id: number code: string label?: string | null description?: string | null profile_id?: number | null profile?: ProfileOption | null role?: 'user' | 'admin' | null max_uses?: number | null use_count: number remaining_uses?: number | null enabled: boolean expires_at?: string | null is_expired?: boolean is_usable?: boolean created_at?: string | null } type InviteForm = { code: string label: string description: string profile_id: string role: '' | 'user' | 'admin' max_uses: string enabled: boolean expires_at: string } const defaultForm = (): InviteForm => ({ code: '', label: '', description: '', profile_id: '', role: '', max_uses: '', enabled: true, expires_at: '', }) const formatDate = (value?: string | null) => { if (!value) return 'Never' const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } export default function AdminInvitesPage() { const router = useRouter() const [invites, setInvites] = useState([]) const [profiles, setProfiles] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [status, setStatus] = useState(null) const [editingId, setEditingId] = useState(null) const [form, setForm] = useState(defaultForm()) const signupBaseUrl = useMemo(() => { if (typeof window === 'undefined') return '/signup' return `${window.location.origin}/signup` }, []) const handleAuthResponse = (response: Response) => { if (response.status === 401) { clearToken() router.push('/login') return true } if (response.status === 403) { router.push('/') return true } return false } const loadData = async () => { if (!getToken()) { router.push('/login') return } setLoading(true) setError(null) try { const baseUrl = getApiBase() const [inviteRes, profileRes] = await Promise.all([ authFetch(`${baseUrl}/admin/invites`), authFetch(`${baseUrl}/admin/profiles`), ]) if (!inviteRes.ok) { if (handleAuthResponse(inviteRes)) return throw new Error(`Failed to load invites (${inviteRes.status})`) } if (!profileRes.ok) { if (handleAuthResponse(profileRes)) return throw new Error(`Failed to load profiles (${profileRes.status})`) } const [inviteData, profileData] = await Promise.all([inviteRes.json(), profileRes.json()]) setInvites(Array.isArray(inviteData?.invites) ? inviteData.invites : []) const profileRows = Array.isArray(profileData?.profiles) ? profileData.profiles : [] setProfiles( profileRows.map((profile: any) => ({ id: Number(profile.id ?? 0), name: String(profile.name ?? 'Unnamed'), })) ) } catch (err) { console.error(err) setError('Could not load invites.') } finally { setLoading(false) } } useEffect(() => { void loadData() }, []) const resetEditor = () => { setEditingId(null) setForm(defaultForm()) } const editInvite = (invite: Invite) => { setEditingId(invite.id) setForm({ code: invite.code ?? '', label: invite.label ?? '', description: invite.description ?? '', profile_id: typeof invite.profile_id === 'number' && invite.profile_id > 0 ? String(invite.profile_id) : '', role: (invite.role ?? '') as '' | 'user' | 'admin', max_uses: typeof invite.max_uses === 'number' ? String(invite.max_uses) : '', enabled: invite.enabled !== false, expires_at: invite.expires_at ?? '', }) setStatus(null) setError(null) } const saveInvite = async (event: React.FormEvent) => { event.preventDefault() setSaving(true) setError(null) setStatus(null) try { const baseUrl = getApiBase() const payload = { code: form.code || null, label: form.label || null, description: form.description || null, profile_id: form.profile_id || null, role: form.role || null, max_uses: form.max_uses || null, enabled: form.enabled, expires_at: form.expires_at || null, } const url = editingId == null ? `${baseUrl}/admin/invites` : `${baseUrl}/admin/invites/${editingId}` const response = await authFetch(url, { method: editingId == null ? 'POST' : 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }) if (!response.ok) { if (handleAuthResponse(response)) return const text = await response.text() throw new Error(text || 'Save failed') } setStatus(editingId == null ? 'Invite created.' : 'Invite updated.') resetEditor() await loadData() } catch (err) { console.error(err) setError(err instanceof Error ? err.message : 'Could not save invite.') } finally { setSaving(false) } } const deleteInvite = async (invite: Invite) => { if (!window.confirm(`Delete invite "${invite.code}"?`)) return setError(null) setStatus(null) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/invites/${invite.id}`, { method: 'DELETE', }) if (!response.ok) { if (handleAuthResponse(response)) return const text = await response.text() throw new Error(text || 'Delete failed') } if (editingId === invite.id) resetEditor() setStatus(`Deleted invite ${invite.code}.`) await loadData() } catch (err) { console.error(err) setError(err instanceof Error ? err.message : 'Could not delete invite.') } } const copyInviteLink = async (invite: Invite) => { const url = `${signupBaseUrl}?code=${encodeURIComponent(invite.code)}` try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url) setStatus(`Copied invite link for ${invite.code}.`) } else { window.prompt('Copy invite link', url) } } catch (err) { console.error(err) window.prompt('Copy invite link', url) } } return ( } >
{error &&
{error}
} {status &&
{status}
}

{editingId == null ? 'Create invite' : 'Edit invite'}

Link an invite to a profile to apply account defaults at sign-up.