diff --git a/front_minimal/src/app/dashboard/homework/page.jsx b/front_minimal/src/app/dashboard/homework/page.jsx
new file mode 100644
index 0000000..d65445f
--- /dev/null
+++ b/front_minimal/src/app/dashboard/homework/page.jsx
@@ -0,0 +1,11 @@
+import { CONFIG } from 'src/config-global';
+
+import { HomeworkView } from 'src/sections/homework/view';
+
+// ----------------------------------------------------------------------
+
+export const metadata = { title: `Домашние задания | ${CONFIG.site.name}` };
+
+export default function Page() {
+ return ;
+}
diff --git a/front_minimal/src/app/dashboard/materials/page.jsx b/front_minimal/src/app/dashboard/materials/page.jsx
new file mode 100644
index 0000000..bde7e98
--- /dev/null
+++ b/front_minimal/src/app/dashboard/materials/page.jsx
@@ -0,0 +1,11 @@
+import { CONFIG } from 'src/config-global';
+
+import { MaterialsView } from 'src/sections/materials/view';
+
+// ----------------------------------------------------------------------
+
+export const metadata = { title: `Материалы | ${CONFIG.site.name}` };
+
+export default function Page() {
+ return ;
+}
diff --git a/front_minimal/src/app/dashboard/notifications/page.jsx b/front_minimal/src/app/dashboard/notifications/page.jsx
new file mode 100644
index 0000000..616c1ec
--- /dev/null
+++ b/front_minimal/src/app/dashboard/notifications/page.jsx
@@ -0,0 +1,11 @@
+import { CONFIG } from 'src/config-global';
+
+import { NotificationsView } from 'src/sections/notifications/view';
+
+// ----------------------------------------------------------------------
+
+export const metadata = { title: `Уведомления | ${CONFIG.site.name}` };
+
+export default function Page() {
+ return ;
+}
diff --git a/front_minimal/src/app/dashboard/students/page.jsx b/front_minimal/src/app/dashboard/students/page.jsx
new file mode 100644
index 0000000..2207113
--- /dev/null
+++ b/front_minimal/src/app/dashboard/students/page.jsx
@@ -0,0 +1,11 @@
+import { CONFIG } from 'src/config-global';
+
+import { StudentsView } from 'src/sections/students/view';
+
+// ----------------------------------------------------------------------
+
+export const metadata = { title: `Ученики | ${CONFIG.site.name}` };
+
+export default function Page() {
+ return ;
+}
diff --git a/front_minimal/src/layouts/components/notifications-drawer/index.jsx b/front_minimal/src/layouts/components/notifications-drawer/index.jsx
index d67efb2..40e2fed 100644
--- a/front_minimal/src/layouts/components/notifications-drawer/index.jsx
+++ b/front_minimal/src/layouts/components/notifications-drawer/index.jsx
@@ -1,116 +1,122 @@
'use client';
import { m } from 'framer-motion';
-import { useState, useCallback } from 'react';
+import { useState, useEffect, useCallback } from 'react';
-import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';
+import Tab from '@mui/material/Tab';
+import List from '@mui/material/List';
import Stack from '@mui/material/Stack';
import Badge from '@mui/material/Badge';
import Drawer from '@mui/material/Drawer';
import Button from '@mui/material/Button';
+import Divider from '@mui/material/Divider';
import SvgIcon from '@mui/material/SvgIcon';
import Tooltip from '@mui/material/Tooltip';
+import ListItem from '@mui/material/ListItem';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
+import ListItemText from '@mui/material/ListItemText';
+import CircularProgress from '@mui/material/CircularProgress';
+
+import { paths } from 'src/routes/paths';
+import { useRouter } from 'src/routes/hooks';
import { useBoolean } from 'src/hooks/use-boolean';
+import {
+ markAsRead,
+ markAllAsRead,
+ getNotifications,
+} from 'src/utils/notifications-api';
+
import { Label } from 'src/components/label';
import { Iconify } from 'src/components/iconify';
import { varHover } from 'src/components/animate';
import { Scrollbar } from 'src/components/scrollbar';
import { CustomTabs } from 'src/components/custom-tabs';
-import { NotificationItem } from './notification-item';
+// ----------------------------------------------------------------------
+
+function formatDate(s) {
+ if (!s) return '';
+ const d = new Date(s);
+ return Number.isNaN(d.getTime())
+ ? ''
+ : d.toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
+}
+
+function notifIcon(n) {
+ const t = n.type || n.notification_type || '';
+ if (t === 'success') return 'eva:checkmark-circle-2-outline';
+ if (t === 'warning') return 'eva:alert-triangle-outline';
+ if (t === 'error') return 'eva:close-circle-outline';
+ return 'eva:bell-outline';
+}
+
+function notifColor(n) {
+ const t = n.type || n.notification_type || '';
+ if (t === 'success') return 'success.main';
+ if (t === 'warning') return 'warning.main';
+ if (t === 'error') return 'error.main';
+ return 'info.main';
+}
// ----------------------------------------------------------------------
-const TABS = [
- { value: 'all', label: 'All', count: 22 },
- { value: 'unread', label: 'Unread', count: 12 },
- { value: 'archived', label: 'Archived', count: 10 },
-];
-
-// ----------------------------------------------------------------------
-
-export function NotificationsDrawer({ data = [], sx, ...other }) {
+export function NotificationsDrawer({ sx, ...other }) {
const drawer = useBoolean();
+ const router = useRouter();
+ const [currentTab, setCurrentTab] = useState('unread');
+ const [notifications, setNotifications] = useState([]);
+ const [loading, setLoading] = useState(false);
- const [currentTab, setCurrentTab] = useState('all');
-
- const handleChangeTab = useCallback((event, newValue) => {
- setCurrentTab(newValue);
+ const load = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await getNotifications({ page_size: 50 });
+ setNotifications(res.results);
+ } catch {
+ // silently fail
+ } finally {
+ setLoading(false);
+ }
}, []);
- const [notifications, setNotifications] = useState(data);
+ // Load when drawer opens
+ useEffect(() => {
+ if (drawer.value) load();
+ }, [drawer.value, load]);
- const totalUnRead = notifications.filter((item) => item.isUnRead === true).length;
+ const unreadCount = notifications.filter((n) => !n.is_read).length;
- const handleMarkAllAsRead = () => {
- setNotifications(notifications.map((notification) => ({ ...notification, isUnRead: false })));
+ const filtered =
+ currentTab === 'unread'
+ ? notifications.filter((n) => !n.is_read)
+ : notifications;
+
+ const handleMarkAllAsRead = async () => {
+ try {
+ await markAllAsRead();
+ setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
+ } catch {
+ // ignore
+ }
};
- const renderHead = (
-
-
- Notifications
-
+ const handleMarkOne = async (id) => {
+ try {
+ await markAsRead(id);
+ setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, is_read: true } : n));
+ } catch {
+ // ignore
+ }
+ };
- {!!totalUnRead && (
-
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- );
-
- const renderTabs = (
-
- {TABS.map((tab) => (
-
- {tab.count}
-
- }
- />
- ))}
-
- );
-
- const renderList = (
-
-
- {notifications?.map((notification) => (
-
-
-
- ))}
-
-
- );
+ const TABS = [
+ { value: 'unread', label: 'Непрочитанные', count: unreadCount },
+ { value: 'all', label: 'Все', count: notifications.length },
+ ];
return (
<>
@@ -123,9 +129,8 @@ export function NotificationsDrawer({ data = [], sx, ...other }) {
sx={sx}
{...other}
>
-
+
- {/* https://icon-sets.iconify.design/solar/bell-bing-bold-duotone/ */}
- {renderHead}
+ {/* Head */}
+
+
+ Уведомления
+
+ {unreadCount > 0 && (
+
+
+
+
+
+ )}
+
+
+
+
- {renderTabs}
+ {/* Tabs */}
+ setCurrentTab(v)}>
+ {TABS.map((tab) => (
+
+ {tab.count}
+
+ }
+ />
+ ))}
+
- {renderList}
+ {/* List */}
+
+ {loading ? (
+
+
+
+ ) : filtered.length === 0 ? (
+
+
+
+ {currentTab === 'unread' ? 'Нет непрочитанных' : 'Нет уведомлений'}
+
+
+ ) : (
+
+ {filtered.map((n, idx) => (
+
+ !n.is_read && handleMarkOne(n.id)}
+ >
+
+
+
+
+
+ {n.title || 'Уведомление'}
+
+ {!n.is_read && (
+
+ )}
+
+ }
+ secondary={
+ <>
+
+ {n.message}
+
+
+ {formatDate(n.created_at)}
+
+ >
+ }
+ />
+
+ {idx < filtered.length - 1 && }
+
+ ))}
+
+ )}
+
-
-
+ ))}
+
+ );
+}
+
+// ----------------------------------------------------------------------
+// Client view — own submission
+
+function ClientSubmissionSection({ homework, childId, onOpenSubmit, onReload }) {
+ const [submission, setSubmission] = useState(undefined); // undefined = loading
+ const [deleting, setDeleting] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ getMySubmission(homework.id, childId ? { child_id: childId } : undefined)
+ .then(setSubmission)
+ .catch(() => setSubmission(null));
+ }, [homework.id, childId]);
+
+ const handleDelete = async () => {
+ try {
+ setDeleting(true);
+ await deleteSubmission(submission.id);
+ setSubmission(null);
+ onReload();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка');
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ if (submission === undefined) return ;
+
+ if (!submission) {
+ return (
+
+
+ Вы ещё не сдали задание.
+
+ {homework.status === 'published' && !childId && (
+
+ Сдать ДЗ
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+
+ {formatDateTime(submission.submitted_at)}
+
+
+
+ {submission.content && (
+
+
+ {submission.content}
+
+
+ )}
+
+ f.file_type === 'submission')}
+ label="Решение"
+ />
+
+ {submission.score != null && (
+
+ Оценка
+
+
+ {submission.score} / {homework.max_score || 5}
+
+
+ )}
+
+ {submission.feedback && (
+
+
+ Комментарий ментора
+
+ {submission.feedback_html ? (
+
+ ) : (
+
+
+ {submission.feedback}
+
+
+ )}
+
+ )}
+
+ {error && {error}}
+
+ {submission.status !== 'checked' && !childId && (
+
+
+ {deleting ? 'Удаление...' : 'Удалить и переотправить'}
+
+
+ )}
+
+ );
+}
+
+// ----------------------------------------------------------------------
+// Mentor submission item
+
+function MentorSubmissionItem({ submission, homework, onReload }) {
+ const [score, setScore] = useState(submission.score != null ? submission.score : '');
+ const [feedback, setFeedback] = useState('');
+ const [returnFeedback, setReturnFeedback] = useState('');
+ const [showReturnForm, setShowReturnForm] = useState(false);
+ const [grading, setGrading] = useState(false);
+ const [returning, setReturning] = useState(false);
+ const [aiChecking, setAiChecking] = useState(false);
+ const [error, setError] = useState(null);
+
+ const {student} = submission;
+
+ const handleGrade = async () => {
+ if (!score && score !== 0) {
+ setError('Укажите оценку');
+ return;
+ }
+ try {
+ setGrading(true);
+ setError(null);
+ await gradeSubmission(submission.id, { score: Number(score), feedback: feedback.trim() });
+ onReload();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка');
+ } finally {
+ setGrading(false);
+ }
+ };
+
+ const handleReturn = async () => {
+ if (!returnFeedback.trim()) {
+ setError('Укажите причину возврата');
+ return;
+ }
+ try {
+ setReturning(true);
+ setError(null);
+ await returnSubmissionForRevision(submission.id, returnFeedback.trim());
+ onReload();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка');
+ } finally {
+ setReturning(false);
+ }
+ };
+
+ const handleAiCheck = async () => {
+ try {
+ setAiChecking(true);
+ setError(null);
+ const result = await checkSubmissionWithAi(submission.id);
+ if (result.ai_score != null) setScore(result.ai_score);
+ if (result.ai_feedback) setFeedback(result.ai_feedback);
+ onReload();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка AI-проверки');
+ } finally {
+ setAiChecking(false);
+ }
+ };
+
+ const isChecked = submission.status === 'checked';
+ const isReturned = submission.status === 'returned';
+
+ return (
+
+
+
+ {student?.first_name} {student?.last_name}
+
+
+
+
+ {submission.content && (
+
+
+ {submission.content}
+
+
+ )}
+
+ f.file_type === 'submission')}
+ label="Решение"
+ />
+
+ {/* AI draft info */}
+ {submission.ai_score != null && !isChecked && (
+
+
+ Черновик от ИИ: оценка {submission.ai_score}/5
+
+ {submission.ai_feedback && (
+
+ {submission.ai_feedback}
+
+ )}
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Grade form */}
+ {!isChecked && !showReturnForm && (
+
+
+
+ Оценка:
+
+ setScore(v ?? '')}
+ />
+
+ setFeedback(e.target.value)}
+ size="small"
+ fullWidth
+ />
+
+ : null}
+ >
+ {grading ? 'Сохранение...' : 'Выставить оценку'}
+
+ setShowReturnForm(true)}
+ disabled={grading || aiChecking}
+ >
+ Вернуть на доработку
+
+
+ ) : (
+
+ )
+ }
+ >
+ {aiChecking ? 'Проверка...' : 'ИИ-проверка'}
+
+
+
+ )}
+
+ {/* Return form */}
+ {showReturnForm && (
+
+ setReturnFeedback(e.target.value)}
+ size="small"
+ fullWidth
+ />
+
+
+ {returning ? 'Отправка...' : 'Вернуть'}
+
+ setShowReturnForm(false)}
+ disabled={returning}
+ >
+ Отмена
+
+
+
+ )}
+
+ );
+}
+
+// ----------------------------------------------------------------------
+// Main drawer
+
+export function HomeworkDetailsDrawer({ open, homework, userRole, childId, onClose, onSuccess, onOpenSubmit, onOpenEdit }) {
+ const [submissions, setSubmissions] = useState([]);
+ const [subsLoading, setSubsLoading] = useState(false);
+ const [tab, setTab] = useState(0);
+ const isMentor = userRole === 'mentor';
+
+ const loadSubmissions = () => {
+ if (!homework || !isMentor) return;
+ setSubsLoading(true);
+ getHomeworkSubmissions(homework.id)
+ .then(setSubmissions)
+ .catch(() => setSubmissions([]))
+ .finally(() => setSubsLoading(false));
+ };
+
+ useEffect(() => {
+ if (open && homework && isMentor) {
+ loadSubmissions();
+ setTab(0);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open, homework?.id, isMentor]);
+
+ if (!homework) return null;
+
+ const assignmentFiles = (homework.files ?? []).filter((f) => f.file_type === 'assignment');
+
+ return (
+
+ {/* Header */}
+
+
+ {homework.title}
+ {homework.deadline && (
+
+ Дедлайн: {formatDateTime(homework.deadline)}
+ {homework.is_overdue && ' • Просрочено'}
+
+ )}
+
+
+
+
+
+
+ {/* Tabs for mentor */}
+ {isMentor && (
+ setTab(v)} sx={{ px: 3, borderBottom: '1px solid', borderColor: 'divider' }}>
+
+
+
+ )}
+
+
+ {/* Assignment tab / single view */}
+ {(!isMentor || tab === 0) && (
+
+ {homework.description && (
+
+
+ Описание
+
+
+ {homework.description}
+
+
+ )}
+
+ {(homework.attachment || assignmentFiles.length > 0 || homework.attachment_url) && (
+
+
+ Файлы задания
+
+
+ {homework.attachment_url && (
+ }
+ sx={{ mt: 1, justifyContent: 'flex-start' }}
+ >
+ Ссылка на материал
+
+ )}
+
+ )}
+
+ {isMentor && homework.fill_later && (
+ { onClose(); onOpenEdit(homework); }}>
+ Заполнить
+
+ }
+ >
+ Задание ожидает заполнения
+
+ )}
+
+ {isMentor && (
+
+
+ Статус: {homework.status === 'published' ? 'Опубликовано' : homework.status === 'draft' ? 'Черновик' : 'Архив'}
+
+ {homework.average_score > 0 && (
+
+ Средняя оценка: {homework.average_score.toFixed(1)} / 5
+
+ )}
+
+ )}
+
+ {/* Client submission section */}
+ {(userRole === 'client' || userRole === 'parent') && homework.status === 'published' && (
+ <>
+
+
+
+ Ваше решение
+
+
+
+ >
+ )}
+
+ )}
+
+ {/* Submissions tab for mentor */}
+ {isMentor && tab === 1 && (
+
+ {subsLoading ? (
+
+
+
+ ) : submissions.length === 0 ? (
+
+ Нет решений
+
+ ) : (
+ submissions.map((sub) => (
+ {
+ loadSubmissions();
+ onSuccess();
+ }}
+ />
+ ))
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/front_minimal/src/sections/homework/homework-edit-drawer.jsx b/front_minimal/src/sections/homework/homework-edit-drawer.jsx
new file mode 100644
index 0000000..1b3d081
--- /dev/null
+++ b/front_minimal/src/sections/homework/homework-edit-drawer.jsx
@@ -0,0 +1,254 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+
+import Box from '@mui/material/Box';
+import Chip from '@mui/material/Chip';
+import Stack from '@mui/material/Stack';
+import Alert from '@mui/material/Alert';
+import Button from '@mui/material/Button';
+import Drawer from '@mui/material/Drawer';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import IconButton from '@mui/material/IconButton';
+import CircularProgress from '@mui/material/CircularProgress';
+import { MobileDateTimePicker } from '@mui/x-date-pickers/MobileDateTimePicker';
+
+import {
+ updateHomework,
+ publishHomework,
+ uploadHomeworkFile,
+ deleteHomeworkFile,
+} from 'src/utils/homework-api';
+
+import { Iconify } from 'src/components/iconify';
+
+// ----------------------------------------------------------------------
+
+export function HomeworkEditDrawer({ open, homework, onClose, onSuccess }) {
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [deadline, setDeadline] = useState(null);
+ const [existingFiles, setExistingFiles] = useState([]);
+ const [uploadingCount, setUploadingCount] = useState(0);
+ const [saving, setSaving] = useState(false);
+ const [publishing, setPublishing] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!open || !homework) return;
+ setTitle(homework.title || '');
+ setDescription(homework.description || '');
+ setDeadline(homework.deadline ? new Date(homework.deadline) : null);
+ setExistingFiles(homework.files?.filter((f) => f.file_type === 'assignment') || []);
+ setError(null);
+ }, [open, homework]);
+
+ const handleFileChange = (e) => {
+ const newFiles = Array.from(e.target.files || []);
+ if (!newFiles.length || !homework) return;
+ e.target.value = '';
+
+ const validFiles = newFiles.filter((file) => {
+ if (file.size > 10 * 1024 * 1024) {
+ setError(`Файл "${file.name}" больше 10 МБ`);
+ return false;
+ }
+ return true;
+ });
+
+ if (!validFiles.length) return;
+
+ setUploadingCount((c) => c + validFiles.length);
+
+ Promise.all(
+ validFiles.map((file) =>
+ uploadHomeworkFile(homework.id, file)
+ .then((uploaded) => {
+ setExistingFiles((prev) => [...prev, uploaded]);
+ })
+ .catch((err) => {
+ setError(err?.response?.data?.detail || err?.message || 'Ошибка загрузки файла');
+ })
+ .finally(() => {
+ setUploadingCount((c) => c - 1);
+ })
+ )
+ );
+ };
+
+ const handleRemoveFile = async (fileId) => {
+ try {
+ await deleteHomeworkFile(fileId);
+ setExistingFiles((prev) => prev.filter((f) => f.id !== fileId));
+ } catch (err) {
+ setError(err?.response?.data?.detail || err?.message || 'Ошибка удаления файла');
+ }
+ };
+
+ const handleSave = async () => {
+ if (!homework) return;
+ try {
+ setError(null);
+ setSaving(true);
+ await updateHomework(homework.id, {
+ title: title.trim() || homework.title,
+ description: description.trim(),
+ deadline: deadline ? deadline.toISOString() : null,
+ });
+ onSuccess();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка сохранения');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const handlePublish = async () => {
+ if (!homework) return;
+ if (!title.trim()) {
+ setError('Укажите название задания');
+ return;
+ }
+ if (!description.trim()) {
+ setError('Укажите текст задания');
+ return;
+ }
+ try {
+ setError(null);
+ setPublishing(true);
+ await updateHomework(homework.id, {
+ title: title.trim(),
+ description: description.trim(),
+ deadline: deadline ? deadline.toISOString() : null,
+ });
+ await publishHomework(homework.id);
+ onSuccess();
+ onClose();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка публикации');
+ } finally {
+ setPublishing(false);
+ }
+ };
+
+ const isLoading = saving || publishing || uploadingCount > 0;
+
+ return (
+
+
+ Заполнить домашнее задание
+
+
+
+
+
+
+ setTitle(e.target.value)}
+ disabled={isLoading}
+ fullWidth
+ />
+
+ setDescription(e.target.value)}
+ disabled={isLoading}
+ placeholder="Опишите задание, шаги, ссылки..."
+ fullWidth
+ />
+
+
+
+
+
+ Файлы к заданию
+
+
+
+ 0 ? (
+
+ ) : (
+
+ )
+ }
+ fullWidth
+ disabled={isLoading}
+ sx={{ py: 1.5, border: '2px dashed', '&:hover': { border: '2px dashed' } }}
+ >
+ {uploadingCount > 0 ? `Загрузка ${uploadingCount}...` : 'Загрузить файлы'}
+
+
+ {existingFiles.length > 0 && (
+
+ {existingFiles.map((file) => (
+ handleRemoveFile(file.id)}
+ icon={}
+ />
+ ))}
+
+ )}
+
+
+ {error && {error}}
+
+
+ : null}
+ >
+ {publishing ? 'Публикация...' : 'Опубликовать ДЗ'}
+
+ : null}
+ >
+ {saving ? 'Сохранение...' : 'Сохранить черновик'}
+
+
+
+
+ );
+}
diff --git a/front_minimal/src/sections/homework/homework-submit-drawer.jsx b/front_minimal/src/sections/homework/homework-submit-drawer.jsx
new file mode 100644
index 0000000..3286a66
--- /dev/null
+++ b/front_minimal/src/sections/homework/homework-submit-drawer.jsx
@@ -0,0 +1,265 @@
+'use client';
+
+import { useMemo, useState, useEffect } from 'react';
+
+import Box from '@mui/material/Box';
+import Stack from '@mui/material/Stack';
+import Alert from '@mui/material/Alert';
+import Button from '@mui/material/Button';
+import Drawer from '@mui/material/Drawer';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import IconButton from '@mui/material/IconButton';
+import LinearProgress from '@mui/material/LinearProgress';
+
+import { submitHomework, getHomeworkById, validateHomeworkFiles } from 'src/utils/homework-api';
+
+import { CONFIG } from 'src/config-global';
+
+import { Iconify } from 'src/components/iconify';
+
+// ----------------------------------------------------------------------
+
+function fileUrl(href) {
+ if (!href) return '';
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
+ const base = CONFIG.site.serverUrl?.replace('/api', '') || '';
+ return base + (href.startsWith('/') ? href : `/${href}`);
+}
+
+// ----------------------------------------------------------------------
+
+export function HomeworkSubmitDrawer({ open, homeworkId, onClose, onSuccess }) {
+ const [homework, setHomework] = useState(null);
+ const [content, setContent] = useState('');
+ const [files, setFiles] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [uploadProgress, setUploadProgress] = useState(null);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!open) {
+ setContent('');
+ setFiles([]);
+ setUploadProgress(null);
+ setError(null);
+ setHomework(null);
+ } else if (homeworkId) {
+ getHomeworkById(homeworkId)
+ .then(setHomework)
+ .catch(() => setHomework(null));
+ }
+ }, [open, homeworkId]);
+
+ const assignmentFiles = useMemo(() => {
+ if (!homework) return [];
+ const list = [];
+ if (homework.attachment) list.push({ label: 'Файл задания', url: fileUrl(homework.attachment) });
+ (homework.files ?? [])
+ .filter((f) => f.file_type === 'assignment')
+ .forEach((f) => {
+ if (f.file) list.push({ label: f.filename || 'Файл', url: fileUrl(f.file) });
+ });
+ if (homework.attachment_url?.trim())
+ list.push({ label: 'Ссылка на материал', url: homework.attachment_url.trim() });
+ return list;
+ }, [homework]);
+
+ const handleFileChange = (e) => {
+ const list = Array.from(e.target.files || []);
+ const combined = [...files, ...list];
+ const { valid, error: validationError } = validateHomeworkFiles(combined);
+ if (!valid) {
+ setError(validationError ?? 'Ошибка файлов');
+ return;
+ }
+ setFiles(combined);
+ setError(null);
+ e.target.value = '';
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!content.trim() && files.length === 0) {
+ setError('Укажите текст или прикрепите файлы');
+ return;
+ }
+ const { valid, error: validationError } = validateHomeworkFiles(files);
+ if (!valid) {
+ setError(validationError ?? 'Ошибка файлов');
+ return;
+ }
+ try {
+ setLoading(true);
+ setError(null);
+ setUploadProgress(0);
+ await submitHomework(homeworkId, { content: content.trim(), files }, (p) =>
+ setUploadProgress(p)
+ );
+ await onSuccess();
+ onClose();
+ } catch (err) {
+ setError(err?.response?.data?.detail || err?.message || 'Ошибка отправки');
+ setUploadProgress(null);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Отправить решение
+
+
+
+
+
+
+ {assignmentFiles.length > 0 && (
+
+
+ Файлы задания (от ментора)
+
+
+ {assignmentFiles.map(({ label, url }, i) => (
+ }
+ sx={{ justifyContent: 'flex-start', textAlign: 'left' }}
+ >
+ {label}
+
+ ))}
+
+
+ )}
+
+ setContent(e.target.value)}
+ disabled={loading}
+ placeholder="Введите текст решения..."
+ fullWidth
+ />
+
+
+
+ Файлы (до 50 МБ, не более 10 шт.)
+
+
+
+ }
+ fullWidth
+ disabled={loading}
+ sx={{
+ py: 2,
+ border: '2px dashed',
+ '&:hover': { border: '2px dashed' },
+ }}
+ >
+ Прикрепить файлы
+
+
+ {files.length > 0 && (
+
+ {files.map((f, i) => (
+
+
+ {f.name}
+
+ setFiles((p) => p.filter((_, j) => j !== i))}
+ disabled={loading}
+ color="error"
+ >
+
+
+
+ ))}
+
+ )}
+
+ {uploadProgress != null && (
+
+
+
+ Загрузка на сервер
+
+
+ {uploadProgress}%
+
+
+
+
+ )}
+
+
+ {error && {error}}
+
+
+
+ Отмена
+
+ : null}
+ >
+ {loading ? (uploadProgress != null ? `${uploadProgress}%` : 'Отправка...') : 'Отправить'}
+
+
+
+
+ );
+}
diff --git a/front_minimal/src/sections/homework/view/homework-view.jsx b/front_minimal/src/sections/homework/view/homework-view.jsx
new file mode 100644
index 0000000..1a522a5
--- /dev/null
+++ b/front_minimal/src/sections/homework/view/homework-view.jsx
@@ -0,0 +1,406 @@
+'use client';
+
+import { useMemo, useState, useEffect, useCallback } from 'react';
+
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import Chip from '@mui/material/Chip';
+import Stack from '@mui/material/Stack';
+import Alert from '@mui/material/Alert';
+import Button from '@mui/material/Button';
+import Typography from '@mui/material/Typography';
+import CardContent from '@mui/material/CardContent';
+import CardActionArea from '@mui/material/CardActionArea';
+import CircularProgress from '@mui/material/CircularProgress';
+
+import { paths } from 'src/routes/paths';
+
+import { getHomework, getHomeworkById, getHomeworkStatus } from 'src/utils/homework-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';
+
+import { HomeworkEditDrawer } from '../homework-edit-drawer';
+import { HomeworkSubmitDrawer } from '../homework-submit-drawer';
+import { HomeworkDetailsDrawer } from '../homework-details-drawer';
+
+// ----------------------------------------------------------------------
+
+function formatDate(s) {
+ if (!s) return null;
+ const d = new Date(s);
+ return Number.isNaN(d.getTime())
+ ? null
+ : d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
+}
+
+function formatTime(s) {
+ if (!s) return null;
+ const d = new Date(s);
+ return Number.isNaN(d.getTime())
+ ? null
+ : d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
+}
+
+// ----------------------------------------------------------------------
+
+function HomeworkCard({ hw, userRole, onView, onSubmit, onEdit }) {
+ const status = getHomeworkStatus(hw);
+ const isFillLater = hw.fill_later === true;
+
+ const statusConfig = {
+ pending: { label: hw.is_overdue ? 'Просрочено' : 'Ожидает сдачи', color: hw.is_overdue ? 'error' : 'default' },
+ submitted: { label: 'На проверке', color: 'info' },
+ returned: { label: 'На доработке', color: 'warning' },
+ reviewed: { label: 'Проверено', color: 'success' },
+ };
+ const statusInfo = statusConfig[status] || statusConfig.pending;
+
+ return (
+
+
+
+
+
+ {hw.title}
+
+
+
+
+ {userRole === 'client' && (
+
+
+
+ {hw.mentor?.first_name} {hw.mentor?.last_name}
+
+
+ )}
+
+ {userRole === 'mentor' && hw.total_submissions > 0 && (
+
+ Решений: {hw.total_submissions}
+ {hw.checked_submissions > 0 && ` • Проверено: ${hw.checked_submissions}`}
+
+ )}
+
+ {hw.deadline && (
+
+
+
+ {formatDate(hw.deadline)} {formatTime(hw.deadline)}
+
+
+ )}
+
+ {hw.student_score?.score != null && (
+
+ Оценка: {hw.student_score.score} / {hw.max_score || 5}
+
+ )}
+
+ {hw.ai_draft_count > 0 && userRole === 'mentor' && (
+
+ )}
+
+
+
+ {((userRole === 'client' && status === 'pending' && !hw.is_overdue && hw.status === 'published') ||
+ (userRole === 'mentor' && isFillLater)) && (
+
+ {userRole === 'client' && (
+ { e.stopPropagation(); onSubmit(); }}
+ >
+ Сдать ДЗ
+
+ )}
+ {userRole === 'mentor' && isFillLater && (
+ { e.stopPropagation(); onEdit(); }}
+ startIcon={}
+ >
+ Заполнить задание
+
+ )}
+
+ )}
+
+ );
+}
+
+// ----------------------------------------------------------------------
+
+function HomeworkColumn({ title, items, userRole, onView, onSubmit, onEdit, emptyText }) {
+ return (
+
+
+ {title}
+ {items.length > 0 && (
+
+ ({items.length})
+
+ )}
+
+
+ {items.length === 0 ? (
+
+ {emptyText || 'Нет заданий'}
+
+ ) : (
+ items.map((hw) => (
+ onView(hw)}
+ onSubmit={() => onSubmit(hw)}
+ onEdit={() => onEdit(hw)}
+ />
+ ))
+ )}
+
+
+ );
+}
+
+// ----------------------------------------------------------------------
+
+export function HomeworkView() {
+ const { user } = useAuthContext();
+ const userRole = user?.role ?? '';
+
+ const [homework, setHomework] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Drawers
+ const [selectedHw, setSelectedHw] = useState(null);
+ const [detailsOpen, setDetailsOpen] = useState(false);
+ const [submitOpen, setSubmitOpen] = useState(false);
+ const [submitHwId, setSubmitHwId] = useState(null);
+ const [editOpen, setEditOpen] = useState(false);
+ const [editHw, setEditHw] = useState(null);
+
+ const loadHomework = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await getHomework({ page_size: 1000 });
+ setHomework(res.results);
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка загрузки');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadHomework();
+ }, [loadHomework]);
+
+ const handleViewDetails = useCallback(async (hw) => {
+ try {
+ const full = await getHomeworkById(hw.id);
+ setSelectedHw(full);
+ } catch {
+ setSelectedHw(hw);
+ }
+ setDetailsOpen(true);
+ }, []);
+
+ const handleOpenSubmit = useCallback((hw) => {
+ setSubmitHwId(hw.id);
+ setSubmitOpen(true);
+ }, []);
+
+ const handleOpenEdit = useCallback((hw) => {
+ setEditHw(hw);
+ setEditOpen(true);
+ }, []);
+
+ // Categorize
+ const pending = useMemo(
+ () => homework.filter((hw) => getHomeworkStatus(hw) === 'pending' && hw.status === 'published'),
+ [homework]
+ );
+ const submitted = useMemo(
+ () => homework.filter((hw) => getHomeworkStatus(hw) === 'submitted'),
+ [homework]
+ );
+ const returned = useMemo(
+ () => homework.filter((hw) => getHomeworkStatus(hw) === 'returned'),
+ [homework]
+ );
+ const reviewed = useMemo(
+ () => homework.filter((hw) => getHomeworkStatus(hw) === 'reviewed'),
+ [homework]
+ );
+ const fillLater = useMemo(
+ () => (userRole === 'mentor' ? homework.filter((hw) => hw.fill_later === true) : []),
+ [homework, userRole]
+ );
+ const aiDraft = useMemo(
+ () => (userRole === 'mentor' ? homework.filter((hw) => (hw.ai_draft_count ?? 0) > 0) : []),
+ [homework, userRole]
+ );
+
+ return (
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+
+
+ ) : homework.length === 0 ? (
+
+ Нет домашних заданий
+
+ ) : (
+
+ {userRole === 'mentor' && fillLater.length > 0 && (
+
+ )}
+
+
+
+
+
+ {userRole === 'mentor' && aiDraft.length > 0 && (
+
+ )}
+
+
+
+
+
+ )}
+
+ {/* Details Drawer */}
+ { setDetailsOpen(false); setSelectedHw(null); }}
+ onSuccess={loadHomework}
+ onOpenSubmit={() => {
+ setSubmitHwId(selectedHw?.id);
+ setSubmitOpen(true);
+ }}
+ onOpenEdit={(hw) => {
+ setEditHw(hw);
+ setEditOpen(true);
+ }}
+ />
+
+ {/* Submit Drawer */}
+ { setSubmitOpen(false); setSubmitHwId(null); }}
+ onSuccess={loadHomework}
+ />
+
+ {/* Edit Draft Drawer */}
+ { setEditOpen(false); setEditHw(null); }}
+ onSuccess={loadHomework}
+ />
+
+ );
+}
diff --git a/front_minimal/src/sections/homework/view/index.js b/front_minimal/src/sections/homework/view/index.js
new file mode 100644
index 0000000..e363f6b
--- /dev/null
+++ b/front_minimal/src/sections/homework/view/index.js
@@ -0,0 +1 @@
+export * from './homework-view';
diff --git a/front_minimal/src/sections/materials/view/index.js b/front_minimal/src/sections/materials/view/index.js
new file mode 100644
index 0000000..501cefa
--- /dev/null
+++ b/front_minimal/src/sections/materials/view/index.js
@@ -0,0 +1 @@
+export * from './materials-view';
diff --git a/front_minimal/src/sections/materials/view/materials-view.jsx b/front_minimal/src/sections/materials/view/materials-view.jsx
new file mode 100644
index 0000000..b8fea10
--- /dev/null
+++ b/front_minimal/src/sections/materials/view/materials-view.jsx
@@ -0,0 +1,411 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+
+import Box from '@mui/material/Box';
+import Card from '@mui/material/Card';
+import Grid from '@mui/material/Grid';
+import Stack from '@mui/material/Stack';
+import Alert from '@mui/material/Alert';
+import Button from '@mui/material/Button';
+import Dialog from '@mui/material/Dialog';
+import Tooltip from '@mui/material/Tooltip';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import IconButton from '@mui/material/IconButton';
+import DialogTitle from '@mui/material/DialogTitle';
+import CardContent from '@mui/material/CardContent';
+import DialogContent from '@mui/material/DialogContent';
+import DialogActions from '@mui/material/DialogActions';
+import InputAdornment from '@mui/material/InputAdornment';
+import CircularProgress from '@mui/material/CircularProgress';
+
+import { paths } from 'src/routes/paths';
+
+import {
+ getMaterials,
+ createMaterial,
+ deleteMaterial,
+ getMaterialTypeIcon,
+} from 'src/utils/materials-api';
+
+import { CONFIG } from 'src/config-global';
+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';
+
+// ----------------------------------------------------------------------
+
+function fileUrl(href) {
+ if (!href) return '';
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
+ const base = CONFIG.site.serverUrl?.replace('/api', '') || '';
+ return base + (href.startsWith('/') ? href : `/${href}`);
+}
+
+function formatSize(bytes) {
+ if (!bytes) return '';
+ if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)} КБ`;
+ return `${bytes} Б`;
+}
+
+// ----------------------------------------------------------------------
+
+function MaterialCard({ material, onDelete, isMentor }) {
+ const icon = getMaterialTypeIcon(material);
+ const url = fileUrl(material.file_url || material.file || '');
+ const isImage =
+ material.material_type === 'image' ||
+ /\.(jpe?g|png|gif|webp)$/i.test(material.file_name || material.file || '');
+
+ return (
+
+ {isImage && url ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {material.title}
+
+ {material.description && (
+
+ {material.description}
+
+ )}
+ {(material.file_size || material.category_name) && (
+
+ {material.category_name && (
+
+ {material.category_name}
+
+ )}
+ {material.file_size && (
+
+ {formatSize(material.file_size)}
+
+ )}
+
+ )}
+
+
+
+ {url && (
+
+
+
+
+
+ )}
+ {isMentor && (
+
+ onDelete(material)}>
+
+
+
+ )}
+
+
+ );
+}
+
+// ----------------------------------------------------------------------
+
+function UploadDialog({ open, onClose, onSuccess }) {
+ const [title, setTitle] = useState('');
+ const [description, setDescription] = useState('');
+ const [file, setFile] = useState(null);
+ const [isPublic, setIsPublic] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const reset = () => {
+ setTitle('');
+ setDescription('');
+ setFile(null);
+ setIsPublic(false);
+ setError(null);
+ };
+
+ const handleClose = () => {
+ if (!loading) {
+ reset();
+ onClose();
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!title.trim()) {
+ setError('Укажите название');
+ return;
+ }
+ if (!file) {
+ setError('Выберите файл');
+ return;
+ }
+ try {
+ setLoading(true);
+ setError(null);
+ await createMaterial({ title: title.trim(), description: description.trim(), file, is_public: isPublic });
+ await onSuccess();
+ handleClose();
+ } catch (err) {
+ setError(err?.response?.data?.detail || err?.message || 'Ошибка загрузки');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
+
+// ----------------------------------------------------------------------
+
+export function MaterialsView() {
+ const { user } = useAuthContext();
+ const isMentor = user?.role === 'mentor';
+
+ const [materials, setMaterials] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [search, setSearch] = useState('');
+ const [uploadOpen, setUploadOpen] = useState(false);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleting, setDeleting] = useState(false);
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await getMaterials({ page_size: 200 });
+ setMaterials(res.results);
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка загрузки');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const handleDelete = async () => {
+ if (!deleteTarget) return;
+ try {
+ setDeleting(true);
+ await deleteMaterial(deleteTarget.id);
+ setMaterials((prev) => prev.filter((m) => m.id !== deleteTarget.id));
+ setDeleteTarget(null);
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка удаления');
+ } finally {
+ setDeleting(false);
+ }
+ };
+
+ const filtered = materials.filter((m) => {
+ if (!search.trim()) return true;
+ const q = search.toLowerCase();
+ return (
+ (m.title || '').toLowerCase().includes(q) ||
+ (m.description || '').toLowerCase().includes(q) ||
+ (m.category_name || '').toLowerCase().includes(q)
+ );
+ });
+
+ return (
+
+ }
+ onClick={() => setUploadOpen(true)}
+ >
+ Добавить
+
+ )
+ }
+ sx={{ mb: 3 }}
+ />
+
+
+ setSearch(e.target.value)}
+ size="small"
+ sx={{ width: 300 }}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ }}
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+
+
+ ) : filtered.length === 0 ? (
+
+ {search ? 'Ничего не найдено' : 'Нет материалов'}
+
+ ) : (
+
+ {filtered.map((material) => (
+
+
+
+ ))}
+
+ )}
+
+ {/* Upload */}
+ setUploadOpen(false)} onSuccess={load} />
+
+ {/* Delete confirm */}
+
+
+ );
+}
diff --git a/front_minimal/src/sections/notifications/view/index.js b/front_minimal/src/sections/notifications/view/index.js
new file mode 100644
index 0000000..768a92b
--- /dev/null
+++ b/front_minimal/src/sections/notifications/view/index.js
@@ -0,0 +1 @@
+export * from './notifications-view';
diff --git a/front_minimal/src/sections/notifications/view/notifications-view.jsx b/front_minimal/src/sections/notifications/view/notifications-view.jsx
new file mode 100644
index 0000000..4870e3e
--- /dev/null
+++ b/front_minimal/src/sections/notifications/view/notifications-view.jsx
@@ -0,0 +1,244 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+
+import Box from '@mui/material/Box';
+import Tab from '@mui/material/Tab';
+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 Badge from '@mui/material/Badge';
+import Button from '@mui/material/Button';
+import Divider from '@mui/material/Divider';
+import Tooltip from '@mui/material/Tooltip';
+import ListItem from '@mui/material/ListItem';
+import Typography from '@mui/material/Typography';
+import IconButton from '@mui/material/IconButton';
+import ListItemText from '@mui/material/ListItemText';
+import CircularProgress from '@mui/material/CircularProgress';
+
+import { paths } from 'src/routes/paths';
+
+import {
+ markAsRead,
+ markAllAsRead,
+ getNotifications,
+ deleteNotification,
+} from 'src/utils/notifications-api';
+
+import { DashboardContent } from 'src/layouts/dashboard';
+
+import { Iconify } from 'src/components/iconify';
+import { CustomBreadcrumbs } from 'src/components/custom-breadcrumbs';
+
+// ----------------------------------------------------------------------
+
+function formatDate(s) {
+ if (!s) return '';
+ const d = new Date(s);
+ return Number.isNaN(d.getTime())
+ ? ''
+ : d.toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
+}
+
+function notifIcon(type) {
+ switch (type) {
+ case 'success': return 'eva:checkmark-circle-2-outline';
+ case 'warning': return 'eva:alert-triangle-outline';
+ case 'error': return 'eva:close-circle-outline';
+ default: return 'eva:bell-outline';
+ }
+}
+
+function notifColor(type) {
+ switch (type) {
+ case 'success': return 'success.main';
+ case 'warning': return 'warning.main';
+ case 'error': return 'error.main';
+ default: return 'info.main';
+ }
+}
+
+// ----------------------------------------------------------------------
+
+export function NotificationsView() {
+ const [notifications, setNotifications] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [tab, setTab] = useState('unread');
+ const [processing, setProcessing] = useState(null);
+
+ const load = useCallback(async () => {
+ try {
+ setLoading(true);
+ const res = await getNotifications({ page_size: 100 });
+ setNotifications(res.results);
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка загрузки');
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => { load(); }, [load]);
+
+ const filtered = notifications.filter((n) =>
+ tab === 'all' ? true : !n.is_read
+ );
+
+ const unreadCount = notifications.filter((n) => !n.is_read).length;
+
+ const handleMarkAsRead = async (id) => {
+ try {
+ setProcessing(id);
+ await markAsRead(id);
+ setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, is_read: true } : n));
+ } catch {
+ // ignore
+ } finally {
+ setProcessing(null);
+ }
+ };
+
+ const handleMarkAll = async () => {
+ try {
+ await markAllAsRead();
+ setNotifications((prev) => prev.map((n) => ({ ...n, is_read: true })));
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.message || 'Ошибка');
+ }
+ };
+
+ const handleDelete = async (id) => {
+ try {
+ setProcessing(id);
+ await deleteNotification(id);
+ setNotifications((prev) => prev.filter((n) => n.id !== id));
+ } catch {
+ // ignore
+ } finally {
+ setProcessing(null);
+ }
+ };
+
+ return (
+
+ 0 && (
+
+ Отметить все как прочитанные
+
+ )
+ }
+ sx={{ mb: 3 }}
+ />
+
+ {error && {error}}
+
+ setTab(v)} sx={{ mb: 3 }}>
+
+ 0 ? 1.5 : 0 }}>Непрочитанные
+
+ } />
+
+
+
+ {loading ? (
+
+
+
+ ) : filtered.length === 0 ? (
+
+
+
+ {tab === 'unread' ? 'Нет непрочитанных уведомлений' : 'Нет уведомлений'}
+
+
+ ) : (
+
+ {filtered.map((n, idx) => (
+
+
+ {!n.is_read && (
+
+ handleMarkAsRead(n.id)}
+ disabled={processing === n.id}
+ >
+
+
+
+ )}
+
+ handleDelete(n.id)}
+ disabled={processing === n.id}
+ >
+
+
+
+
+ }
+ >
+
+
+
+
+
+ {n.title || 'Уведомление'}
+
+ {!n.is_read && (
+
+ )}
+
+ }
+ secondary={
+ <>
+
+ {n.message}
+
+
+ {formatDate(n.created_at)}
+
+ >
+ }
+ />
+
+ {idx < filtered.length - 1 && }
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/front_minimal/src/sections/students/view/index.js b/front_minimal/src/sections/students/view/index.js
new file mode 100644
index 0000000..6496d7c
--- /dev/null
+++ b/front_minimal/src/sections/students/view/index.js
@@ -0,0 +1 @@
+export * from './students-view';
diff --git a/front_minimal/src/sections/students/view/students-view.jsx b/front_minimal/src/sections/students/view/students-view.jsx
new file mode 100644
index 0000000..9a11205
--- /dev/null
+++ b/front_minimal/src/sections/students/view/students-view.jsx
@@ -0,0 +1,517 @@
+'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 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 ListItem from '@mui/material/ListItem';
+import TextField from '@mui/material/TextField';
+import Typography from '@mui/material/Typography';
+import CardHeader from '@mui/material/CardHeader';
+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 {
+ getStudents,
+ getMyMentors,
+ getMyInvitations,
+ addStudentInvitation,
+ sendMentorshipRequest,
+ acceptMentorshipRequest,
+ rejectMentorshipRequest,
+ rejectInvitationAsStudent,
+ confirmInvitationAsStudent,
+ getMentorshipRequestsPending,
+} from 'src/utils/students-api';
+
+import { CONFIG } from 'src/config-global';
+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';
+
+// ----------------------------------------------------------------------
+
+function avatarUrl(href) {
+ if (!href) return null;
+ if (href.startsWith('http://') || href.startsWith('https://')) return href;
+ const base = CONFIG.site.serverUrl?.replace('/api', '') || '';
+ return base + (href.startsWith('/') ? href : `/${href}`);
+}
+
+function initials(firstName, lastName) {
+ return `${(firstName || '')[0] || ''}${(lastName || '')[0] || ''}`.toUpperCase();
+}
+
+// ----------------------------------------------------------------------
+// MENTOR VIEWS
+
+function MentorStudentList({ onRefresh }) {
+ const [students, setStudents] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
+ const [error, setError] = useState(null);
+
+ 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 || {};
+ return (
+
+
+
+ {initials(u.first_name, u.last_name)}
+
+
+
+ {s.total_lessons != null && (
+
+ {s.total_lessons} уроков
+
+ )}
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+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 (
+
+ handle(r.id, 'accept')}
+ disabled={processing === r.id}
+ >
+ Принять
+
+ handle(r.id, 'reject')}
+ disabled={processing === r.id}
+ >
+ Отклонить
+
+
+ }
+ >
+
+
+ {initials(s.first_name, s.last_name)}
+
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+function InviteDialog({ open, onClose, onSuccess }) {
+ const [mode, setMode] = useState('email'); // 'email' | 'code'
+ const [value, setValue] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [successMsg, setSuccessMsg] = useState(null);
+
+ const reset = () => { setValue(''); setError(null); setSuccessMsg(null); };
+
+ const handleSend = async () => {
+ if (!value.trim()) return;
+ try {
+ setLoading(true);
+ setError(null);
+ const payload = mode === 'email' ? { email: value.trim() } : { universal_code: value.trim() };
+ const res = await addStudentInvitation(payload);
+ setSuccessMsg(res?.message || 'Приглашение отправлено');
+ onSuccess();
+ } catch (e) {
+ setError(e?.response?.data?.detail || e?.response?.data?.email?.[0] || e?.message || 'Ошибка');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
+
+// ----------------------------------------------------------------------
+// CLIENT VIEWS
+
+function ClientMentorList() {
+ const [mentors, setMentors] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ getMyMentors()
+ .then((list) => setMentors(Array.isArray(list) ? list : []))
+ .catch(() => setMentors([]))
+ .finally(() => setLoading(false));
+ }, []);
+
+ if (loading) return ;
+
+ return (
+
+ Мои менторы
+ {mentors.length === 0 ? (
+ Нет подключённых менторов
+ ) : (
+
+ {mentors.map((m) => (
+
+
+
+ {initials(m.first_name, m.last_name)}
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+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) => i.status === 'pending') : []);
+ } 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 null;
+ if (invitations.length === 0) return null;
+
+ return (
+
+
+
+
+ {error && {error}}
+
+ {invitations.map((inv) => {
+ const m = inv.mentor || {};
+ return (
+
+ handle(inv.id, 'confirm')} disabled={processing === inv.id}>
+ Принять
+
+ handle(inv.id, 'reject')} disabled={processing === inv.id}>
+ Отклонить
+
+
+ }
+ >
+
+ {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 (
+
+ );
+}
+
+// ----------------------------------------------------------------------
+
+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)}>
+ Пригласить ученика
+
+ ) : (
+ } onClick={() => setRequestOpen(true)}>
+ Найти ментора
+
+ )
+ }
+ sx={{ mb: 3 }}
+ />
+
+ {isMentor ? (
+ <>
+ setTab(v)} sx={{ mb: 3 }}>
+
+
+
+ {tab === 0 && }
+ {tab === 1 && }
+ setInviteOpen(false)} onSuccess={refresh} />
+ >
+ ) : (
+ <>
+
+
+ setRequestOpen(false)} onSuccess={refresh} />
+ >
+ )}
+
+ );
+}
diff --git a/front_minimal/src/utils/homework-api.js b/front_minimal/src/utils/homework-api.js
new file mode 100644
index 0000000..68c2299
--- /dev/null
+++ b/front_minimal/src/utils/homework-api.js
@@ -0,0 +1,135 @@
+import axios from 'src/utils/axios';
+
+// ----------------------------------------------------------------------
+
+export async function getHomework(params) {
+ const q = new URLSearchParams();
+ if (params?.status) q.append('status', params.status);
+ if (params?.page_size) q.append('page_size', String(params.page_size || 1000));
+ if (params?.child_id) q.append('child_id', params.child_id);
+ const query = q.toString();
+ const url = `/homework/homeworks/${query ? `?${query}` : ''}`;
+ const res = await axios.get(url);
+ const {data} = res;
+ if (Array.isArray(data)) return { results: data, count: data.length };
+ return { results: data?.results ?? [], count: data?.count ?? 0 };
+}
+
+export async function getHomeworkById(id) {
+ const res = await axios.get(`/homework/homeworks/${id}/`);
+ return res.data;
+}
+
+export async function createHomework(data) {
+ const payload = { max_score: 5, passing_score: 1, ...data };
+ const res = await axios.post('/homework/homeworks/', payload);
+ return res.data;
+}
+
+export async function updateHomework(id, data) {
+ const res = await axios.patch(`/homework/homeworks/${id}/`, data);
+ return res.data;
+}
+
+export async function publishHomework(id) {
+ const res = await axios.post(`/homework/homeworks/${id}/publish/`);
+ return res.data;
+}
+
+export async function getHomeworkSubmissions(homeworkId, options) {
+ const params = new URLSearchParams({ homework_id: String(homeworkId) });
+ if (options?.child_id) params.append('child_id', options.child_id);
+ const res = await axios.get(`/homework/submissions/?${params.toString()}`);
+ const {data} = res;
+ if (Array.isArray(data)) return data;
+ return data?.results ?? [];
+}
+
+export async function getMySubmission(homeworkId, options) {
+ const list = await getHomeworkSubmissions(homeworkId, options);
+ return list.length > 0 ? list[0] : null;
+}
+
+export async function getHomeworkSubmission(submissionId) {
+ const res = await axios.get(`/homework/submissions/${submissionId}/`);
+ return res.data;
+}
+
+export async function gradeSubmission(submissionId, data) {
+ const res = await axios.post(`/homework/submissions/${submissionId}/grade/`, data);
+ return res.data;
+}
+
+export async function checkSubmissionWithAi(submissionId) {
+ const res = await axios.post(`/homework/submissions/${submissionId}/check_with_ai/`);
+ return res.data;
+}
+
+export async function returnSubmissionForRevision(submissionId, feedback) {
+ const res = await axios.post(`/homework/submissions/${submissionId}/return_for_revision/`, {
+ feedback,
+ });
+ return res.data;
+}
+
+export async function deleteSubmission(submissionId) {
+ await axios.delete(`/homework/submissions/${submissionId}/`);
+}
+
+export async function submitHomework(homeworkId, data, onUploadProgress) {
+ const hasFiles = data.files && data.files.length > 0;
+ if (hasFiles) {
+ const formData = new FormData();
+ formData.append('homework_id', String(homeworkId));
+ if (data.content) formData.append('content', data.content);
+ data.files.forEach((f) => formData.append('attachment', f));
+ const res = await axios.post('/homework/submissions/', formData, {
+ onUploadProgress:
+ onUploadProgress &&
+ ((event) => {
+ if (event.total && event.total > 0) {
+ onUploadProgress(Math.min(Math.round((event.loaded / event.total) * 100), 100));
+ }
+ }),
+ });
+ return res.data;
+ }
+ const res = await axios.post('/homework/submissions/', {
+ homework_id: homeworkId,
+ content: data.content || '',
+ });
+ return res.data;
+}
+
+export async function uploadHomeworkFile(homeworkId, file) {
+ const formData = new FormData();
+ formData.append('homework', String(homeworkId));
+ formData.append('file_type', 'assignment');
+ formData.append('file', file);
+ const res = await axios.post('/homework/files/', formData);
+ return res.data;
+}
+
+export async function deleteHomeworkFile(fileId) {
+ await axios.delete(`/homework/files/${fileId}/`);
+}
+
+// Helpers
+export function validateHomeworkFiles(files) {
+ const MAX_FILES = 10;
+ const MAX_SIZE = 50 * 1024 * 1024;
+ if (files.length > MAX_FILES) return { valid: false, error: `Максимум ${MAX_FILES} файлов` };
+ const oversized = files.find((f) => f.size > MAX_SIZE);
+ if (oversized) return { valid: false, error: `Файл "${oversized.name}" больше 50 МБ` };
+ return { valid: true };
+}
+
+export function getHomeworkStatus(hw) {
+ if (hw.status !== 'published') return 'pending';
+ if (hw.checked_submissions > 0 && hw.checked_submissions === hw.total_submissions)
+ return 'reviewed';
+ if (hw.returned_submissions > 0 && hw.returned_submissions === hw.total_submissions)
+ return 'returned';
+ if (hw.total_submissions > 0) return 'submitted';
+ return 'pending';
+}
diff --git a/front_minimal/src/utils/materials-api.js b/front_minimal/src/utils/materials-api.js
new file mode 100644
index 0000000..008dbe7
--- /dev/null
+++ b/front_minimal/src/utils/materials-api.js
@@ -0,0 +1,71 @@
+import axios from 'src/utils/axios';
+
+// ----------------------------------------------------------------------
+
+export async function getMaterials(params) {
+ const res = await axios.get('/materials/materials/', { params });
+ const {data} = res;
+ if (Array.isArray(data)) return { results: data, count: data.length };
+ return { results: data?.results ?? [], count: data?.count ?? 0 };
+}
+
+export async function getMyMaterials() {
+ const res = await axios.get('/materials/materials/my_materials/');
+ const {data} = res;
+ if (Array.isArray(data)) return data;
+ return data?.results ?? [];
+}
+
+export async function getMaterialById(id) {
+ const res = await axios.get(`/materials/materials/${id}/`);
+ return res.data;
+}
+
+export async function createMaterial(data) {
+ const formData = new FormData();
+ formData.append('title', data.title);
+ if (data.description) formData.append('description', data.description);
+ if (data.file) formData.append('file', data.file);
+ if (data.category) formData.append('category', String(data.category));
+ if (data.is_public !== undefined) formData.append('is_public', String(data.is_public));
+ const res = await axios.post('/materials/materials/', formData);
+ return res.data;
+}
+
+export async function updateMaterial(id, data) {
+ const formData = new FormData();
+ if (data.title) formData.append('title', data.title);
+ if (data.description !== undefined) formData.append('description', data.description);
+ if (data.file) formData.append('file', data.file);
+ if (data.category) formData.append('category', String(data.category));
+ if (data.is_public !== undefined) formData.append('is_public', String(data.is_public));
+ const res = await axios.patch(`/materials/materials/${id}/`, formData);
+ return res.data;
+}
+
+export async function deleteMaterial(id) {
+ await axios.delete(`/materials/materials/${id}/`);
+}
+
+export async function shareMaterial(id, userIds) {
+ await axios.post(`/materials/materials/${id}/share/`, { user_ids: userIds });
+}
+
+export async function getMaterialCategories() {
+ const res = await axios.get('/materials/categories/');
+ return res.data;
+}
+
+// Helper: icon by type
+export function getMaterialTypeIcon(material) {
+ const type = material?.material_type;
+ const mime = (material?.file_type || '').toLowerCase();
+ const name = material?.file_name || material?.file || '';
+ if (type === 'image' || mime.startsWith('image/') || /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(name)) return 'eva:image-outline';
+ if (type === 'video' || mime.startsWith('video/') || /\.(mp4|webm|mov|avi)$/i.test(name)) return 'eva:video-outline';
+ if (type === 'audio' || mime.startsWith('audio/') || /\.(mp3|wav|ogg|m4a)$/i.test(name)) return 'eva:headphones-outline';
+ if (type === 'document' || mime.includes('pdf') || /\.(pdf|docx?|odt)$/i.test(name)) return 'eva:file-text-outline';
+ if (type === 'presentation' || /\.(pptx?|odp)$/i.test(name)) return 'eva:monitor-outline';
+ if (type === 'archive' || /\.(zip|rar|7z|tar)$/i.test(name)) return 'eva:archive-outline';
+ return 'eva:file-outline';
+}
diff --git a/front_minimal/src/utils/notifications-api.js b/front_minimal/src/utils/notifications-api.js
new file mode 100644
index 0000000..2e8f47f
--- /dev/null
+++ b/front_minimal/src/utils/notifications-api.js
@@ -0,0 +1,29 @@
+import axios from 'src/utils/axios';
+
+export async function getNotifications(params) {
+ const res = await axios.get('/notifications/', { params });
+ const {data} = res;
+ if (Array.isArray(data)) return { results: data, count: data.length };
+ return { results: data?.results ?? [], count: data?.count ?? 0 };
+}
+
+export async function getUnreadNotifications() {
+ const res = await axios.get('/notifications/unread/');
+ const data = res.data ?? {};
+ return {
+ data: data.data ?? [],
+ count: data.count ?? 0,
+ };
+}
+
+export async function markAsRead(id) {
+ await axios.post(`/notifications/${id}/mark_as_read/`);
+}
+
+export async function markAllAsRead() {
+ await axios.post('/notifications/mark_all_as_read/');
+}
+
+export async function deleteNotification(id) {
+ await axios.delete(`/notifications/${id}/`);
+}
diff --git a/front_minimal/src/utils/students-api.js b/front_minimal/src/utils/students-api.js
new file mode 100644
index 0000000..89277dd
--- /dev/null
+++ b/front_minimal/src/utils/students-api.js
@@ -0,0 +1,67 @@
+import axios from 'src/utils/axios';
+
+// Students (mentor's clients)
+export async function getStudents(params) {
+ const res = await axios.get('/manage/clients/', { params });
+ const {data} = res;
+ if (Array.isArray(data)) return { results: data, count: data.length };
+ return { results: data?.results ?? [], count: data?.count ?? 0 };
+}
+
+// Invite student by email or universal_code
+export async function addStudentInvitation(payload) {
+ const res = await axios.post('/manage/clients/add_client/', payload);
+ return res.data;
+}
+
+// Generate invitation link for mentor
+export async function generateInvitationLink() {
+ const res = await axios.post('/manage/clients/generate-invitation-link/');
+ return res.data;
+}
+
+// Pending mentorship requests for mentor
+export async function getMentorshipRequestsPending() {
+ const res = await axios.get('/mentorship-requests/pending/');
+ return res.data;
+}
+
+export async function acceptMentorshipRequest(id) {
+ const res = await axios.post(`/mentorship-requests/${id}/accept/`);
+ return res.data;
+}
+
+export async function rejectMentorshipRequest(id) {
+ const res = await axios.post(`/mentorship-requests/${id}/reject/`);
+ return res.data;
+}
+
+// Client: send mentorship request to mentor by code
+export async function sendMentorshipRequest(mentorCode) {
+ const res = await axios.post('/mentorship-requests/send/', {
+ mentor_code: mentorCode.trim().toUpperCase(),
+ });
+ return res.data;
+}
+
+// Client: my mentors
+export async function getMyMentors() {
+ const res = await axios.get('/mentorship-requests/my-mentors/');
+ return res.data;
+}
+
+// Client: incoming invitations from mentors
+export async function getMyInvitations() {
+ const res = await axios.get('/invitation/my-invitations/');
+ return res.data;
+}
+
+export async function confirmInvitationAsStudent(invitationId) {
+ const res = await axios.post('/invitation/confirm-as-student/', { invitation_id: invitationId });
+ return res.data;
+}
+
+export async function rejectInvitationAsStudent(invitationId) {
+ const res = await axios.post('/invitation/reject-as-student/', { invitation_id: invitationId });
+ return res.data;
+}