253 lines
9.0 KiB
TypeScript
253 lines
9.0 KiB
TypeScript
/**
|
||
* Dashboard для студента (роль client) или для выбранного ребёнка (роль parent)
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import { getClientDashboard, getChildDashboard, DashboardStats } from '@/api/dashboard';
|
||
import { StatCard } from './StatCard';
|
||
import { LessonCard } from './LessonCard';
|
||
import { HomeworkCard } from './HomeworkCard';
|
||
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
|
||
|
||
export interface ClientDashboardProps {
|
||
/** Для родителя: id выбранного ребёнка (user_id) — данные загружаются как для этого ребёнка */
|
||
childId?: string | null;
|
||
/** Для родителя: имя ребёнка для приветствия */
|
||
childName?: string | null;
|
||
}
|
||
|
||
export const ClientDashboard: React.FC<ClientDashboardProps> = ({ childId }) => {
|
||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const isParentView = Boolean(childId);
|
||
|
||
useEffect(() => {
|
||
loadDashboard();
|
||
}, [childId]);
|
||
|
||
const loadDashboard = async () => {
|
||
try {
|
||
setLoading(true);
|
||
setError(null);
|
||
const data = isParentView && childId
|
||
? await getChildDashboard(childId)
|
||
: await getClientDashboard();
|
||
setStats(data);
|
||
} catch (err: any) {
|
||
console.error('Error loading dashboard:', err);
|
||
setError(err.message || 'Ошибка загрузки данных');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
if (error && !stats) {
|
||
return (
|
||
<div style={{
|
||
padding: '24px',
|
||
textAlign: 'center',
|
||
color: 'var(--md-sys-color-error)'
|
||
}}>
|
||
<p>{error}</p>
|
||
<button
|
||
onClick={loadDashboard}
|
||
style={{
|
||
marginTop: '16px',
|
||
padding: '12px 24px',
|
||
background: 'var(--md-sys-color-primary)',
|
||
color: 'var(--md-sys-color-on-primary)',
|
||
border: 'none',
|
||
borderRadius: '20px',
|
||
cursor: 'pointer',
|
||
fontSize: '14px',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
Попробовать снова
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{
|
||
width: '100%',
|
||
maxWidth: '100%',
|
||
padding: '16px',
|
||
minHeight: '100vh'
|
||
}}>
|
||
{/* Статистика студента */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||
gap: '16px',
|
||
marginBottom: '24px'
|
||
}}>
|
||
<StatCard
|
||
title="Занятий всего"
|
||
value={loading ? '—' : (stats?.total_lessons || 0)}
|
||
loading={loading}
|
||
icon={
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
</svg>
|
||
}
|
||
/>
|
||
|
||
<StatCard
|
||
title="Пройдено"
|
||
value={loading ? '—' : (stats?.completed_lessons || 0)}
|
||
loading={loading}
|
||
icon={
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||
</svg>
|
||
}
|
||
/>
|
||
|
||
<StatCard
|
||
title="ДЗ к выполнению"
|
||
value={loading ? '—' : (stats?.homework_pending || 0)}
|
||
loading={loading}
|
||
icon={
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||
</svg>
|
||
}
|
||
/>
|
||
|
||
<StatCard
|
||
title="Средняя оценка"
|
||
value={loading ? '—' : (stats?.average_grade != null ? String(parseFloat(Number(stats.average_grade).toFixed(2))) : '-')}
|
||
loading={loading}
|
||
icon={
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
|
||
</svg>
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
{/* Следующее занятие */}
|
||
{stats?.next_lesson && (
|
||
<div style={{
|
||
background: 'var(--md-sys-color-surface)',
|
||
borderRadius: '20px',
|
||
padding: '24px',
|
||
border: '1px solid var(--md-sys-color-outline-variant)',
|
||
marginBottom: '24px',
|
||
borderLeft: '4px solid var(--md-sys-color-primary)'
|
||
}}>
|
||
<h3 style={{
|
||
fontSize: '20px',
|
||
fontWeight: '500',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
margin: '0 0 16px 0'
|
||
}}>
|
||
Ближайшее занятие
|
||
</h3>
|
||
<LessonCard lesson={stats.next_lesson} showMentor />
|
||
</div>
|
||
)}
|
||
|
||
{/* Домашние задания и расписание */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
|
||
gap: '16px',
|
||
marginBottom: '24px'
|
||
}}>
|
||
{/* Домашние задания */}
|
||
<div style={{
|
||
background: 'var(--md-sys-color-surface)',
|
||
borderRadius: '20px',
|
||
padding: '24px',
|
||
border: '1px solid var(--md-sys-color-outline-variant)'
|
||
}}>
|
||
<h3 style={{
|
||
fontSize: '20px',
|
||
fontWeight: '500',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
margin: '0 0 20px 0'
|
||
}}>
|
||
Ваши домашние задания
|
||
</h3>
|
||
{loading ? (
|
||
<LoadingSpinner size="medium" />
|
||
) : stats?.recent_homework && stats.recent_homework.length > 0 ? (
|
||
<div>
|
||
{stats.recent_homework.slice(0, 3).map((homework) => (
|
||
<HomeworkCard key={homework.id} homework={homework} />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
textAlign: 'center',
|
||
padding: '32px',
|
||
color: 'var(--md-sys-color-on-surface-variant)'
|
||
}}>
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ margin: '0 auto 16px', opacity: 0.3 }}>
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
</svg>
|
||
<p style={{ margin: 0 }}>Нет домашних заданий</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Ближайшие занятия */}
|
||
<div style={{
|
||
background: 'var(--md-sys-color-surface)',
|
||
borderRadius: '20px',
|
||
padding: '24px',
|
||
border: '1px solid var(--md-sys-color-outline-variant)'
|
||
}}>
|
||
<h3 style={{
|
||
fontSize: '20px',
|
||
fontWeight: '500',
|
||
color: 'var(--md-sys-color-on-surface)',
|
||
margin: '0 0 20px 0'
|
||
}}>
|
||
Ваши занятия
|
||
</h3>
|
||
{loading ? (
|
||
<LoadingSpinner size="medium" />
|
||
) : stats?.upcoming_lessons && stats.upcoming_lessons.length > 0 ? (
|
||
<div>
|
||
{stats.upcoming_lessons.slice(0, 3).map((lesson) => (
|
||
<LessonCard key={lesson.id} lesson={lesson} showMentor />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
textAlign: 'center',
|
||
padding: '32px',
|
||
color: 'var(--md-sys-color-on-surface-variant)'
|
||
}}>
|
||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ margin: '0 auto 16px', opacity: 0.3 }}>
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
</svg>
|
||
<p style={{ margin: 0 }}>Нет запланированных занятий</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|