417 lines
15 KiB
JavaScript
417 lines
15 KiB
JavaScript
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
|
||
import Box from '@mui/material/Box';
|
||
import Card from '@mui/material/Card';
|
||
import Stack from '@mui/material/Stack';
|
||
import Divider from '@mui/material/Divider';
|
||
import Avatar from '@mui/material/Avatar';
|
||
import Button from '@mui/material/Button';
|
||
import Typography from '@mui/material/Typography';
|
||
import CircularProgress from '@mui/material/CircularProgress';
|
||
import LinearProgress from '@mui/material/LinearProgress';
|
||
import { useTheme } from '@mui/material/styles';
|
||
|
||
import { useRouter } from 'src/routes/hooks';
|
||
import { fDateTime } from 'src/utils/format-time';
|
||
import { getClientDashboard, getChildDashboard } from 'src/utils/dashboard-api';
|
||
import { createLiveKitRoom } from 'src/utils/livekit-api';
|
||
import { DashboardContent } from 'src/layouts/dashboard';
|
||
import { useAuthContext } from 'src/auth/hooks';
|
||
import { CONFIG } from 'src/config-global';
|
||
import { varAlpha } from 'src/theme/styles';
|
||
import { Iconify } from 'src/components/iconify';
|
||
|
||
import { CourseWidgetSummary } from '../../course/course-widget-summary';
|
||
import { CourseProgress } from '../../course/course-progress';
|
||
import { CourseMyAccount } from '../../course/course-my-account';
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
const formatDateTime = (str) => {
|
||
if (!str) return '—';
|
||
try {
|
||
const date = new Date(str);
|
||
if (isNaN(date.getTime())) return '—';
|
||
const today = new Date();
|
||
const tomorrow = new Date(today);
|
||
tomorrow.setDate(today.getDate() + 1);
|
||
const time = date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
||
if (date.toDateString() === today.toDateString()) return `Сегодня, ${time}`;
|
||
if (date.toDateString() === tomorrow.toDateString()) return `Завтра, ${time}`;
|
||
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
|
||
} catch {
|
||
return '—';
|
||
}
|
||
};
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
function LessonItem({ lesson }) {
|
||
const router = useRouter();
|
||
const [joining, setJoining] = useState(false);
|
||
|
||
const mentorName = lesson.mentor
|
||
? `${lesson.mentor.first_name || ''} ${lesson.mentor.last_name || ''}`.trim() || 'Ментор'
|
||
: 'Ментор';
|
||
|
||
const now = new Date();
|
||
const startTime = new Date(lesson.start_time);
|
||
const diffMin = (startTime - now) / 60000;
|
||
const canJoin = diffMin < 11 && diffMin > -90;
|
||
|
||
const handleJoin = async () => {
|
||
try {
|
||
setJoining(true);
|
||
const res = await createLiveKitRoom(lesson.id);
|
||
const token = res?.access_token || res?.token;
|
||
router.push(`/video-call?token=${token}&lesson_id=${lesson.id}`);
|
||
} catch (err) {
|
||
console.error('Join error:', err);
|
||
setJoining(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Stack
|
||
direction="row"
|
||
spacing={2}
|
||
alignItems="center"
|
||
sx={{
|
||
py: 1.5,
|
||
px: 2,
|
||
borderRadius: 1.5,
|
||
'&:hover': { bgcolor: 'action.hover' },
|
||
}}
|
||
>
|
||
<Avatar sx={{ width: 40, height: 40, bgcolor: 'primary.main', fontSize: 14 }}>
|
||
{mentorName[0]?.toUpperCase()}
|
||
</Avatar>
|
||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||
<Typography variant="subtitle2" noWrap>
|
||
{lesson.title || lesson.subject || 'Занятие'}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||
{mentorName}
|
||
</Typography>
|
||
</Box>
|
||
{canJoin ? (
|
||
<Button
|
||
size="small"
|
||
variant="contained"
|
||
color="success"
|
||
onClick={handleJoin}
|
||
disabled={joining}
|
||
startIcon={<Iconify icon="solar:videocamera-bold" width={16} />}
|
||
sx={{ flexShrink: 0, minWidth: 110 }}
|
||
>
|
||
{joining ? '...' : 'Подключиться'}
|
||
</Button>
|
||
) : (
|
||
<Typography variant="caption" sx={{ color: 'text.secondary', flexShrink: 0 }}>
|
||
{formatDateTime(lesson.start_time)}
|
||
</Typography>
|
||
)}
|
||
</Stack>
|
||
);
|
||
}
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
function HomeworkItem({ homework }) {
|
||
const statusColor = {
|
||
pending: 'warning',
|
||
submitted: 'info',
|
||
reviewed: 'success',
|
||
completed: 'success',
|
||
}[homework.status] || 'default';
|
||
|
||
return (
|
||
<Stack
|
||
direction="row"
|
||
spacing={2}
|
||
alignItems="center"
|
||
sx={{
|
||
py: 1.5,
|
||
px: 2,
|
||
borderRadius: 1.5,
|
||
'&:hover': { bgcolor: 'action.hover' },
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
width: 40,
|
||
height: 40,
|
||
borderRadius: 1,
|
||
bgcolor: `${statusColor}.main`,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
flexShrink: 0,
|
||
opacity: 0.8,
|
||
}}
|
||
>
|
||
<Iconify icon="solar:document-bold" width={20} sx={{ color: 'white' }} />
|
||
</Box>
|
||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||
<Typography variant="subtitle2" noWrap>
|
||
{homework.title}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||
{homework.subject || 'Предмет не указан'}
|
||
</Typography>
|
||
</Box>
|
||
{homework.grade != null && (
|
||
<Typography variant="caption" sx={{ color: `${statusColor}.main`, fontWeight: 600, flexShrink: 0 }}>
|
||
{homework.grade}/5
|
||
</Typography>
|
||
)}
|
||
</Stack>
|
||
);
|
||
}
|
||
|
||
// ----------------------------------------------------------------------
|
||
|
||
export function OverviewClientView({ childId, childName }) {
|
||
const theme = useTheme();
|
||
const { user } = useAuthContext();
|
||
const [stats, setStats] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
const fetchData = useCallback(
|
||
async (signal) => {
|
||
try {
|
||
setLoading(true);
|
||
const data = childId
|
||
? await getChildDashboard(childId, { signal })
|
||
: await getClientDashboard({ signal });
|
||
if (!signal?.aborted) setStats(data);
|
||
} catch (err) {
|
||
if (err?.name === 'AbortError' || err?.name === 'CanceledError') return;
|
||
console.error('Client dashboard error:', err);
|
||
} finally {
|
||
if (!signal?.aborted) setLoading(false);
|
||
}
|
||
},
|
||
[childId]
|
||
);
|
||
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
fetchData(controller.signal);
|
||
return () => controller.abort();
|
||
}, [fetchData]);
|
||
|
||
const displayName = childName || user?.first_name || 'Студент';
|
||
|
||
const completionPct =
|
||
stats?.total_lessons > 0
|
||
? Math.round((stats.completed_lessons / stats.total_lessons) * 100)
|
||
: 0;
|
||
|
||
if (loading && !stats) {
|
||
return (
|
||
<Box sx={{ display: 'flex', height: '80vh', alignItems: 'center', justifyContent: 'center' }}>
|
||
<CircularProgress />
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<DashboardContent
|
||
maxWidth={false}
|
||
disablePadding
|
||
sx={{ borderTop: { lg: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)}` } }}
|
||
>
|
||
<Box sx={{ display: 'flex', flex: '1 1 auto', flexDirection: { xs: 'column', lg: 'row' } }}>
|
||
{/* LEFT */}
|
||
<Box
|
||
sx={{
|
||
gap: 3,
|
||
display: 'flex',
|
||
flex: '1 1 auto',
|
||
flexDirection: 'column',
|
||
px: { xs: 2, sm: 3, xl: 5 },
|
||
py: 3,
|
||
borderRight: { lg: `solid 1px ${varAlpha(theme.vars.palette.grey['500Channel'], 0.12)}` },
|
||
}}
|
||
>
|
||
{/* Stat widgets */}
|
||
<Box
|
||
sx={{
|
||
gap: 3,
|
||
display: 'grid',
|
||
gridTemplateColumns: { xs: 'repeat(2, 1fr)', md: 'repeat(4, 1fr)' },
|
||
}}
|
||
>
|
||
<CourseWidgetSummary
|
||
title="Занятий всего"
|
||
total={Number(stats?.total_lessons || 0)}
|
||
icon={`${CONFIG.site.basePath}/assets/icons/courses/ic-courses-progress.svg`}
|
||
color="primary"
|
||
/>
|
||
<CourseWidgetSummary
|
||
title="Пройдено"
|
||
total={Number(stats?.completed_lessons || 0)}
|
||
icon={`${CONFIG.site.basePath}/assets/icons/courses/ic-courses-certificates.svg`}
|
||
color="success"
|
||
/>
|
||
<CourseWidgetSummary
|
||
title="ДЗ к выполнению"
|
||
total={Number(stats?.homework_pending || 0)}
|
||
icon={`${CONFIG.site.basePath}/assets/icons/courses/ic-courses-completed.svg`}
|
||
color="warning"
|
||
/>
|
||
<CourseWidgetSummary
|
||
title="Средняя оценка"
|
||
total={
|
||
stats?.average_grade
|
||
? `${parseFloat(Number(stats.average_grade).toFixed(1))}/5`
|
||
: '—'
|
||
}
|
||
icon={`${CONFIG.site.basePath}/assets/icons/glass/ic-glass-bag.svg`}
|
||
color="info"
|
||
/>
|
||
</Box>
|
||
|
||
{/* Progress bar */}
|
||
<Card sx={{ p: 3 }}>
|
||
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
|
||
<Typography variant="h6">Прогресс занятий</Typography>
|
||
<Typography variant="h6" sx={{ color: 'primary.main' }}>
|
||
{completionPct}%
|
||
</Typography>
|
||
</Stack>
|
||
<LinearProgress
|
||
variant="determinate"
|
||
value={completionPct}
|
||
sx={{ height: 8, borderRadius: 1, bgcolor: 'background.neutral' }}
|
||
/>
|
||
<Stack direction="row" justifyContent="space-between" sx={{ mt: 1 }}>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||
Пройдено: {stats?.completed_lessons || 0}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||
Всего: {stats?.total_lessons || 0}
|
||
</Typography>
|
||
</Stack>
|
||
</Card>
|
||
|
||
{/* Upcoming lessons + homework */}
|
||
<Box
|
||
sx={{
|
||
gap: 3,
|
||
display: 'grid',
|
||
gridTemplateColumns: { xs: '1fr', md: 'repeat(2, 1fr)' },
|
||
}}
|
||
>
|
||
{/* Upcoming lessons */}
|
||
<Card>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="space-between"
|
||
sx={{ px: 3, py: 2 }}
|
||
>
|
||
<Typography variant="h6">Ближайшие занятия</Typography>
|
||
<Iconify icon="solar:calendar-bold" width={20} sx={{ color: 'primary.main' }} />
|
||
</Stack>
|
||
<Divider />
|
||
{loading ? (
|
||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||
<CircularProgress size={24} />
|
||
</Box>
|
||
) : stats?.upcoming_lessons?.length > 0 ? (
|
||
<Box sx={{ py: 1 }}>
|
||
{stats.upcoming_lessons.slice(0, 4).map((lesson) => (
|
||
<LessonItem key={lesson.id} lesson={lesson} />
|
||
))}
|
||
</Box>
|
||
) : (
|
||
<Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>
|
||
<Iconify icon="solar:calendar-bold" width={40} sx={{ mb: 1, opacity: 0.3 }} />
|
||
<Typography variant="body2">Нет запланированных занятий</Typography>
|
||
</Box>
|
||
)}
|
||
</Card>
|
||
|
||
{/* Homework */}
|
||
<Card>
|
||
<Stack
|
||
direction="row"
|
||
alignItems="center"
|
||
justifyContent="space-between"
|
||
sx={{ px: 3, py: 2 }}
|
||
>
|
||
<Typography variant="h6">Домашние задания</Typography>
|
||
<Iconify icon="solar:document-bold" width={20} sx={{ color: 'warning.main' }} />
|
||
</Stack>
|
||
<Divider />
|
||
{loading ? (
|
||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||
<CircularProgress size={24} />
|
||
</Box>
|
||
) : stats?.recent_homework?.length > 0 ? (
|
||
<Box sx={{ py: 1 }}>
|
||
{stats.recent_homework.slice(0, 4).map((hw) => (
|
||
<HomeworkItem key={hw.id} homework={hw} />
|
||
))}
|
||
</Box>
|
||
) : (
|
||
<Box sx={{ p: 3, textAlign: 'center', color: 'text.secondary' }}>
|
||
<Iconify icon="solar:document-bold" width={40} sx={{ mb: 1, opacity: 0.3 }} />
|
||
<Typography variant="body2">Нет домашних заданий</Typography>
|
||
</Box>
|
||
)}
|
||
</Card>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* RIGHT sidebar */}
|
||
<Box
|
||
sx={{
|
||
width: 1,
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
px: { xs: 2, sm: 3, xl: 5 },
|
||
pt: { lg: 8 },
|
||
pb: { xs: 8, xl: 10 },
|
||
flexShrink: 0,
|
||
gap: 3,
|
||
maxWidth: { lg: 300, xl: 340 },
|
||
bgcolor: { lg: 'background.neutral' },
|
||
}}
|
||
>
|
||
<CourseMyAccount user={childId ? { first_name: childName } : user} />
|
||
|
||
{/* Next lesson highlight */}
|
||
{stats?.next_lesson && (
|
||
<Card sx={{ p: 2.5, bgcolor: 'primary.lighter' }}>
|
||
<Typography variant="subtitle2" sx={{ mb: 1, color: 'primary.dark' }}>
|
||
Следующее занятие
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5 }}>
|
||
{stats.next_lesson.title || 'Занятие'}
|
||
</Typography>
|
||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||
{formatDateTime(stats.next_lesson.start_time)}
|
||
</Typography>
|
||
{stats.next_lesson.mentor && (
|
||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mt: 1.5 }}>
|
||
<Avatar sx={{ width: 24, height: 24, fontSize: 10, bgcolor: 'primary.main' }}>
|
||
{(stats.next_lesson.mentor.first_name || 'М')[0]}
|
||
</Avatar>
|
||
<Typography variant="caption">
|
||
{`${stats.next_lesson.mentor.first_name || ''} ${stats.next_lesson.mentor.last_name || ''}`.trim()}
|
||
</Typography>
|
||
</Stack>
|
||
)}
|
||
</Card>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</DashboardContent>
|
||
);
|
||
}
|