Розроблення системи оцінок та прогресу у LMS
Система оцінок та прогресу відстежує прогрес студента через контент курсу, обчислює підсумкові оцінки з різних видів діяльності (завдання, квізи, участь), генерує звіти про прогрес та підтримує різні схеми оцінювання.
Моделі даних
interface Enrollment {
id: string;
userId: string;
courseId: string;
enrolledAt: Date;
completionStatus: 'in_progress' | 'completed' | 'abandoned';
progressPercent: number;
finalGrade?: number;
certificateIssuedAt?: Date;
}
interface LessonProgress {
id: string;
enrollmentId: string;
lessonId: string;
status: 'not_started' | 'in_progress' | 'completed';
progressPercent: number;
completedAt?: Date;
}
interface CourseGradeWeight {
component: 'assignment' | 'quiz' | 'participation' | 'project';
weight: number; // Відсоток підсумкової оцінки
}
interface StudentGrade {
enrollmentId: string;
component: string;
score: number;
maxScore: number;
weight: number;
}
Обчислення прогресу курсу
async function calculateCourseProgress(enrollmentId: string): Promise<number> {
const enrollment = await db.enrollments.findById(enrollmentId);
const lessonProgress = await db.lessonProgress.findByEnrollment(enrollmentId);
const lessons = await db.lessons.findByCourse(enrollment.courseId);
if (lessons.length === 0) return 0;
const completedLessons = lessonProgress.filter(p => p.status === 'completed').length;
return Math.round((completedLessons / lessons.length) * 100);
}
app.get('/api/enrollments/:enrollmentId/progress', authenticate, async (req, res) => {
const enrollment = await db.enrollments.findById(req.params.enrollmentId);
if (enrollment.userId !== req.user.id) return res.status(403).end();
const progress = await calculateCourseProgress(req.params.enrollmentId);
const lessonProgress = await db.lessonProgress.findByEnrollment(req.params.enrollmentId);
const completedLessons = lessonProgress.filter(p => p.status === 'completed').length;
const totalLessons = await db.lessons.countByCourse(enrollment.courseId);
res.json({
progressPercent: progress,
completedLessons,
totalLessons,
estimatedCompletionDate: progress < 100 ? estimateCompletion(progress) : null,
});
});
Обчислення підсумкової оцінки
async function calculateFinalGrade(enrollmentId: string): Promise<number> {
const enrollment = await db.enrollments.findById(enrollmentId);
const grades = await db.studentGrades.findByEnrollment(enrollmentId);
const weights = await db.courseGradeWeights.findByCourse(enrollment.courseId);
let weightedTotal = 0;
let totalWeight = 0;
for (const weight of weights) {
const componentGrades = grades.filter(g => g.component.startsWith(weight.component));
if (componentGrades.length === 0) continue;
const avgComponentScore = componentGrades.reduce((sum, g) => {
return sum + (g.score / g.maxScore) * 100;
}, 0) / componentGrades.length;
weightedTotal += avgComponentScore * (weight.weight / 100);
totalWeight += weight.weight / 100;
}
return totalWeight > 0 ? Math.round(weightedTotal / totalWeight) : 0;
}
app.get('/api/enrollments/:enrollmentId/grades', authenticate, async (req, res) => {
const enrollment = await db.enrollments.findById(req.params.enrollmentId);
if (enrollment.userId !== req.user.id) return res.status(403).end();
const finalGrade = await calculateFinalGrade(req.params.enrollmentId);
const grades = await db.studentGrades.findByEnrollment(req.params.enrollmentId);
const weights = await db.courseGradeWeights.findByCourse(enrollment.courseId);
const breakdown = weights.map(w => {
const componentGrades = grades.filter(g => g.component.startsWith(w.component));
const avgScore = componentGrades.length > 0
? Math.round(componentGrades.reduce((sum, g) => sum + (g.score / g.maxScore) * 100, 0) / componentGrades.length)
: 0;
return { component: w.component, score: avgScore, weight: w.weight };
});
res.json({
finalGrade,
breakdown,
isPassed: finalGrade >= 70,
});
});
Компонент звіту про прогрес
function ProgressReport({ enrollmentId }) {
const [progress, setProgress] = useState<Progress | null>(null);
const [grades, setGrades] = useState<Grades | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
Promise.all([
fetch(`/api/enrollments/${enrollmentId}/progress`).then(r => r.json()),
fetch(`/api/enrollments/${enrollmentId}/grades`).then(r => r.json()),
]).then(([p, g]) => {
setProgress(p);
setGrades(g);
setLoading(false);
});
}, [enrollmentId]);
if (loading) return <div>Завантаження...</div>;
return (
<div className="max-w-2xl mx-auto p-6 space-y-6">
<div>
<h2 className="text-2xl font-bold mb-4">Прогрес курсу</h2>
<div className="bg-gray-200 rounded-full h-4 overflow-hidden">
<div
className="bg-blue-600 h-full transition-all duration-300"
style={{ width: `${progress?.progressPercent}%` }}
/>
</div>
<p className="mt-2 text-sm text-gray-600">
{progress?.completedLessons}/{progress?.totalLessons} уроків завершено
</p>
</div>
<div>
<h2 className="text-2xl font-bold mb-4">Оцінки</h2>
<div className="bg-white border rounded-lg p-4">
<div className="text-3xl font-bold text-blue-600 mb-4">
{grades?.finalGrade}% {grades?.isPassed ? '✓' : '✗'}
</div>
<div className="space-y-3">
{grades?.breakdown.map((item) => (
<div key={item.component} className="flex justify-between items-center">
<span className="capitalize">{item.component}</span>
<div className="flex items-center gap-2">
<span className="font-semibold">{item.score}%</span>
<span className="text-gray-500 text-sm">({item.weight}%)</span>
</div>
</div>
))}
</div>
</div>
</div>
{progress?.estimatedCompletionDate && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
Орієнтовне завершення: <strong>{progress.estimatedCompletionDate}</strong>
</p>
</div>
)}
</div>
);
}
Строки виконання
Базове відстеження прогресу та оцінок — 1 тиждень. З зваженим оцінюванням, детальним розбиттям та прогнозами прогресу — 2–3 тижні.







