uchill/front_minimal/src/sections/overview/client/view/overview-client-view.jsx

428 lines
15 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 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 { paths } from 'src/routes/paths';
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(`${paths.dashboard.prejoin}?token=${encodeURIComponent(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]);
// Периодическое обновление каждые 60 сек — кнопка «Подключиться» появляется автоматически
useEffect(() => {
const id = setInterval(() => {
const controller = new AbortController();
fetchData(controller.signal);
return () => controller.abort();
}, 60_000);
return () => clearInterval(id);
}, [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>
);
}