uchill/front_minimal/src/sections/students/view/students-view.jsx

894 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>
{type === 'student' ? `Удалить ученика ${name}?` : `Удалить ментора ${name}?`}
</DialogTitle>
<DialogContent>
<Alert severity="warning" sx={{ mb: 2 }}>
Это действие нельзя отменить автоматически
</Alert>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
При удалении произойдёт следующее:
</Typography>
<Stack spacing={0.75}>
{[
'Все будущие занятия будут отменены',
'Доступ к общим доскам будет приостановлен',
'Доступ к материалам будет закрыт',
].map((item) => (
<Stack key={item} direction="row" spacing={1} alignItems="flex-start">
<Iconify icon="eva:close-circle-fill" width={16} sx={{ color: 'error.main', mt: 0.3, flexShrink: 0 }} />
<Typography variant="body2">{item}</Typography>
</Stack>
))}
</Stack>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
<strong>Доски и файлы не удаляются.</strong> Если связь будет восстановлена доступ вернётся автоматически.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>Отмена</Button>
<Button
variant="contained"
color="error"
onClick={onConfirm}
disabled={loading}
startIcon={loading ? <CircularProgress size={16} /> : null}
>
{loading ? 'Удаление...' : 'Удалить'}
</Button>
</DialogActions>
</Dialog>
);
}
// ----------------------------------------------------------------------
// 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 <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>;
return (
<Stack spacing={3}>
<TextField
placeholder="Поиск учеников..."
value={search}
onChange={(e) => setSearch(e.target.value)}
size="small"
InputProps={{
startAdornment: <InputAdornment position="start"><Iconify icon="eva:search-outline" /></InputAdornment>,
}}
/>
{error && <Alert severity="error">{error}</Alert>}
{filtered.length === 0 ? (
<Box sx={{ py: 8, textAlign: 'center' }}>
<Iconify icon="solar:users-group-rounded-bold" width={48} sx={{ color: 'text.disabled', mb: 2 }} />
<Typography variant="body2" color="text.secondary">
{search ? 'Ничего не найдено' : 'Нет учеников'}
</Typography>
</Box>
) : (
<Grid container spacing={2}>
{filtered.map((s) => {
const u = s.user || {};
const name = `${u.first_name || ''} ${u.last_name || ''}`.trim() || u.email || '—';
return (
<Grid item key={s.id} xs={12} sm={6} md={4} lg={3}>
<Card
sx={{
p: 3,
textAlign: 'center',
height: '100%',
position: 'relative',
transition: 'box-shadow 0.2s, transform 0.2s',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: (t) => t.customShadows?.z16 || '0 8px 24px rgba(0,0,0,0.12)',
},
}}
>
<Tooltip title="Удалить ученика">
<IconButton
size="small"
color="error"
onClick={(e) => { e.stopPropagation(); setRemoveTarget({ id: s.id, name }); }}
sx={{ position: 'absolute', top: 8, right: 8 }}
>
<Iconify icon="solar:trash-bin-trash-bold" width={18} />
</IconButton>
</Tooltip>
<Box onClick={() => router.push(paths.dashboard.studentDetail(s.id))} sx={{ cursor: 'pointer' }}>
<Avatar
src={avatarUrl(u.avatar_url || u.avatar)}
sx={{ width: 72, height: 72, mx: 'auto', mb: 2, fontSize: 26, bgcolor: 'primary.main' }}
>
{initials(u.first_name, u.last_name)}
</Avatar>
<Typography variant="subtitle1" noWrap sx={{ mb: 0.5 }}>{name}</Typography>
<Typography variant="body2" color="text.secondary" noWrap sx={{ mb: 2 }}>{u.email || ''}</Typography>
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap" useFlexGap>
{s.total_lessons != null && (
<Chip label={`${s.total_lessons} уроков`} size="small" color="primary" variant="soft"
icon={<Iconify icon="solar:calendar-bold" width={14} />} />
)}
{s.subject && <Chip label={s.subject} size="small" variant="outlined" />}
</Stack>
</Box>
</Card>
</Grid>
);
})}
</Grid>
)}
<RemoveConnectionDialog
open={!!removeTarget}
onClose={() => setRemoveTarget(null)}
onConfirm={handleRemove}
name={removeTarget?.name || ''}
type="student"
loading={removing}
/>
</Stack>
);
}
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 <Box sx={{ display: 'flex', justifyContent: 'center', py: 3 }}><CircularProgress /></Box>;
return (
<Stack spacing={2}>
{error && <Alert severity="error">{error}</Alert>}
{requests.length === 0 ? (
<Typography variant="body2" color="text.secondary">Нет входящих заявок</Typography>
) : (
<List disablePadding>
{requests.map((r) => {
const s = r.student || {};
return (
<ListItem
key={r.id}
divider
secondaryAction={
<Stack direction="row" spacing={1}>
<Button
size="small"
variant="contained"
color="success"
onClick={() => handle(r.id, 'accept')}
disabled={processing === r.id}
>
Принять
</Button>
<Button
size="small"
variant="outlined"
color="error"
onClick={() => handle(r.id, 'reject')}
disabled={processing === r.id}
>
Отклонить
</Button>
</Stack>
}
>
<ListItemAvatar>
<Avatar src={avatarUrl(s.avatar)}>
{initials(s.first_name, s.last_name)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={`${s.first_name || ''} ${s.last_name || ''}`.trim() || s.email}
secondary={s.email}
/>
</ListItem>
);
})}
</List>
)}
</Stack>
);
}
// 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 (
<TextField
value={value}
onChange={handleChange}
disabled={disabled}
fullWidth
placeholder="ABCD1234"
autoComplete="off"
inputProps={{
maxLength: 8,
style: {
textAlign: 'center',
fontWeight: 700,
fontSize: 22,
letterSpacing: 6,
textTransform: 'uppercase',
},
}}
/>
);
}
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 (
<Dialog open={open} onClose={handleClose} maxWidth="xs" fullWidth>
<DialogTitle>Пригласить ученика</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<Tabs value={mode} onChange={(_, v) => { setMode(v); setError(null); setSuccessMsg(null); }}>
<Tab value="email" label="По email" />
<Tab value="code" label="По коду" />
<Tab value="link" label="По ссылке" />
</Tabs>
{mode === 'email' && (
<TextField
label="Email ученика"
value={emailValue}
onChange={(e) => setEmailValue(e.target.value)}
disabled={loading}
fullWidth
size="small"
autoFocus
/>
)}
{mode === 'code' && (
<Stack spacing={1}>
<Typography variant="body2" color="text.secondary" textAlign="center">
Введите 8-значный код ученика
</Typography>
<CodeInput value={codeValue} onChange={setCodeValue} disabled={loading} />
</Stack>
)}
{mode === 'link' && (
<Stack spacing={2}>
<Typography variant="body2" color="text.secondary">
Сгенерируйте ссылку для регистрации ученика. Каждая ссылка действует 12 часов и может быть использована только 1 раз.
</Typography>
{!linkData ? (
<Button
variant="outlined"
onClick={handleGenerateLink}
disabled={linkLoading}
startIcon={linkLoading ? <CircularProgress size={16} /> : <Iconify icon="solar:link-bold" width={18} />}
>
Сгенерировать ссылку
</Button>
) : (
<Stack spacing={1}>
<TextField
value={linkData.invitation_link}
fullWidth
size="small"
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<Tooltip title={copied ? 'Скопировано!' : 'Скопировать'}>
<IconButton edge="end" onClick={handleCopyLink} size="small">
<Iconify icon={copied ? 'solar:check-circle-bold' : 'solar:copy-bold'} width={18} />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
/>
{expiresText && (
<Typography variant="caption" color="text.secondary">
{expiresText} · только 1 ученик
</Typography>
)}
<Button
size="small"
variant="text"
onClick={handleGenerateLink}
disabled={linkLoading}
startIcon={<Iconify icon="solar:refresh-bold" width={16} />}
>
Новая ссылка
</Button>
</Stack>
)}
</Stack>
)}
{error && <Alert severity="error">{error}</Alert>}
{successMsg && <Alert severity="success">{successMsg}</Alert>}
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={handleClose} disabled={loading || linkLoading}>Закрыть</Button>
{mode !== 'link' && (
<Button
variant="contained"
onClick={handleSend}
disabled={loading || !canSend}
startIcon={loading ? <CircularProgress size={16} /> : null}
>
{loading ? 'Отправка...' : 'Пригласить'}
</Button>
)}
</DialogActions>
</Dialog>
);
}
// ----------------------------------------------------------------------
// 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 <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>;
if (mentors.length === 0) {
return (
<Box sx={{ py: 8, textAlign: 'center' }}>
<Iconify icon="solar:user-bold" width={48} sx={{ color: 'text.disabled', mb: 2 }} />
<Typography variant="body2" color="text.secondary">Нет подключённых менторов</Typography>
</Box>
);
}
return (
<>
<Grid container spacing={2}>
{mentors.map((m) => {
const name = `${m.first_name || ''} ${m.last_name || ''}`.trim() || m.email || '—';
return (
<Grid item key={m.id} xs={12} sm={6} md={4} lg={3}>
<Card
sx={{
p: 3,
textAlign: 'center',
height: '100%',
position: 'relative',
transition: 'box-shadow 0.2s, transform 0.2s',
'&:hover': {
transform: 'translateY(-2px)',
boxShadow: (t) => t.customShadows?.z16 || '0 8px 24px rgba(0,0,0,0.12)',
},
}}
>
<Tooltip title="Удалить ментора">
<IconButton
size="small"
color="error"
onClick={() => setRemoveTarget({ id: m.id, name })}
sx={{ position: 'absolute', top: 8, right: 8 }}
>
<Iconify icon="solar:trash-bin-trash-bold" width={18} />
</IconButton>
</Tooltip>
<Avatar
src={avatarUrl(m.avatar_url)}
sx={{ width: 72, height: 72, mx: 'auto', mb: 2, fontSize: 26, bgcolor: 'secondary.main' }}
>
{initials(m.first_name, m.last_name)}
</Avatar>
<Typography variant="subtitle1" noWrap sx={{ mb: 0.5 }}>{name}</Typography>
<Typography variant="body2" color="text.secondary" noWrap>{m.email || ''}</Typography>
</Card>
</Grid>
);
})}
</Grid>
<RemoveConnectionDialog
open={!!removeTarget}
onClose={() => 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 <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>;
if (invitations.length === 0) return (
<Box sx={{ py: 8, textAlign: 'center' }}>
<Iconify icon="solar:letter-bold" width={48} sx={{ color: 'text.disabled', mb: 2 }} />
<Typography variant="body2" color="text.secondary">Нет входящих приглашений</Typography>
</Box>
);
return (
<Card variant="outlined" sx={{ mb: 3 }}>
<CardHeader title="Приглашения от менторов" titleTypographyProps={{ variant: 'subtitle1' }} />
<Divider />
<CardContent sx={{ pt: 1 }}>
{error && <Alert severity="error" sx={{ mb: 1 }}>{error}</Alert>}
<List disablePadding>
{invitations.map((inv) => {
const m = inv.mentor || {};
return (
<ListItem
key={inv.id}
divider
secondaryAction={
<Stack direction="row" spacing={1}>
<Button size="small" variant="contained" color="success" onClick={() => handle(inv.id, 'confirm')} disabled={processing === inv.id}>
Принять
</Button>
<Button size="small" variant="outlined" color="error" onClick={() => handle(inv.id, 'reject')} disabled={processing === inv.id}>
Отклонить
</Button>
</Stack>
}
>
<ListItemAvatar>
<Avatar>{initials(m.first_name, m.last_name)}</Avatar>
</ListItemAvatar>
<ListItemText
primary={`${m.first_name || ''} ${m.last_name || ''}`.trim() || m.email}
secondary={m.email}
/>
</ListItem>
);
})}
</List>
</CardContent>
</Card>
);
}
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 (
<Dialog open={open} onClose={() => { if (!loading) { setCode(''); setError(null); setSuccessMsg(null); onClose(); } }} maxWidth="xs" fullWidth>
<DialogTitle>Найти ментора</DialogTitle>
<DialogContent>
<Stack spacing={2} sx={{ mt: 1 }}>
<TextField
label="Код ментора"
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())}
disabled={loading}
fullWidth
size="small"
placeholder="XXXXXXXX"
/>
{error && <Alert severity="error">{error}</Alert>}
{successMsg && <Alert severity="success">{successMsg}</Alert>}
</Stack>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={() => { setCode(''); setError(null); setSuccessMsg(null); onClose(); }} disabled={loading}>Закрыть</Button>
<Button variant="contained" onClick={handleSend} disabled={loading || !code.trim()}>
{loading ? 'Отправка...' : 'Отправить заявку'}
</Button>
</DialogActions>
</Dialog>
);
}
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 <Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>;
if (requests.length === 0) return (
<Box sx={{ py: 8, textAlign: 'center' }}>
<Iconify icon="solar:outbox-bold" width={48} sx={{ color: 'text.disabled', mb: 2 }} />
<Typography variant="body2" color="text.secondary">Нет исходящих заявок</Typography>
</Box>
);
return (
<Card variant="outlined">
<CardHeader title="Исходящие заявки к менторам" titleTypographyProps={{ variant: 'subtitle1' }} />
<Divider />
<List disablePadding>
{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 (
<ListItem key={req.id} divider>
<ListItemAvatar>
<Avatar src={avatarUrl(m.avatar_url)} sx={{ bgcolor: 'secondary.main' }}>
{initials(m.first_name, m.last_name)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={name}
secondary={m.email}
/>
<Chip label={statusInfo.label} color={statusInfo.color} size="small" variant="soft" />
</ListItem>
);
})}
</List>
</Card>
);
}
// ----------------------------------------------------------------------
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 (
<DashboardContent>
<CustomBreadcrumbs
heading={isMentor ? 'Ученики' : 'Мои менторы'}
links={[{ name: 'Главная', href: paths.dashboard.root }, { name: isMentor ? 'Ученики' : 'Менторы' }]}
action={
isMentor ? (
<Button variant="contained" startIcon={<Iconify icon="eva:person-add-outline" />} onClick={() => setInviteOpen(true)}>
Пригласить ученика
</Button>
) : (
<Button variant="contained" startIcon={<Iconify icon="eva:search-outline" />} onClick={() => setRequestOpen(true)}>
Найти ментора
</Button>
)
}
sx={{ mb: 3 }}
/>
{isMentor ? (
<>
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 3 }}>
<Tab label="Мои ученики" />
<Tab label="Входящие заявки" />
</Tabs>
{tab === 0 && <MentorStudentList key={refreshKey} onRefresh={refresh} />}
{tab === 1 && <MentorRequests key={refreshKey} onRefresh={refresh} />}
<InviteDialog open={inviteOpen} onClose={() => setInviteOpen(false)} onSuccess={refresh} />
</>
) : (
<>
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 3 }}>
<Tab label="Мои менторы" />
<Tab label="Входящие приглашения" />
<Tab label="Исходящие заявки" />
</Tabs>
{tab === 0 && <ClientMentorList key={refreshKey} />}
{tab === 1 && <ClientInvitations key={refreshKey} onRefresh={refresh} />}
{tab === 2 && <ClientOutgoingRequests key={refreshKey} />}
<SendRequestDialog open={requestOpen} onClose={() => setRequestOpen(false)} onSuccess={refresh} />
</>
)}
</DashboardContent>
);
}