'use client'; import { useState, useEffect, useCallback } from 'react'; import Tab from '@mui/material/Tab'; import Box from '@mui/material/Box'; import Card from '@mui/material/Card'; import Chip from '@mui/material/Chip'; import Grid from '@mui/material/Grid'; import Tabs from '@mui/material/Tabs'; import List from '@mui/material/List'; import Stack from '@mui/material/Stack'; import Alert from '@mui/material/Alert'; import Avatar from '@mui/material/Avatar'; import Button from '@mui/material/Button'; import Dialog from '@mui/material/Dialog'; import Divider from '@mui/material/Divider'; import Tooltip from '@mui/material/Tooltip'; import ListItem from '@mui/material/ListItem'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import CardHeader from '@mui/material/CardHeader'; import IconButton from '@mui/material/IconButton'; import DialogTitle from '@mui/material/DialogTitle'; import CardContent from '@mui/material/CardContent'; import ListItemText from '@mui/material/ListItemText'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import InputAdornment from '@mui/material/InputAdornment'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import CircularProgress from '@mui/material/CircularProgress'; import { paths } from 'src/routes/paths'; import { useRouter } from 'src/routes/hooks'; import { resolveMediaUrl } from 'src/utils/axios'; import { getStudents, getMyMentors, getMyRequests, getMyInvitations, addStudentInvitation, generateInvitationLink, sendMentorshipRequest, acceptMentorshipRequest, rejectMentorshipRequest, rejectInvitationAsStudent, confirmInvitationAsStudent, getMentorshipRequestsPending, removeStudent, removeMentor, } from 'src/utils/students-api'; import { DashboardContent } from 'src/layouts/dashboard'; import { Iconify } from 'src/components/iconify'; import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs'; import { useAuthContext } from 'src/auth/hooks'; // ---------------------------------------------------------------------- const avatarUrl = (href) => resolveMediaUrl(href) || null; function initials(firstName, lastName) { return `${(firstName || '')[0] || ''}${(lastName || '')[0] || ''}`.toUpperCase(); } // ---------------------------------------------------------------------- function RemoveConnectionDialog({ open, onClose, onConfirm, name, type, loading }) { return ( {type === 'student' ? `Удалить ученика ${name}?` : `Удалить ментора ${name}?`} Это действие нельзя отменить автоматически При удалении произойдёт следующее: {[ 'Все будущие занятия будут отменены', 'Доступ к общим доскам будет приостановлен', 'Доступ к материалам будет закрыт', ].map((item) => ( {item} ))} Доски и файлы не удаляются. Если связь будет восстановлена — доступ вернётся автоматически. ); } // ---------------------------------------------------------------------- // MENTOR VIEWS function MentorStudentList({ onRefresh }) { const router = useRouter(); const [students, setStudents] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [error, setError] = useState(null); const [removeTarget, setRemoveTarget] = useState(null); // { id, name } const [removing, setRemoving] = useState(false); const handleRemove = async () => { try { setRemoving(true); await removeStudent(removeTarget.id); setRemoveTarget(null); load(); onRefresh?.(); } catch (e) { setError(e?.response?.data?.error?.message || e?.message || 'Ошибка удаления'); setRemoveTarget(null); } finally { setRemoving(false); } }; const load = useCallback(async () => { try { setLoading(true); const res = await getStudents({ page_size: 200 }); setStudents(res.results); } catch (e) { setError(e?.response?.data?.detail || e?.message || 'Ошибка загрузки'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const filtered = students.filter((s) => { if (!search.trim()) return true; const q = search.toLowerCase(); const u = s.user || {}; return ( (u.first_name || '').toLowerCase().includes(q) || (u.last_name || '').toLowerCase().includes(q) || (u.email || '').toLowerCase().includes(q) ); }); if (loading) return ; return ( setSearch(e.target.value)} size="small" InputProps={{ startAdornment: , }} /> {error && {error}} {filtered.length === 0 ? ( {search ? 'Ничего не найдено' : 'Нет учеников'} ) : ( {filtered.map((s) => { const u = s.user || {}; const name = `${u.first_name || ''} ${u.last_name || ''}`.trim() || u.email || '—'; return ( t.customShadows?.z16 || '0 8px 24px rgba(0,0,0,0.12)', }, }} > { e.stopPropagation(); setRemoveTarget({ id: s.id, name }); }} sx={{ position: 'absolute', top: 8, right: 8 }} > router.push(paths.dashboard.studentDetail(s.id))} sx={{ cursor: 'pointer' }}> {initials(u.first_name, u.last_name)} {name} {u.email || ''} {s.total_lessons != null && ( } /> )} {s.subject && } ); })} )} setRemoveTarget(null)} onConfirm={handleRemove} name={removeTarget?.name || ''} type="student" loading={removing} /> ); } function MentorRequests({ onRefresh }) { const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(true); const [processing, setProcessing] = useState(null); const [error, setError] = useState(null); const load = useCallback(async () => { try { setLoading(true); const res = await getMentorshipRequestsPending(); setRequests(Array.isArray(res) ? res : []); } catch (e) { setError(e?.response?.data?.detail || e?.message || 'Ошибка загрузки'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const handle = async (id, action) => { try { setProcessing(id); if (action === 'accept') await acceptMentorshipRequest(id); else await rejectMentorshipRequest(id); await load(); onRefresh(); } catch (e) { setError(e?.response?.data?.detail || e?.message || 'Ошибка'); } finally { setProcessing(null); } }; if (loading) return ; return ( {error && {error}} {requests.length === 0 ? ( Нет входящих заявок ) : ( {requests.map((r) => { const s = r.student || {}; return ( } > {initials(s.first_name, s.last_name)} ); })} )} ); } // 8 отдельных полей для ввода кода function CodeInput({ value, onChange, disabled }) { const handleChange = (e) => { const v = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8); onChange(v); }; return ( ); } function InviteDialog({ open, onClose, onSuccess }) { const [mode, setMode] = useState('email'); // 'email' | 'code' | 'link' const [emailValue, setEmailValue] = useState(''); const [codeValue, setCodeValue] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); const [linkData, setLinkData] = useState(null); // { invitation_link, expires_at } const [linkLoading, setLinkLoading] = useState(false); const [copied, setCopied] = useState(false); const reset = () => { setEmailValue(''); setCodeValue(''); setError(null); setSuccessMsg(null); setLinkData(null); setCopied(false); }; const handleClose = () => { if (!loading && !linkLoading) { reset(); onClose(); } }; const handleSend = async () => { const val = mode === 'email' ? emailValue.trim() : codeValue.trim(); if (!val) return; try { setLoading(true); setError(null); const payload = mode === 'email' ? { email: val } : { universal_code: val }; const res = await addStudentInvitation(payload); setSuccessMsg(res?.message || 'Приглашение отправлено'); onSuccess(); } catch (e) { const msg = e?.response?.data?.error?.message || e?.response?.data?.detail || e?.response?.data?.email?.[0] || e?.message || 'Ошибка'; setError(msg); } finally { setLoading(false); } }; const handleGenerateLink = async () => { try { setLinkLoading(true); setError(null); const res = await generateInvitationLink(); setLinkData(res); setCopied(false); } catch (e) { setError(e?.response?.data?.error?.message || e?.message || 'Ошибка генерации ссылки'); } finally { setLinkLoading(false); } }; const handleCopyLink = () => { navigator.clipboard.writeText(linkData.invitation_link); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const expiresText = linkData?.expires_at ? `Действует до ${new Date(linkData.expires_at).toLocaleString('ru-RU', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit' })}` : ''; const canSend = mode === 'email' ? !!emailValue.trim() : codeValue.length === 8; return ( Пригласить ученика { setMode(v); setError(null); setSuccessMsg(null); }}> {mode === 'email' && ( setEmailValue(e.target.value)} disabled={loading} fullWidth size="small" autoFocus /> )} {mode === 'code' && ( Введите 8-значный код ученика )} {mode === 'link' && ( Сгенерируйте ссылку для регистрации ученика. Каждая ссылка действует 12 часов и может быть использована только 1 раз. {!linkData ? ( ) : ( ), }} /> {expiresText && ( {expiresText} · только 1 ученик )} )} )} {error && {error}} {successMsg && {successMsg}} {mode !== 'link' && ( )} ); } // ---------------------------------------------------------------------- // CLIENT VIEWS function ClientMentorList() { const [mentors, setMentors] = useState([]); const [loading, setLoading] = useState(true); const [removeTarget, setRemoveTarget] = useState(null); const [removing, setRemoving] = useState(false); const load = useCallback(() => { setLoading(true); getMyMentors() .then((list) => setMentors(Array.isArray(list) ? list : [])) .catch(() => setMentors([])) .finally(() => setLoading(false)); }, []); useEffect(() => { load(); }, [load]); const handleRemove = async () => { try { setRemoving(true); await removeMentor(removeTarget.id); setRemoveTarget(null); load(); } catch (e) { setRemoveTarget(null); } finally { setRemoving(false); } }; if (loading) return ; if (mentors.length === 0) { return ( Нет подключённых менторов ); } return ( <> {mentors.map((m) => { const name = `${m.first_name || ''} ${m.last_name || ''}`.trim() || m.email || '—'; return ( t.customShadows?.z16 || '0 8px 24px rgba(0,0,0,0.12)', }, }} > setRemoveTarget({ id: m.id, name })} sx={{ position: 'absolute', top: 8, right: 8 }} > {initials(m.first_name, m.last_name)} {name} {m.email || ''} ); })} setRemoveTarget(null)} onConfirm={handleRemove} name={removeTarget?.name || ''} type="mentor" loading={removing} /> ); } function ClientInvitations({ onRefresh }) { const [invitations, setInvitations] = useState([]); const [loading, setLoading] = useState(true); const [processing, setProcessing] = useState(null); const [error, setError] = useState(null); const load = useCallback(async () => { try { setLoading(true); const res = await getMyInvitations(); setInvitations(Array.isArray(res) ? res.filter((i) => ['pending', 'pending_student', 'pending_parent'].includes(i.status)) : []); } catch { setInvitations([]); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const handle = async (id, action) => { try { setProcessing(id); if (action === 'confirm') await confirmInvitationAsStudent(id); else await rejectInvitationAsStudent(id); await load(); onRefresh(); } catch (e) { setError(e?.response?.data?.detail || e?.message || 'Ошибка'); } finally { setProcessing(null); } }; if (loading) return ; if (invitations.length === 0) return ( Нет входящих приглашений ); return ( {error && {error}} {invitations.map((inv) => { const m = inv.mentor || {}; return ( } > {initials(m.first_name, m.last_name)} ); })} ); } function SendRequestDialog({ open, onClose, onSuccess }) { const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [successMsg, setSuccessMsg] = useState(null); const handleSend = async () => { if (!code.trim()) return; try { setLoading(true); setError(null); const res = await sendMentorshipRequest(code.trim()); setSuccessMsg(res?.message || 'Заявка отправлена'); onSuccess(); } catch (e) { setError(e?.response?.data?.detail || e?.response?.data?.mentor_code?.[0] || e?.message || 'Ошибка'); } finally { setLoading(false); } }; return ( { if (!loading) { setCode(''); setError(null); setSuccessMsg(null); onClose(); } }} maxWidth="xs" fullWidth> Найти ментора setCode(e.target.value.toUpperCase())} disabled={loading} fullWidth size="small" placeholder="XXXXXXXX" /> {error && {error}} {successMsg && {successMsg}} ); } const STATUS_LABELS = { pending_mentor: { label: 'Ожидает ментора', color: 'warning' }, pending_student: { label: 'Ожидает вас', color: 'info' }, accepted: { label: 'Принято', color: 'success' }, rejected: { label: 'Отклонено', color: 'error' }, removed: { label: 'Удалено', color: 'default' }, }; function ClientOutgoingRequests() { const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { getMyRequests() .then((data) => setRequests(Array.isArray(data) ? data : [])) .catch(() => setRequests([])) .finally(() => setLoading(false)); }, []); if (loading) return ; if (requests.length === 0) return ( Нет исходящих заявок ); return ( {requests.map((req) => { const m = req.mentor || {}; const name = `${m.first_name || ''} ${m.last_name || ''}`.trim() || m.email || '—'; const statusInfo = STATUS_LABELS[req.status] || { label: req.status, color: 'default' }; return ( {initials(m.first_name, m.last_name)} ); })} ); } // ---------------------------------------------------------------------- export function StudentsView() { const { user } = useAuthContext(); const isMentor = user?.role === 'mentor'; const [tab, setTab] = useState(0); const [inviteOpen, setInviteOpen] = useState(false); const [requestOpen, setRequestOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const refresh = () => setRefreshKey((k) => k + 1); return ( } onClick={() => setInviteOpen(true)}> Пригласить ученика ) : ( ) } sx={{ mb: 3 }} /> {isMentor ? ( <> setTab(v)} sx={{ mb: 3 }}> {tab === 0 && } {tab === 1 && } setInviteOpen(false)} onSuccess={refresh} /> ) : ( <> setTab(v)} sx={{ mb: 3 }}> {tab === 0 && } {tab === 1 && } {tab === 2 && } setRequestOpen(false)} onSuccess={refresh} /> )} ); }