diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index c3c94084a..d8c73cfa8 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -275,7 +275,8 @@ public async Task GetMentorWorkspace(long courseId, string mentor var workspace = new WorkspaceViewModel { Homeworks = mentorCourseView.Value.Homeworks, - Students = students.OrderBy(x => x.Surname).ThenBy(x => x.Name).ToArray() + Students = students.OrderBy(x => x.Surname).ThenBy(x => x.Name).ToArray(), + Groups = mentorCourseView.Value.Groups, }; return Ok(workspace); } diff --git a/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteExpertViewModel.cs b/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteExpertViewModel.cs index ba6d40d25..222cb1319 100644 --- a/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteExpertViewModel.cs +++ b/HwProj.Common/HwProj.Models/AuthService/ViewModels/InviteExpertViewModel.cs @@ -10,6 +10,8 @@ public class InviteExpertViewModel public long CourseId { get; set; } + public List GroupIds { get; set; } = new List(); + public List StudentIds { get; set; } = new List(); public List HomeworkIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs index b1612eabe..135b859b4 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs @@ -8,6 +8,8 @@ public class CreateCourseFilterModel public long CourseId { get; set; } + public List GroupIds { get; set; } = new List(); + public List StudentIds { get; set; } = new List(); public List HomeworkIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/CourseFilterDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/CourseFilterDTO.cs index 8b456d742..4eb0612a1 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/CourseFilterDTO.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/CourseFilterDTO.cs @@ -4,6 +4,8 @@ namespace HwProj.Models.CoursesService.DTO { public class CourseFilterDTO { + public List GroupIds { get; set; } = new List(); + public List StudentIds { get; set; } = new List(); public List HomeworkIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs index a0838be69..165a006fa 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs @@ -6,6 +6,8 @@ public class CreateCourseFilterDTO { public string Id { get; set; } + public List GroupIds { get; set; } = new List(); + public List StudentIds { get; set; } = new List(); public List HomeworkIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/EditMentorWorkspaceDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/EditMentorWorkspaceDTO.cs index f9fc7b17c..402485f47 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/EditMentorWorkspaceDTO.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/EditMentorWorkspaceDTO.cs @@ -4,6 +4,8 @@ namespace HwProj.Models.CoursesService.DTO { public class EditMentorWorkspaceDTO { + public List GroupIds { get; set; } = new List(); + public List StudentIds { get; set; } = new List(); public List HomeworkIds { get; set; } = new List(); diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/WorkspaceViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/WorkspaceViewModel.cs index 795374fcc..50c02ffe1 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/WorkspaceViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/WorkspaceViewModel.cs @@ -5,6 +5,8 @@ namespace HwProj.Models.CoursesService.ViewModels { public class WorkspaceViewModel { + public GroupViewModel[] Groups { get; set; } + public AccountDataDto[] Students { get; set; } public HomeworkViewModel[] Homeworks { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index d248e5098..a5350e652 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -1,6 +1,8 @@ using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using AutoMapper; +using HwProj.Common.Net8; using HwProj.CoursesService.API.Filters; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Services; @@ -15,11 +17,19 @@ public class CourseGroupsController : Controller { private readonly IGroupsService _groupsService; private readonly IMapper _mapper; + private readonly ICourseFilterService _courseFilterService; + private readonly ICoursesService _coursesService; - public CourseGroupsController(IMapper mapper, IGroupsService groupsService) + public CourseGroupsController( + IMapper mapper, + IGroupsService groupsService, + ICourseFilterService courseFilterService, + ICoursesService coursesService) { _mapper = mapper; _groupsService = groupsService; + _courseFilterService = courseFilterService; + _coursesService = coursesService; } [HttpGet("{courseId}/getAll")] @@ -47,6 +57,15 @@ public async Task CreateGroup([FromBody] CreateGroupViewModel gro GroupMates = groupViewModel.GroupMatesIds.Select(t => new GroupMate() { StudentId = t }).ToList() }; var id = await _groupsService.AddGroupAsync(group); + + var userId = Request.GetUserIdFromHeader(); + var courseMentorIds = await _coursesService.GetCourseLecturers(groupViewModel.CourseId); + if (userId != null && courseMentorIds.Contains(userId)) + await _courseFilterService.AddToFilter(groupViewModel.CourseId, userId, new Filter + { + GroupIds = new List { id } + }); + return Ok(id); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs index dbbdb1ba6..0eddddd25 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/HomeworksController.cs @@ -1,7 +1,9 @@ using System.Threading.Tasks; using HwProj.CoursesService.API.Domains; using HwProj.CoursesService.API.Filters; +using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Services; +using HwProj.Common.Net8; using HwProj.Models.CoursesService.ViewModels; using Microsoft.AspNetCore.Mvc; using System.Linq; @@ -13,10 +15,12 @@ namespace HwProj.CoursesService.API.Controllers public class HomeworksController : Controller { private readonly IHomeworksService _homeworksService; + private readonly ICourseFilterService _courseFilterService; - public HomeworksController(IHomeworksService homeworksService) + public HomeworksController(IHomeworksService homeworksService, ICourseFilterService courseFilterService) { _homeworksService = homeworksService; + _courseFilterService = courseFilterService; } [HttpPost("{courseId}/add")] @@ -28,6 +32,13 @@ public async Task AddHomework(long courseId, if (validationResult.Any()) return BadRequest(validationResult); var newHomework = await _homeworksService.AddHomeworkAsync(courseId, homeworkViewModel); + var mentorId = Request.GetUserIdFromHeader(); + if (mentorId != null) + await _courseFilterService.AddToFilter(courseId, mentorId, new Filter + { + HomeworkIds = new System.Collections.Generic.List { newHomework.Id } + }); + return Ok(newHomework.ToHomeworkViewModel()); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Filter.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Filter.cs index 46a1eb4b8..edc0ba632 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Filter.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Filter.cs @@ -5,6 +5,9 @@ namespace HwProj.CoursesService.API.Models { public class Filter { + [JsonProperty(PropertyName = "GRP")] + public List GroupIds { get; set; } + [JsonProperty(PropertyName = "STUD")] public List StudentIds { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index e5e51dfc8..9de5a55ad 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -22,11 +22,13 @@ public class CourseFilterService : ICourseFilterService { private const string GlobalFilterId = ""; private readonly ICourseFilterRepository _courseFilterRepository; + private readonly IGroupsService _groupsService; public CourseFilterService( - ICourseFilterRepository courseFilterRepository) + ICourseFilterRepository courseFilterRepository, IGroupsService groupsService) { _courseFilterRepository = courseFilterRepository; + _groupsService = groupsService; } public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterModel courseFilterModel) @@ -130,15 +132,41 @@ public async Task ApplyFilter(CourseDTO course, string userId) public async Task GetAssignedStudentsIds(long courseId, string[] mentorsIds) { var usersCourseFilters = await _courseFilterRepository.GetAsync(mentorsIds, courseId); + if (usersCourseFilters == null || usersCourseFilters.Count == 0) + return Array.Empty(); - return usersCourseFilters - .Where(u => u.CourseFilter.Filter.HomeworkIds.Count == 0) - .Select(u => new MentorToAssignedStudentsDTO + var groupIds = usersCourseFilters + .SelectMany(filter => filter.CourseFilter.Filter.GroupIds ?? Enumerable.Empty()) + .Distinct() + .ToArray(); + + var groups = await _groupsService.GetGroupsAsync(groupIds); + var groupToStudentIds = groups + .ToDictionary( + g => g.Id, + g => g.GroupMates?.Select(gm => gm.StudentId).ToArray() ?? Array.Empty() + ); + + var result = usersCourseFilters + .Select(u => { - MentorId = u.Id, - SelectedStudentsIds = u.CourseFilter.Filter.StudentIds + var directStudents = u.CourseFilter.Filter.StudentIds ?? new List() {}; + var groupIdsForMentor = u.CourseFilter.Filter.GroupIds ?? Enumerable.Empty(); + var studentsFromGroups = groupIdsForMentor + .Where(gid => groupToStudentIds.ContainsKey(gid)) + .SelectMany(gid => groupToStudentIds[gid]) + .Distinct() + .ToList(); + + return new MentorToAssignedStudentsDTO + { + MentorId = u.Id, + SelectedStudentsIds = directStudents.Concat(studentsFromGroups).Distinct().ToList() + }; }) .ToArray(); + + return result; } private async Task AddCourseFilter(Filter filter, long courseId, string userId) @@ -176,6 +204,34 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit } : editingCourseDto.Homeworks; + var groups = filter.GroupIds.Any() + ? editingCourseDto.Groups.Where(g => filter.GroupIds.Contains(g.Id)) + : editingCourseDto.Groups; + + var filteredStudentIds = filter.GroupIds.Any() + ? filter.StudentIds.Concat(groups.SelectMany(g => g.StudentsIds)) + : filter.StudentIds.Any() + ? filter.StudentIds + : editingCourseDto.AcceptedStudents.Select(st => st.StudentId); + + var filteredGroups = filteredStudentIds.Any() + ? editingCourseDto.Groups + .Select(gs => + { + var groupStudentsIds = gs.StudentsIds.Intersect(filteredStudentIds).ToArray(); + return groupStudentsIds.Any() + ? new GroupViewModel + { + Id = gs.Id, + Name = gs.Name, + StudentsIds = groupStudentsIds + } + : null; + }) + .Where(t => t != null) + .ToArray() + : editingCourseDto.Groups; + return new CourseDTO { Id = editingCourseDto.Id, @@ -184,32 +240,21 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit IsCompleted = editingCourseDto.IsCompleted, IsOpen = editingCourseDto.IsOpen, InviteCode = editingCourseDto.InviteCode, - Groups = - (filter.StudentIds.Any() - ? editingCourseDto.Groups.Select(gs => - { - var filteredStudentsIds = gs.StudentsIds.Intersect(filter.StudentIds).ToArray(); - return filteredStudentsIds.Any() - ? new GroupViewModel - { - Id = gs.Id, - Name = gs.Name, - StudentsIds = filteredStudentsIds - } - : null; - }) - .Where(t => t != null) - .ToArray() - : editingCourseDto.Groups)!, + Groups = filter.GroupIds.Any() + ? filteredGroups + .Where(g => filter.GroupIds.Contains(g.Id) || g.Name == string.Empty) + .ToArray() + : filteredGroups, MentorIds = filter.MentorIds.Any() ? editingCourseDto.MentorIds.Intersect(filter.MentorIds).ToArray() : editingCourseDto.MentorIds, - CourseMates = - filter.StudentIds.Any() - ? editingCourseDto.CourseMates - .Where(mate => !mate.IsAccepted || filter.StudentIds.Contains(mate.StudentId)).ToArray() - : editingCourseDto.CourseMates, - Homeworks = homeworks.OrderBy(hw => hw.PublicationDate).ToArray() + CourseMates = editingCourseDto.CourseMates + .Where(mate => !mate.IsAccepted || filteredStudentIds.Contains(mate.StudentId)) + .ToArray(), + Homeworks = homeworks + .Where(hw => hw.GroupId == null || groups.Any(g => g.Id == hw.GroupId)) + .OrderBy(hw => hw.PublicationDate) + .ToArray() }; } @@ -225,7 +270,8 @@ public async Task UpdateGroupFilters(long courseId, long homeworkId, Group group { var existingCourseFilter = filters.SingleOrDefault(f => f.Id == filterId)?.CourseFilter; var newFilter = existingCourseFilter?.Filter - ?? new Filter { StudentIds = new List(), HomeworkIds = new List(), MentorIds = new List() }; + ?? new Filter { GroupIds = new List(), StudentIds = new List(), + HomeworkIds = new List(), MentorIds = new List() }; newFilter.HomeworkIds.Add(homeworkId); if (existingCourseFilter != null) @@ -234,5 +280,42 @@ public async Task UpdateGroupFilters(long courseId, long homeworkId, Group group await AddCourseFilter(newFilter, courseId, filterId); } } + + public async Task AddToFilter(long courseId, string userId, Filter filter) + { + var existingCourseFilter = await _courseFilterRepository.GetAsync(userId, courseId); + if (existingCourseFilter?.Filter is null) + return; + + var targetFilter = existingCourseFilter.Filter.FillEmptyFields(); + filter.FillEmptyFields(); + + var hasChanges = + AddToFilterList(targetFilter.GroupIds, filter.GroupIds) + | AddToFilterList(targetFilter.StudentIds, filter.StudentIds) + | AddToFilterList(targetFilter.HomeworkIds, filter.HomeworkIds) + | AddToFilterList(targetFilter.MentorIds, filter.MentorIds); + + if (hasChanges) + await UpdateAsync(existingCourseFilter.Id, targetFilter); + } + + private static bool AddToFilterList(List target, IEnumerable itemsToAdd) + { + if (!target.Any()) + return false; + + var hasChanges = false; + foreach (var item in itemsToAdd.Distinct()) + { + if (target.Contains(item)) + continue; + + target.Add(item); + hasChanges = true; + } + + return hasChanges; + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterUtils.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterUtils.cs index 9e02c9cef..fc39da3c9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterUtils.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterUtils.cs @@ -10,6 +10,7 @@ public static Filter CreateFilter(CreateCourseFilterModel courseFilterModel) { return new Filter { + GroupIds = courseFilterModel.GroupIds, HomeworkIds = courseFilterModel.HomeworkIds, MentorIds = courseFilterModel.MentorIds, StudentIds = courseFilterModel.StudentIds @@ -18,6 +19,7 @@ public static Filter CreateFilter(CreateCourseFilterModel courseFilterModel) public static Filter FillEmptyFields(this Filter filter) { + filter.GroupIds ??= new List(); filter.StudentIds ??= new List(); filter.HomeworkIds ??= new List(); filter.MentorIds ??= new List(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs index 0d49f1805..f4b3f1c5b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Models.CoursesService; @@ -16,5 +15,6 @@ public interface ICourseFilterService Task ApplyFilter(CourseDTO courseDto, string userId); Task GetAssignedStudentsIds(long courseId, string[] mentorsIds); Task UpdateGroupFilters(long courseId, long homeworkId, Group group); + Task AddToFilter(long courseId, string userId, Filter filter); } } \ No newline at end of file diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index e42934f1c..c6b075094 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -756,6 +756,12 @@ export interface EditMentorWorkspaceDTO { * @memberof EditMentorWorkspaceDTO */ homeworkIds?: Array; + /** + * + * @type {Array} + * @memberof EditMentorWorkspaceDTO + */ + groupIds?: Array; } /** * @@ -2939,6 +2945,12 @@ export interface WorkspaceViewModel { * @memberof WorkspaceViewModel */ homeworks?: Array; + /** + * + * @type {Array} + * @memberof WorkspaceViewModel + */ + groups?: Array; } /** * AccountApi - fetch parameter creator diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 62d867337..9fcdb9ad0 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -20,10 +20,9 @@ interface GroupSelectorProps { courseStudents: AccountDataDto[], groups: GroupViewModel[], onGroupIdChange: (groupId?: number) => void, - onGroupsUpdate: () => void, + onGroupsUpdate: () => Promise, selectedGroupId?: number, choiceDisabled?: boolean, - onCreateNewGroup?: () => void, } const GroupSelector: FC = (props) => { @@ -80,14 +79,14 @@ const GroupSelector: FC = (props) => { groupMates: formState.memberIds.map(studentId => ({studentId})), } ); - props.onGroupsUpdate(); + await props.onGroupsUpdate(); } else { const groupId = await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(props.courseId, { name: formState.name.trim(), groupMatesIds: formState.memberIds, courseId: props.courseId, }); - props.onGroupsUpdate(); + await props.onGroupsUpdate(); props.onGroupIdChange(groupId); } } catch (error) { diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index c33122808..076fd7273 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import {FC, useEffect, useState, useMemo} from "react"; import {useNavigate, useParams, useSearchParams} from "react-router-dom"; -import {AccountDataDto, CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -49,7 +49,6 @@ interface ICourseState { isFound: boolean; course: CourseViewModel; courseHomeworks: HomeworkViewModel[]; - groups: GroupViewModel[]; mentors: AccountDataDto[]; acceptedStudents: AccountDataDto[]; newStudents: AccountDataDto[]; @@ -71,7 +70,6 @@ const Course: React.FC = () => { course: {}, courseHomeworks: [], mentors: [], - groups: [], acceptedStudents: [], newStudents: [], studentSolutions: [], @@ -90,17 +88,8 @@ const Course: React.FC = () => { newStudents, acceptedStudents, courseHomeworks, - groups } = courseState - const loadGroups = async () => { - const groups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroups(course.id!) - setCourseState(prevState => ({ - ...prevState, - groups: groups - })) - }; - const userId = ApiSingleton.authService.getUserId() const isLecturer = ApiSingleton.authService.isLecturer() @@ -153,12 +142,20 @@ const Course: React.FC = () => { courseHomeworks: course.homeworks!, createHomework: false, mentors: course.mentors!, - groups: course.groups || [], acceptedStudents: course.acceptedStudents!, newStudents: course.newStudents!, })) } + const updateCourseGroups = async () => { + const course = await ApiSingleton.coursesApi.coursesGetCourseData(+courseId!) + + setCourseState(prevState => ({ + ...prevState, + course: course, + })) + } + useEffect(() => { setCurrentState() }, []) @@ -187,9 +184,9 @@ const Course: React.FC = () => { const [lecturerStatsState, setLecturerStatsState] = useState(false); const studentsWithoutGroup = useMemo(() => { - const inGroupIds = new Set(groups.flatMap(g => g.studentsIds)); + const inGroupIds = new Set(course.groups?.flatMap(g => g.studentsIds) || []); return acceptedStudents.filter(s => !inGroupIds.has(s.userId!)); - }, [groups, acceptedStudents]); + }, [course.groups, acceptedStudents]); const CourseMenu: FC = () => { const [anchorEl, setAnchorEl] = React.useState(null); @@ -319,7 +316,7 @@ const Course: React.FC = () => { } - {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && + {isCourseMentor && course.groups?.length !== 0 && studentsWithoutGroup.length > 0 && { courseHomeworks: homeworks })) }} - onGroupsUpdate={loadGroups} - groups={groups} + onGroupsUpdate={updateCourseGroups} + groups={course.groups ?? []} /> } {tabValue === "stats" && @@ -416,7 +413,7 @@ const Course: React.FC = () => { isMentor={isCourseMentor} course={courseState.course} solutions={studentSolutions} - groups={groups} + groups={course.groups ?? []} /> } diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index d16fbb3a1..bdc184957 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -61,7 +61,7 @@ interface ICourseExperimentalProps { previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; - onGroupsUpdate: () => void; + onGroupsUpdate: () => Promise; groups: GroupViewModel[]; } @@ -428,6 +428,7 @@ export const CourseExperimental: FC = (props) => { getAllHomeworks={() => homeworks} homeworkAndFilesInfo={{homework, filesInfo}} isMentor={isMentor} + userId={props.userId} initialEditMode={initialEditMode || homeworkEditMode} onMount={onSelectedItemMount} onAddTask={addNewTask} diff --git a/hwproj.front/src/components/Courses/CourseFilter.tsx b/hwproj.front/src/components/Courses/CourseFilter.tsx index 04d2fea2c..6319c600e 100644 --- a/hwproj.front/src/components/Courses/CourseFilter.tsx +++ b/hwproj.front/src/components/Courses/CourseFilter.tsx @@ -1,5 +1,5 @@ import React, {FC, useEffect, useState} from 'react'; -import {HomeworkViewModel, AccountDataDto, MentorToAssignedStudentsDTO} from '../../api'; +import {HomeworkViewModel, AccountDataDto, MentorToAssignedStudentsDTO, GroupViewModel } from '../../api'; import Grid from "@material-ui/core/Grid"; import {Autocomplete, Chip, Stack, Typography} from "@mui/material"; import TextField from "@material-ui/core/TextField"; @@ -13,6 +13,7 @@ interface ICourseFilterProps { mentorId: string; onSelectedHomeworksChange: (homeworks: HomeworkViewModel[]) => void; onSelectedStudentsChange: (students: AccountDataDto[]) => void; + onSelectedGroupsChange: (groups: GroupViewModel[]) => void; onWorkspaceInitialize: (success: boolean, errors?: string[]) => void; isStudentsSelectionHidden: boolean; } @@ -20,8 +21,10 @@ interface ICourseFilterProps { interface ICourseFilterState { courseHomeworks: HomeworkViewModel[]; courseStudents: AccountDataDto[]; + courseGroups: GroupViewModel[]; selectedHomeworks: HomeworkViewModel[]; selectedStudents: AccountDataDto[]; + selectedGroups: GroupViewModel[]; mentors: AccountDataDto[]; assignedStudents: MentorToAssignedStudentsDTO[] } @@ -31,8 +34,10 @@ const CourseFilter: FC = (props) => { const [state, setState] = useState({ courseHomeworks: [], courseStudents: [], + courseGroups: [], selectedHomeworks: [], selectedStudents: [], + selectedGroups: [], assignedStudents: [], mentors: [] }); @@ -43,6 +48,9 @@ const CourseFilter: FC = (props) => { // Состояние для отображения поля выбора студентов const [isStudentsSelectionHidden, setIsStudentsSelectionHidden] = useState(props.isStudentsSelectionHidden); + const isAccountDataDto = (obj: any): obj is AccountDataDto => 'userId' in obj; + const isGroupViewModel = (obj: any): obj is GroupViewModel => 'id' in obj && 'name' in obj; + useEffect(() => { const fetchCourseDataForMentor = async () => { try { @@ -55,22 +63,38 @@ const CourseFilter: FC = (props) => { const mentorWorkspace = await ApiSingleton.coursesApi.coursesGetMentorWorkspace(props.courseId, props.mentorId); - props.onSelectedStudentsChange(mentorWorkspace.students ?? []) - props.onSelectedHomeworksChange(mentorWorkspace.homeworks ?? []) + const courseGroups = course.groups?.filter(g => g.name?.trim()) ?? []; + const selectedGroups = (mentorWorkspace.groups?.length === courseGroups.length + ? [] + : mentorWorkspace.groups ?? [] + ) + .filter(g => g.name?.trim()); - // Для корректного отображения "Все" при инцициализации (получении данных с бэкенда) + const selectedGroupsStudents = selectedGroups.flatMap(g => g.studentsIds ?? []); + const selectedStudentsWithoutGroups = mentorWorkspace.students + ?.filter(st => !selectedGroupsStudents.includes(st.userId!)) ?? []; const allCourseStudentsCount = (course.acceptedStudents?.length ?? 0) + (course.newStudents?.length ?? 0); - const initSelectedStudentsView = mentorWorkspace.students?.length === allCourseStudentsCount ? - [] : (mentorWorkspace.students) ?? []; - const initSelectedHomeworksView = mentorWorkspace.homeworks?.length === course.homeworks?.length ? - [] : (mentorWorkspace.homeworks ?? []); + const selectedStudents = selectedStudentsWithoutGroups.length === allCourseStudentsCount + ? [] + : selectedStudentsWithoutGroups; + + const availableHomeworks = course.homeworks + ?.filter(h => + !h.groupId + || selectedGroups.length === 0 + || selectedGroups.some(g => g.id === h.groupId)); + const selectedHomeworks = mentorWorkspace.homeworks?.length === availableHomeworks?.length + ? [] + : mentorWorkspace.homeworks ?? []; setState(prevState => ({ ...prevState, courseHomeworks: course.homeworks ?? [], courseStudents: course.acceptedStudents ?? [], - selectedStudents: initSelectedStudentsView, - selectedHomeworks: initSelectedHomeworksView, + courseGroups, + selectedHomeworks, + selectedStudents, + selectedGroups, mentors: course.mentors!, assignedStudents: assignedStudents.filter(x => x.mentorId !== props.mentorId) })) @@ -99,6 +123,10 @@ const CourseFilter: FC = (props) => { props.onSelectedHomeworksChange(state.selectedHomeworks) }, [state.selectedHomeworks]); + useEffect(() => { + props.onSelectedGroupsChange(state.selectedGroups) + }, [state.selectedGroups]); + //TODO: memoize? const getAssignedMentors = (studentId: string) => state.assignedStudents @@ -131,7 +159,11 @@ const CourseFilter: FC = (props) => { + !h.groupId + || state.selectedGroups.length === 0 + || state.selectedGroups.some(g => g.id === h.groupId) + )} getOptionLabel={(option: HomeworkViewModel) => option.title ?? "Без названия"} getOptionKey={(option: HomeworkViewModel) => option.id ?? 0} filterSelectedOptions @@ -169,37 +201,118 @@ const CourseFilter: FC = (props) => { { - const assignedMentors = getAssignedMentors(option.userId!) - const suffix = assignedMentors.length > 0 ? " — преподаватель " + assignedMentors[0] + "" : "" - return option.surname + ' ' + option.name + suffix; + options={(() => { + const availableStudents = state.courseStudents.filter( + s => !state.selectedGroups.some(g => g.studentsIds?.includes(s.userId!)) + ); + return [...state.courseGroups, ...availableStudents]; + })()} + getOptionKey={(option) => { + if (isAccountDataDto(option)) return option.userId ?? ''; + return option.id?.toString() ?? ''; }} - getOptionKey={(option: AccountDataDto) => option.userId ?? ""} filterSelectedOptions - isOptionEqualToValue={(option, value) => option.userId === value.userId} - renderInput={(params) => ( - )} - renderTags={(value, getTagProps) => - value.map((option, index) => - ) - } - noOptionsText={'Больше нет студентов для выбора'} - value={state.selectedStudents} + isOptionEqualToValue={(option, value) => { + if (isAccountDataDto(option) && isAccountDataDto(value)) { + return option.userId === value.userId; + } + if (isGroupViewModel(option) && isGroupViewModel(value)) { + return option.id === value.id; + } + return false; + }} + renderInput={(params) => { + const totalSelectedStudents = + state.selectedStudents.length + + [...new Set(state.selectedGroups.flatMap(g => g.studentsIds))].length; + + return ( + + ); + }} + renderTags={(value, getTagProps) => ( + <> + {value.map((option, index) => { + // Исключаем поле key из пропсов, если оно там есть + const { key: _key, ...chipProps } = getTagProps({ index }); + + if (isAccountDataDto(option)) { + return ( + + ); + } else { + return ( + + ); + } + })} + + )} + renderOption={(props, option) => { + if(isGroupViewModel(option)) { + return ( +
  • + {option.name} +
  • + ); + } else { + const assignedMentors = getAssignedMentors(option.userId!); + const suffix = assignedMentors.length > 0 ? ` — преподаватель ${assignedMentors[0]}` : ''; + return ( +
  • + {option.surname} {option.name}{suffix} +
  • + ); + } + }} + noOptionsText="Больше нет студентов для выбора" + value={[...state.selectedStudents, ...state.selectedGroups]} onChange={(_, values) => { - setState((prevState) => ({ - ...prevState, - selectedStudents: values - })); + const newGroups = new Set(); + const groupStudentIds = new Set(); + + // Сначала собираем все группы и id их студентов + for (const item of values) { + if (isGroupViewModel(item) && item.studentsIds) { + item.studentsIds?.forEach(sid => groupStudentIds.add(sid)); + newGroups.add(item); + } + } + + // Добавляем только студентов, не входящих ни в одну из выбранных групп + const newStudents = new Array(); + for (const item of values) { + if (isAccountDataDto(item) && item.userId && !groupStudentIds.has(item.userId)) { + newStudents.push(item); + } + } + + const selectedGroups = [...newGroups]; + setState((prev) => ({ + ...prev, + selectedStudents: newStudents, + selectedGroups, + selectedHomeworks: prev.selectedHomeworks + .filter(h => + !h.groupId + || selectedGroups.length === 0 + || selectedGroups.some(g => g.id === h.groupId)), + })) }} /> {studentsWithMultipleReviewers.size > 0 && @@ -209,7 +322,6 @@ const CourseFilter: FC = (props) => { - )} )} @@ -217,4 +329,4 @@ const CourseFilter: FC = (props) => { ) } -export default CourseFilter; \ No newline at end of file +export default CourseFilter; diff --git a/hwproj.front/src/components/Courses/MentorWorkspaceModal.tsx b/hwproj.front/src/components/Courses/MentorWorkspaceModal.tsx index 772ebb29c..5611449f7 100644 --- a/hwproj.front/src/components/Courses/MentorWorkspaceModal.tsx +++ b/hwproj.front/src/components/Courses/MentorWorkspaceModal.tsx @@ -7,7 +7,7 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import ApiSingleton from "../../api/ApiSingleton"; import Typography from "@material-ui/core/Typography"; import Grid from '@material-ui/core/Grid'; -import {HomeworkViewModel, AccountDataDto, EditMentorWorkspaceDTO} from "../../api"; +import {HomeworkViewModel, AccountDataDto, EditMentorWorkspaceDTO, GroupViewModel} from "../../api"; import {Alert} from "@mui/material"; import ErrorsHandler from "../Utils/ErrorsHandler"; import {Snackbar} from "@material-ui/core"; @@ -25,6 +25,7 @@ interface MentorWorkspaceProps { interface MentorWorkspaceState { selectedHomeworks: HomeworkViewModel[]; selectedStudents: AccountDataDto[]; + selectedGroups: GroupViewModel[]; errors: string[]; } @@ -32,6 +33,7 @@ const MentorWorkspaceModal: FC = (props) => { const [state, setState] = useState({ selectedHomeworks: [], selectedStudents: [], + selectedGroups: [], errors: [] }); @@ -43,12 +45,13 @@ const MentorWorkspaceModal: FC = (props) => { const [isWorkspaceUpdated, setIsWorkspaceUpdated] = useState(false); - // Если преподаватель не выбрал ни одного студента, по умолчанию регистрируем всех. Аналогично с выбором домашних работ + // Если преподаватель не выбрал ни одного студента, по умолчанию регистрируем всех. Аналогично с выбором домашних работ и групп const handleWorkspaceChanges = async () => { try { const workspaceViewModel: EditMentorWorkspaceDTO = { homeworkIds: state.selectedHomeworks.map(homeworkViewModel => homeworkViewModel.id!), - studentIds: state.selectedStudents.map(accountData => accountData.userId!) + studentIds: state.selectedStudents.map(accountData => accountData.userId!), + groupIds: state.selectedGroups.map(groupViewModel => groupViewModel.id!) } await ApiSingleton.coursesApi.coursesEditMentorWorkspace( @@ -98,6 +101,12 @@ const MentorWorkspaceModal: FC = (props) => { selectedStudents: students })) } + onSelectedGroupsChange={(groups) => + setState(prevState => ({ + ...prevState, + selectedGroups: groups + })) + } onWorkspaceInitialize={(success, errors) => { if (!success) { setState(prevState => ({ diff --git a/hwproj.front/src/components/Experts/InviteModal.tsx b/hwproj.front/src/components/Experts/InviteModal.tsx index 0fe1d93e8..d92934a64 100644 --- a/hwproj.front/src/components/Experts/InviteModal.tsx +++ b/hwproj.front/src/components/Experts/InviteModal.tsx @@ -218,6 +218,12 @@ const InviteExpertModal: FC = (props) => { selectedStudents: students })) } + onSelectedGroupsChange={(groups) => + setState(prevState => ({ + ...prevState, + selectedGroups: groups + })) + } onWorkspaceInitialize={(success, errors) => { if (!success) { setState(prevState => ({ diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index ea6486fbe..23a4a36bd 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -21,7 +21,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, GroupViewModel + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, GroupViewModel, } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -61,6 +61,7 @@ interface IEditHomeworkState { const CourseHomeworkEditor: FC<{ homeworkAndFilesInfo: HomeworkAndFilesInfo, + mentorId: string, getAllHomeworks: () => HomeworkViewModel[], onUpdate: (update: { homework: HomeworkViewModel } & { isDeleted?: boolean, @@ -71,7 +72,7 @@ const CourseHomeworkEditor: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; - onGroupsUpdate: () => void; + onGroupsUpdate: () => Promise; groups: GroupViewModel[]; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework @@ -131,10 +132,10 @@ const CourseHomeworkEditor: FC<{ useEffect(() => { const loadCourseStudents = async () => { try { - const courseData = await ApiSingleton.coursesApi.coursesGetAllCourseData(courseId) + const courseData = await ApiSingleton.coursesApi.coursesGetAllCourseData(courseId); setCourseStudents(courseData.course?.acceptedStudents || []) } catch (error) { - console.error('Failed to load course students:', error) + console.error('Failed to load course data:', error) } } loadCourseStudents() @@ -471,6 +472,7 @@ const CourseHomeworkExperimental: FC<{ homeworkAndFilesInfo: HomeworkAndFilesInfo, getAllHomeworks: () => HomeworkViewModel[], isMentor: boolean, + userId: string, initialEditMode: boolean, onMount: () => void, onUpdate: (x: { homework: HomeworkViewModel } & { @@ -483,7 +485,7 @@ const CourseHomeworkExperimental: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; - onGroupsUpdate: () => void; + onGroupsUpdate: () => Promise; groups: GroupViewModel[]; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo @@ -501,6 +503,7 @@ const CourseHomeworkExperimental: FC<{ if (editMode) return { if (update.isSaved) setEditMode(false) props.onUpdate(update)