сделал отображение прогресса

This commit is contained in:
Андрей Дувакин 2025-11-29 21:06:28 +05:00
parent be01b5fc22
commit c6a44e8185
15 changed files with 476 additions and 12 deletions

View File

@ -1,10 +1,10 @@
from typing import Optional, List
from sqlalchemy import select
from sqlalchemy import select, func, distinct
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.domain.models import Solution
from app.domain.models import Solution, Task
class SolutionsRepository:
@ -19,6 +19,22 @@ class SolutionsRepository:
result = await self.db.execute(query)
return result.scalars().first()
async def get_completed_tasks_by_course_and_user(
self,
course_id: int,
user_id: int
) -> List[int]:
query = (
select(distinct(Solution.task_id))
.join(Task, Solution.task_id == Task.id)
.where(
Solution.student_id == user_id,
Task.course_id == course_id
)
)
result = await self.db.execute(query)
return result.scalars().all()
async def get_by_task_id(self, task_id: int) -> Optional[List[Solution]]:
query = (
select(Solution)

View File

@ -0,0 +1,36 @@
from typing import Optional, List
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Lesson, UserCheckLessons
class UserCheckLessonsRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_by_course_id_and_user_id(self, course_id: int, user_id: int) -> Optional[List[UserCheckLessons]]:
query = (
select(UserCheckLessons)
.join(Lesson, UserCheckLessons.lesson_id == Lesson.id) # связь с лекцией
.where(
Lesson.course_id == course_id,
UserCheckLessons.user_id == user_id
)
)
result = await self.db.execute(query)
return result.scalars().all()
async def get_by_user_id_and_lesson_id(self, user_id: int, lesson_id: int) -> Optional[UserCheckLessons]:
query = (
select(UserCheckLessons)
.filter_by(user_id=user_id, lesson_id=lesson_id)
)
result = await self.db.execute(query)
return result.scalars().first()
async def create(self, user_check_lessons: UserCheckLessons) -> UserCheckLessons:
self.db.add(user_check_lessons)
await self.db.commit()
return user_check_lessons

View File

@ -1,13 +1,14 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, Response
from fastapi import APIRouter, Depends, status, Response
from sqlalchemy.ext.asyncio import AsyncSession
from app.database.session import get_db
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest, UserCreate
from app.domain.entities.users import UserRead, UserUpdate, PasswordChangeRequest, UserCreate, UserCheckLessonRead
from app.domain.models import User
from app.infrastructure.dependencies import require_auth_user, require_admin
from app.infrastructure.register_service import RegisterService
from app.infrastructure.user_check_lessons_service import UserCheckLessonsService
from app.infrastructure.users_service import UsersService
users_router = APIRouter()
@ -101,3 +102,47 @@ async def get_users_by_role_name(
):
users_service = UsersService(db)
return await users_service.get_by_role_name(role_name)
@users_router.get(
'/check-my-lessons/{course_id}/',
response_model=Optional[List[UserCheckLessonRead]],
summary='Return all users with given lessons',
description='Return all users with given lessons',
)
async def get_all_users_in_lessons(
course_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user)
):
user_check_lessons_service = UserCheckLessonsService(db)
return await user_check_lessons_service.get_by_course_id_and_user_id(course_id, user.id)
@users_router.post(
'/check-lesson/{lesson_id}/',
status_code=status.HTTP_201_CREATED,
summary='Mark lesson as checked',
description='Mark lesson as checked',
)
async def mark_lesson_checked(
lesson_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user)
):
user_check_lessons_service = UserCheckLessonsService(db)
await user_check_lessons_service.create(lesson_id, user.id)
@users_router.get(
'/my-progress/{course_id}/',
summary='Return user progress',
description='Return user progress',
)
async def get_user_progress(
course_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_auth_user),
):
user_check_lessons_service = UserCheckLessonsService(db)
return await user_check_lessons_service.calculate_user_progress(course_id, user.id)

View File

