'use client'; import { useState, useEffect, useRef } from 'react'; import { AdminNav } from '@/components/AdminNav'; import { Button, Card, Input, Spinner } from '@/components/ui'; import { api } from '@/lib/api'; import { formatPoints, formatDate } from '@/lib/utils'; import type { AdminStudent, AdminUsersPage } from '@/lib/types'; interface GrantRecord { key: string; studentName: string; amount: number; description: string; grantedAt: string; } export default function GrantPage() { const [search, setSearch] = useState(''); const [suggestions, setSuggestions] = useState([]); const [selected, setSelected] = useState(null); const [showDropdown, setShowDropdown] = useState(false); const [amount, setAmount] = useState(''); const [description, setDescription] = useState(''); const [loading, setLoading] = useState(false); const [searching, setSearching] = useState(false); const [error, setError] = useState(''); const [history, setHistory] = useState([]); const dropdownRef = useRef(null); // Debounced search: fires 300 ms after the user stops typing. useEffect(() => { if (!search.trim() || selected) { setSuggestions([]); setShowDropdown(false); return; } setSearching(true); const timer = setTimeout(async () => { try { const page = await api.get( `/api/v1/admin/users?search=${encodeURIComponent(search)}&limit=8`, ); setSuggestions(page.users); setShowDropdown(page.users.length > 0); } catch { setSuggestions([]); } finally { setSearching(false); } }, 300); return () => clearTimeout(timer); }, [search, selected]); // Close dropdown when clicking outside. useEffect(() => { function handler(e: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setShowDropdown(false); } } document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); function selectStudent(student: AdminStudent) { setSelected(student); setSearch(student.name + ' — ' + student.email); setSuggestions([]); setShowDropdown(false); } function clearSelection() { setSelected(null); setSearch(''); setSuggestions([]); } async function handleGrant() { if (!selected || !amount) return; const pts = parseInt(amount, 10); if (isNaN(pts) || pts <= 0) return; setLoading(true); setError(''); try { await api.post<{ status: string }>('/api/v1/admin/points/grant', { user_id: selected.id, amount: pts, description, }); // Refresh selected student balance. const page = await api.get( `/api/v1/admin/users?search=${encodeURIComponent(selected.email)}&limit=1`, ); const updated = page.users.find((s) => s.id === selected.id); if (updated) setSelected(updated); setHistory((prev) => [ { key: String(Date.now()), studentName: selected.name, amount: pts, description: description || '—', grantedAt: new Date().toISOString(), }, ...prev, ].slice(0, 5), ); setAmount(''); setDescription(''); } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка начисления'); } finally { setLoading(false); } } const amountNum = parseInt(amount, 10); const canSubmit = !!selected && amountNum > 0 && !loading; return (

Начислить поинты

{/* Student search */}
{ setSearch(e.target.value); if (selected) clearSelection(); }} placeholder="student@cu.ru" autoComplete="off" />
{selected && ( )}
{/* Suggestions dropdown */} {showDropdown && suggestions.length > 0 && (
    {suggestions.map((s) => (
  • ))}
)} {searching && !showDropdown && (
)}
{/* Selected student info */} {selected && (

{selected.name}

{selected.email}

Текущий баланс: {formatPoints(selected.balance)}

)} setAmount(e.target.value)} placeholder="100" /> setDescription(e.target.value)} placeholder="Победа в хакатоне" /> {error && (

{error}

)}
{/* Recent grants in this session */} {history.length > 0 && (

Последние начисления (сессия)

    {history.map((h) => (
  • {h.studentName}

    {h.description}

    +{h.amount}

    {formatDate(h.grantedAt)}

  • ))}
)}
); }