@ -0,0 +1,114 @@
"""0004 добавил таблицу для отслеживания прохождения лекций
Revision ID: 5664875e4492
Revises: f8fd9a27eaa7
Create Date: 2025-11-29 19:49:20.397877
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5664875e4492'
down_revision: Union[str, Sequence[str], None] = 'f8fd9a27eaa7'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user_check_lessons',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('lesson_id', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['lesson_id'], ['public.lessons.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='public'
)
op.drop_constraint(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', type_='foreignkey')
op.drop_constraint(op.f('course_teachers_course_id_fkey'), 'course_teachers', type_='foreignkey')
op.create_foreign_key(None, 'course_teachers', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'course_teachers', 'users', ['teacher_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('enrollments_student_id_fkey'), 'enrollments', type_='foreignkey')
op.drop_constraint(op.f('enrollments_course_id_fkey'), 'enrollments', type_='foreignkey')
op.create_foreign_key(None, 'enrollments', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'enrollments', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', type_='foreignkey')
op.create_foreign_key(None, 'lesson_files', 'lessons', ['lesson_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('lessons_creator_id_fkey'), 'lessons', type_='foreignkey')
op.drop_constraint(op.f('lessons_course_id_fkey'), 'lessons', type_='foreignkey')
op.create_foreign_key(None, 'lessons', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'lessons', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('solution_comments_solution_id_fkey'), 'solution_comments', type_='foreignkey')
op.drop_constraint(op.f('solution_comments_comment_autor_id_fkey'), 'solution_comments', type_='foreignkey')
op.create_foreign_key(None, 'solution_comments', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'solution_comments', 'users', ['comment_autor_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('solution_files_solution_id_fkey'), 'solution_files', type_='foreignkey')
op.create_foreign_key(None, 'solution_files', 'solutions', ['solution_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('solutions_task_id_fkey'), 'solutions', type_='foreignkey')
op.drop_constraint(op.f('solutions_assessment_autor_id_fkey'), 'solutions', type_='foreignkey')
op.drop_constraint(op.f('solutions_student_id_fkey'), 'solutions', type_='foreignkey')
op.create_foreign_key(None, 'solutions', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'solutions', 'users', ['student_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'solutions', 'users', ['assessment_autor_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('task_files_task_id_fkey'), 'task_files', type_='foreignkey')
op.create_foreign_key(None, 'task_files', 'tasks', ['task_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('tasks_course_id_fkey'), 'tasks', type_='foreignkey')
op.drop_constraint(op.f('tasks_creator_id_fkey'), 'tasks', type_='foreignkey')
op.create_foreign_key(None, 'tasks', 'courses', ['course_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'tasks', 'users', ['creator_id'], ['id'], source_schema='public', referent_schema='public')
op.drop_constraint(op.f('users_role_id_fkey'), 'users', type_='foreignkey')
op.drop_constraint(op.f('users_status_id_fkey'), 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'statuses', ['status_id'], ['id'], source_schema='public', referent_schema='public')
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.drop_constraint(None, 'users', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('users_status_id_fkey'), 'users', 'statuses', ['status_id'], ['id'])
op.create_foreign_key(op.f('users_role_id_fkey'), 'users', 'roles', ['role_id'], ['id'])
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
op.drop_constraint(None, 'tasks', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('tasks_creator_id_fkey'), 'tasks', 'users', ['creator_id'], ['id'])
op.create_foreign_key(op.f('tasks_course_id_fkey'), 'tasks', 'courses', ['course_id'], ['id'])
op.drop_constraint(None, 'task_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('task_files_task_id_fkey'), 'task_files', 'tasks', ['task_id'], ['id'])
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.drop_constraint(None, 'solutions', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('solutions_student_id_fkey'), 'solutions', 'users', ['student_id'], ['id'])
op.create_foreign_key(op.f('solutions_assessment_autor_id_fkey'), 'solutions', 'users', ['assessment_autor_id'], ['id'])
op.create_foreign_key(op.f('solutions_task_id_fkey'), 'solutions', 'tasks', ['task_id'], ['id'])
op.drop_constraint(None, 'solution_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('solution_files_solution_id_fkey'), 'solution_files', 'solutions', ['solution_id'], ['id'])
op.drop_constraint(None, 'solution_comments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'solution_comments', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('solution_comments_comment_autor_id_fkey'), 'solution_comments', 'users', ['comment_autor_id'], ['id'])
op.create_foreign_key(op.f('solution_comments_solution_id_fkey'), 'solution_comments', 'solutions', ['solution_id'], ['id'])
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
op.drop_constraint(None, 'lessons', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('lessons_course_id_fkey'), 'lessons', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('lessons_creator_id_fkey'), 'lessons', 'users', ['creator_id'], ['id'])
op.drop_constraint(None, 'lesson_files', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('lesson_files_lesson_id_fkey'), 'lesson_files', 'lessons', ['lesson_id'], ['id'])
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
op.drop_constraint(None, 'enrollments', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('enrollments_course_id_fkey'), 'enrollments', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('enrollments_student_id_fkey'), 'enrollments', 'users', ['student_id'], ['id'])
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
op.drop_constraint(None, 'course_teachers', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('course_teachers_course_id_fkey'), 'course_teachers', 'courses', ['course_id'], ['id'])
op.create_foreign_key(op.f('course_teachers_teacher_id_fkey'), 'course_teachers', 'users', ['teacher_id'], ['id'])
op.drop_table('user_check_lessons', schema='public')
# ### end Alembic commands ###

View File

@ -62,3 +62,19 @@ class UserRead(BaseModel):
class Config:
from_attributes = True
class UserCheckLessonBase(BaseModel):
lesson_id: int
user_id: int
class UserCheckLessonCreate(UserCheckLessonBase):
pass
class UserCheckLessonRead(UserCheckLessonBase):
id: int
class Config:
from_attributes = True

View File

@ -21,4 +21,5 @@ from app.domain.models.solutions import Solution
from app.domain.models.statuses import Status
from app.domain.models.task_files import TaskFile
from app.domain.models.tasks import Task
from app.domain.models.user_check_lessons import UserCheckLessons
from app.domain.models.users import User

View File

@ -21,3 +21,4 @@ class Lesson(RootTable):
creator: Mapped['User'] = relationship('User', back_populates='created_lessons', lazy='joined')
files: Mapped[List['LessonFile']] = relationship('LessonFile', back_populates='lesson')
user_check_lessons: Mapped[List['UserCheckLessons']] = relationship('UserCheckLessons', back_populates='lesson')

View File

@ -0,0 +1,14 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.domain.models.base import RootTable
class UserCheckLessons(RootTable):
__tablename__ = 'user_check_lessons'
user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
lesson_id: Mapped[int] = mapped_column(ForeignKey('lessons.id'))
user: Mapped['User'] = relationship('User', back_populates='user_check_lessons')
lesson: Mapped['Lesson'] = relationship('Lesson', back_populates='user_check_lessons')

View File

@ -51,6 +51,11 @@ class User(PhotoAbstract):
'SolutionComment',
back_populates='comment_autor',
)
from app.domain.models.user_check_lessons import UserCheckLessons
user_check_lessons: Mapped[List['UserCheckLessons']] = relationship(
'UserCheckLessons',
back_populates='user',
)
def check_password(self, password):
return check_password_hash(self.password_hash, password)

View File

@ -0,0 +1,102 @@
from typing import Optional, List
from fastapi import HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.courses_repository import CoursesRepository
from app.application.lessons_repository import LessonsRepository
from app.application.solutions_repository import SolutionsRepository
from app.application.tasks_repository import TasksRepository
from app.application.user_check_lessons_repository import UserCheckLessonsRepository
from app.application.users_repository import UsersRepository
from app.domain.entities.users import UserCheckLessonRead, UserCheckLessonCreate
from app.domain.models import UserCheckLessons
class UserCheckLessonsService:
def __init__(self, db: AsyncSession):
self.user_check_lessons_repository = UserCheckLessonsRepository(db)
self.users_repository = UsersRepository(db)
self.lessons_repository = LessonsRepository(db)
self.courses_repository = CoursesRepository(db)
self.tasks_repository = TasksRepository(db)
self.lessons_repository = LessonsRepository(db)
self.solutions_repository = SolutionsRepository(db)
async def get_by_course_id_and_user_id(self, course_id: int, user_id: int) -> Optional[List[UserCheckLessonRead]]:
user = await self.users_repository.get_by_id(user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
course = await self.courses_repository.get_by_id(course_id)
if course is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Курс не найден"
)
checks = await self.user_check_lessons_repository.get_by_course_id_and_user_id(course_id, user_id)
response = []
for check in checks:
response.append(
UserCheckLessonRead.model_validate(check)
)
return response
async def create(self, lesson_id: int, user_id: int) -> None:
user = await self.users_repository.get_by_id(user_id)
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
lesson = await self.lessons_repository.get_by_id(lesson_id)
if lesson is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Лекция не найдена"
)
user_check = await self.user_check_lessons_repository.get_by_user_id_and_lesson_id(
user_id,
lesson_id
)
if user_check is not None:
return
user_check_model = UserCheckLessons(
lesson_id=lesson_id,
user_id=user_id,
)
user_check_model = await self.user_check_lessons_repository.create(user_check_model)
async def calculate_user_progress(self, course_id: int, user_id: int) -> float:
total_lessons = await self.lessons_repository.get_all_by_course(course_id)
total_tasks = await self.tasks_repository.get_all_by_course(course_id)
total_content = len(total_lessons) + len(total_tasks)
if total_content == 0:
return 100.0
passed_lessons = await self.user_check_lessons_repository.get_by_course_id_and_user_id(
course_id=course_id,
user_id=user_id
)
passed_lessons_count = len(passed_lessons)
completed_tasks = await self.solutions_repository.get_completed_tasks_by_course_and_user(
course_id=course_id,
user_id=user_id
)
completed_tasks_count = len(completed_tasks)
completed_total = passed_lessons_count + completed_tasks_count
progress = (completed_total / total_content) * 100
return round(progress, 2)

View File

@ -46,6 +46,27 @@ export const usersApi = createApi({
}),
providesTags: ["user"],
}),
getReadedLessonsByCourse: builder.query({
query: (courseId) => ({
url: `/users/check-my-lessons/${courseId}/`,
method: "GET",
}),
providesTags: ["user"],
}),
setLessonAsReaded: builder.mutation({
query: (lessonId) => ({
url: `/users/check-lesson/${lessonId}/`,
method: "POST",
}),
invalidatesTags: ["user"],
}),
getMyCourseProgress: builder.query({
query: (courseId) => ({
url: `/users/my-progress/${courseId}/`,
method: "GET",
}),
providesTags: ["user"],
}),
}),
});
@ -56,4 +77,7 @@ export const {
useUpdateUserPasswordMutation,
useCreateUserMutation,
useGetUsersByRoleNameQuery,
useGetReadedLessonsByCourseQuery,
useSetLessonAsReadedMutation,
useGetMyCourseProgressQuery,
} = usersApi;

View File

@ -16,7 +16,7 @@ import {
} from "antd";
import {
ArrowLeftOutlined,
BookOutlined,
BookOutlined, CheckCircleFilled, ClockCircleOutlined,
DeleteOutlined,
EditOutlined,
FormOutlined,
@ -55,6 +55,7 @@ const CourseDetailPage = () => {
handleOpenTask,
handleEditTask,
handleDeleteTask,
listReadLessonsIds,
} = useCourseDetailPage(courseId);
if (isLoading) {
@ -177,10 +178,29 @@ const CourseDetailPage = () => {
)}
</Space>
</div>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}>
<Text>
{isLesson ? "Лекционный материал" : "Задание"}
</Text>
<Text>
{isLesson ? "Лекционный материал" : "Задание"}
</Text>
{isLesson && (
listReadLessonsIds.includes(item.id) ? (
<Space style={{color: "#52c41a", fontWeight: 500}}>
<CheckCircleFilled style={{fontSize: 18}}/>
<span>Пройдено</span>
</Space>
) : (
<Space style={{color: "#999"}}>
<ClockCircleOutlined style={{fontSize: 16}}/>
<span>Не прочитано</span>
</Space>
)
)}
</div>
<div
style={{

View File

@ -1,4 +1,8 @@
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {
useGetAuthenticatedUserDataQuery,
useGetReadedLessonsByCourseQuery,
useSetLessonAsReadedMutation
} from "../../../Api/usersApi.js";
import {useGetCourseByIdQuery} from "../../../Api/coursesApi.js";
import {useEffect} from "react";
import {useDispatch} from "react-redux";
@ -107,7 +111,24 @@ const useCourseDetailPage = (courseId) => {
const isTeacherOrAdmin = [CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData?.role?.title);
const handleOpenLesson = (lesson) => {
const [
markLessonAsRead
] = useSetLessonAsReadedMutation();
const markLesson = async (lessonId) => {
try {
await markLessonAsRead(lessonId);
} catch {
notification.error({
title: "Ошибка",
description: "Не удалось отметить лекцию как прочитанную",
placement: "topRight",
})
}
};
const handleOpenLesson = async (lesson) => {
await markLesson(lesson.id)
dispatch(setSelectedLessonToView(lesson))
};
@ -127,6 +148,15 @@ const useCourseDetailPage = (courseId) => {
dispatch(setSelectedTaskToUpdate(task))
};
const {
data: readedLessons = []
} = useGetReadedLessonsByCourseQuery(courseData?.id, {
pollingInterval: 10000,
skip: courseData?.id === null || courseData?.id === undefined
});
const listReadLessonsIds = readedLessons.map((data) => data.lesson_id);
return {
tasksData,
isTeacherOrAdmin,
@ -143,6 +173,7 @@ const useCourseDetailPage = (courseId) => {
handleOpenTask,
handleEditTask,
handleDeleteTask,
listReadLessonsIds,
}
};

View File

@ -8,7 +8,7 @@ import {
Spin,
Tag,
Typography,
Avatar, Result, FloatButton, Tooltip,
Avatar, Result, FloatButton, Tooltip, Progress, Divider,
} from "antd";
import {
PlusOutlined,
@ -20,6 +20,8 @@ import LoadingIndicator from "../../Widgets/LoadingIndicator/LoadingIndicator.js
import CreateCourseModalForm from "./Components/CreateCourseModalForm/CreateCourseModalForm.jsx";
import UpdateCourseModalForm from "./Components/UpdateCourseModalForm/UpdateCourseModalForm.jsx";
import {useNavigate} from "react-router-dom";
import {useEffect, useState} from "react";
import CONFIG from "../../../Core/сonfig.js";
const {Title, Text} = Typography;
@ -35,6 +37,38 @@ const CoursesPage = () => {
openEditModal,
} = useCoursesPage();
const [courseProgress, setCourseProgress] = useState({});
useEffect(() => {
if (courses.length === 0) return;
const token = localStorage.getItem("access_token");
if (!token) return;
const fetchProgress = async () => {
const progress = {};
for (const course of courses) {
try {
const res = await fetch(`${CONFIG.BASE_URL}/users/my-progress/${course.id}/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.ok) {
const data = await res.json();
progress[course.id] = data; // data это число, например 87.5
}
} catch (err) {
console.error("Progress fetch error for course", course.id);
}
}
setCourseProgress(progress);
};
fetchProgress();
}, [courses]);
if (isLoading) {
return (
<LoadingIndicator/>
@ -119,6 +153,11 @@ const CoursesPage = () => {
</Avatar>
))}
</Avatar.Group>
<Divider/>
<Text type="secondary">Прогресс:</Text>
{courseProgress[course.id] !== undefined && (
<Progress percent={courseProgress[course.id]} size={[300, 20]}/>
)}
</div>
)}
</Card>

View File

@ -1,4 +1,4 @@
import {useGetAuthenticatedUserDataQuery} from "../../../Api/usersApi.js";
import {useGetAuthenticatedUserDataQuery, useGetMyCourseProgressQuery} from "../../../Api/usersApi.js";
import {useGetAllCoursesQuery, useGetAllMyCoursesQuery} from "../../../Api/coursesApi.js";
import CONFIG from "../../../Core/сonfig.js";
import {ROLES} from "../../../Core/constants.js";