From 3b51ebe5681c1db69a4a269caa843d7e3ca3ef0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 2 Mar 2026 22:15:45 +0300 Subject: [PATCH 001/100] feat: add tab for course groups --- hwproj.front/src/components/Courses/Course.tsx | 17 ++++++++++++++--- .../src/components/Courses/CourseGroups.tsx | 10 ++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 hwproj.front/src/components/Courses/CourseGroups.tsx diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 5440ded7c..0915f4eeb 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -34,11 +34,12 @@ import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import CourseGroups from "./CourseGroups"; -type TabValue = "homeworks" | "stats" | "applications" +type TabValue = "homeworks" | "stats" | "applications" | "groups" function isAcceptableTabValue(str: string): str is TabValue { - return str === "homeworks" || str === "stats" || str === "applications"; + return str === "homeworks" || str === "stats" || str === "applications" || str === "groups"; } interface ICourseState { @@ -302,12 +303,13 @@ const Course: React.FC = () => { style={{marginBottom: 10}} variant="scrollable" scrollButtons={"auto"} - value={tabValue === "homeworks" ? 0 : tabValue === "stats" ? 1 : 2} + value={tabValue === "homeworks" ? 0 : tabValue === "stats" ? 1 : tabValue === "applications" ? 2 : 3} indicatorColor="primary" onChange={(event, value) => { if (value === 0 && !isExpert) navigate(`/courses/${courseId}/homeworks`) if (value === 1) navigate(`/courses/${courseId}/stats`) if (value === 2 && !isExpert) navigate(`/courses/${courseId}/applications`) + if (value === 3) navigate(`/courses/${courseId}/groups`) }} > {!isExpert && @@ -325,6 +327,12 @@ const Course: React.FC = () => { }/>} + {isCourseMentor && +
Группы
+ + }/>} {tabValue === "homeworks" && { courseId={courseId!} /> } + {tabValue === "groups" && isCourseMentor && + + } ); diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx new file mode 100644 index 000000000..75af2dc49 --- /dev/null +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -0,0 +1,10 @@ +import {FC} from "react"; +import {Card, CardContent, CardActions, Grid, Button, Typography, Alert, AlertTitle} from '@mui/material'; + +interface ICourseGroupsProps {} + +const CourseGroups: FC = (props) => { + return +} + +export default CourseGroups; From 15a56185567c526ce71a44d2faa22d19b4e6e9c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Wed, 4 Mar 2026 22:09:15 +0300 Subject: [PATCH 002/100] feat: add groups with names get method --- .../Controllers/CourseGroupsController.cs | 11 +++ .../CoursesService/DTO/GroupWithNameDTO.cs | 9 ++ .../Controllers/CourseGroupsController.cs | 17 +++- .../CoursesServiceClient.cs | 10 ++ .../ICoursesServiceClient.cs | 1 + hwproj.front/src/api/api.ts | 93 +++++++++++++++++++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs index 4d1bbd78f..7225b64d7 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using HwProj.CoursesService.Client; using HwProj.Models.CoursesService.ViewModels; +using HwProj.Models.CoursesService.DTO; using HwProj.Models.Roles; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -30,6 +31,16 @@ public async Task GetAllCourseGroups(long courseId) : Ok(result); } + [HttpGet("{courseId}/getAllWithNames")] + [ProducesResponseType(typeof(GroupWithNameDTO[]), (int)HttpStatusCode.OK)] + public async Task GetAllCourseGroupsWithNames(long courseId) + { + var result = await _coursesClient.GetAllCourseGroupsWithNames(courseId); + return result == null + ? NotFound() + : Ok(result); + } + [HttpPost("{courseId}/create")] [Authorize(Roles = Roles.LecturerRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs new file mode 100644 index 000000000..89baefaf3 --- /dev/null +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs @@ -0,0 +1,9 @@ +namespace HwProj.Models.CoursesService.DTO +{ + public class GroupWithNameDTO + { + public long Id { get; set; } + public string Name { get; set; } + public string[] StudentsIds { get; set; } + } +} \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index f99ce1d42..d46c1a450 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -1,9 +1,11 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.API.Filters; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Services; +using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using Microsoft.AspNetCore.Mvc; @@ -36,6 +38,19 @@ public async Task GetAll(long courseId) return result; } + [HttpGet("{courseId}/getAllWithNames")] + public async Task GetAllWithNames(long courseId) + { + var groups = await _groupsService.GetAllAsync(courseId); + var result = groups.Select(t => new GroupWithNameDTO + { + Id = t.Id, + Name = t.Name, + StudentsIds = t.GroupMates.Select(s => s.StudentId).ToArray() + }).ToArray(); + return result; + } + [HttpPost("{courseId}/create")] public async Task CreateGroup([FromBody] CreateGroupViewModel groupViewModel) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index 2cbd55fd2..68d3c6d39 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -414,6 +414,16 @@ public async Task GetAllCourseGroups(long courseId) return await response.DeserializeAsync(); } + public async Task GetAllCourseGroupsWithNames(long courseId) + { + using var httpRequest = new HttpRequestMessage( + HttpMethod.Get, + _coursesServiceUri + $"api/CourseGroups/{courseId}/getAllWithNames"); + + var response = await _httpClient.SendAsync(httpRequest); + return await response.DeserializeAsync(); + } + public async Task CreateCourseGroup(CreateGroupViewModel model, long courseId) { using var httpRequest = new HttpRequestMessage( diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index da84eb73b..ddcde0582 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -37,6 +37,7 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task DeleteTask(long taskId); Task> UpdateTask(long taskId, PostTaskViewModel taskViewModel); Task GetAllCourseGroups(long courseId); + Task GetAllCourseGroupsWithNames(long courseId); Task CreateCourseGroup(CreateGroupViewModel model, long courseId); Task DeleteCourseGroup(long courseId, long groupId); Task UpdateCourseGroup(UpdateGroupViewModel model, long courseId, long groupId); diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index e198db7c5..04a3e0fae 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1065,6 +1065,31 @@ export interface GroupViewModel { */ studentsIds?: Array; } +/** + * + * @export + * @interface Group + */ +export interface Group { + /** + * + * @type {string} + * @memberof Group + */ + name?: string; + /** + * + * @type {number} + * @memberof Group + */ + id?: number; + /** + * + * @type {Array} + * @memberof Group + */ + studentsIds?: Array; +} /** * * @export @@ -3969,6 +3994,36 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config options: localVarRequestOptions, }; }, + courseGroupsGetAllCourseGroupsWithNames(courseId: number, options: any = {}): FetchArgs { + // verify required parameter 'courseId' is not null or undefined + if (courseId === null || courseId === undefined) { + throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling courseGroupsGetAllCourseGroupsWithNames.'); + } + const localVarPath = `/api/CourseGroups/{courseId}/getAllWithNames` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); + const localVarUrlObj = url.parse(localVarPath, true); + const localVarRequestOptions = Object.assign({ method: 'GET' }, options); + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication Bearer required + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? configuration.apiKey("Authorization") + : configuration.apiKey; + localVarHeaderParameter["Authorization"] = localVarApiKeyValue; + } + + localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); + // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 + localVarUrlObj.search = null; + localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); + + return { + url: url.format(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {number} courseId @@ -4256,6 +4311,24 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }); }; }, + /** + * + * @param {number} courseId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + const localVarFetchArgs = CourseGroupsApiFetchParamCreator(configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options); + return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { + return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { + if (response.status >= 200 && response.status < 300) { + return response.json(); + } else { + throw response; + } + }); + }; + }, /** * * @param {number} courseId @@ -4399,6 +4472,15 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f courseGroupsGetAllCourseGroups(courseId: number, options?: any) { return CourseGroupsApiFp(configuration).courseGroupsGetAllCourseGroups(courseId, options)(fetch, basePath); }, + /** + * + * @param {number} courseId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any) { + return CourseGroupsApiFp(configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options)(fetch, basePath); + }, /** * * @param {number} courseId @@ -4506,6 +4588,17 @@ export class CourseGroupsApi extends BaseAPI { return CourseGroupsApiFp(this.configuration).courseGroupsGetAllCourseGroups(courseId, options)(this.fetch, this.basePath); } + /** + * + * @param {number} courseId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CourseGroupsApi + */ + public courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any) { + return CourseGroupsApiFp(this.configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options)(this.fetch, this.basePath); + } + /** * * @param {number} courseId From 8cb61aaf87828d7ce23b5885aed7611fa40dabde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Wed, 4 Mar 2026 22:09:52 +0300 Subject: [PATCH 003/100] feat: add course groups api to apisingleton --- hwproj.front/src/api/ApiSingleton.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hwproj.front/src/api/ApiSingleton.ts b/hwproj.front/src/api/ApiSingleton.ts index df3527ba0..1886ef4ce 100644 --- a/hwproj.front/src/api/ApiSingleton.ts +++ b/hwproj.front/src/api/ApiSingleton.ts @@ -8,7 +8,8 @@ import { TasksApi, StatisticsApi, SystemApi, - FilesApi + FilesApi, + CourseGroupsApi } from "."; import AuthService from "../services/AuthService"; import CustomFilesApi from "./CustomFilesApi"; @@ -18,6 +19,7 @@ class Api { readonly accountApi: AccountApi; readonly expertsApi: ExpertsApi; readonly coursesApi: CoursesApi; + readonly courseGroupsApi: CourseGroupsApi; readonly solutionsApi: SolutionsApi; readonly notificationsApi: NotificationsApi; readonly homeworksApi: HomeworksApi; @@ -32,6 +34,7 @@ class Api { accountApi: AccountApi, expertsApi: ExpertsApi, coursesApi: CoursesApi, + courseGroupsApi: CourseGroupsApi, solutionsApi: SolutionsApi, notificationsApi: NotificationsApi, homeworksApi: HomeworksApi, @@ -45,6 +48,7 @@ class Api { this.accountApi = accountApi; this.expertsApi = expertsApi; this.coursesApi = coursesApi; + this.courseGroupsApi = courseGroupsApi; this.solutionsApi = solutionsApi; this.notificationsApi = notificationsApi; this.homeworksApi = homeworksApi; @@ -78,6 +82,7 @@ ApiSingleton = new Api( new AccountApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new ExpertsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new CoursesApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), + new CourseGroupsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new SolutionsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new NotificationsApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), new HomeworksApi({basePath: basePath, apiKey: () => "Bearer " + authService.getToken()!}), From 446ed7a5c1628bd38869759a5981811e5ac77242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Wed, 4 Mar 2026 22:11:33 +0300 Subject: [PATCH 004/100] feat: add course group tab prototype --- .../src/components/Courses/Course.tsx | 5 +- .../src/components/Courses/CourseGroups.tsx | 248 +++++++++++++++++- 2 files changed, 247 insertions(+), 6 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 0915f4eeb..a753459d8 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -399,7 +399,10 @@ const Course: React.FC = () => { /> } {tabValue === "groups" && isCourseMentor && - + } diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index 75af2dc49..67f5b04ad 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -1,10 +1,248 @@ -import {FC} from "react"; -import {Card, CardContent, CardActions, Grid, Button, Typography, Alert, AlertTitle} from '@mui/material'; +import {FC, useEffect, useState} from "react"; +import { + Card, + CardContent, + Grid, + Button, + Typography, + Alert, + AlertTitle, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Autocomplete, + Stack +} from "@mui/material"; +import {AccountDataDto, CourseGroupsApi, GroupViewModel, Configuration, Group} from "@/api"; +import ApiSingleton from "../../api/ApiSingleton"; -interface ICourseGroupsProps {} +interface ICourseGroupsProps { + courseId: number; + students: AccountDataDto[]; +} -const CourseGroups: FC = (props) => { - return +interface ICreateGroupFormState { + name: string; + memberIds: string[]; } +const CourseGroups: FC = (props) => { + const {courseId, students} = props; + + const [groups, setGroups] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [formState, setFormState] = useState({ + name: "", + memberIds: [] + }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const loadGroups = async () => { + setIsLoading(true); + setIsError(false); + try { + const result = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(courseId); + setGroups(result); + } catch { + setIsError(true); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + loadGroups(); + }, [courseId]); + + const handleOpenDialog = () => { + setFormState({ + name: "", + memberIds: [] + }); + setIsDialogOpen(true); + }; + + const handleCloseDialog = () => { + if (isSubmitting) return; + setIsDialogOpen(false); + }; + + const handleSubmit = async () => { + if (!formState.name.trim() || formState.memberIds.length === 0) { + return; + } + + setIsSubmitting(true); + try { + await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(courseId, { + name: formState.name.trim(), + groupMatesIds: formState.memberIds, + courseId: courseId + }); + setIsDialogOpen(false); + await loadGroups(); + } finally { + setIsSubmitting(false); + } + }; + + const getStudentName = (userId: string) => { + const student = students.find(s => s.userId === userId); + if (!student) { + return userId; + } + const nameParts = [student.surname, student.name, student.middleName].filter(Boolean); + return `${nameParts.join(" ") || student.email}`; + }; + + const namedGroups = groups.filter(g => g.name && g.name.trim().length > 0); + + return ( + + + + + Группы курса + + + + + + {isError && + + + Не удалось загрузить группы + Попробуйте обновить страницу позже. + + + } + + {!isLoading && namedGroups.length === 0 && !isError && + + + Пока нет ни одной именованной группы. + + + } + + + {namedGroups.map(group => { + const name = group.name!; + const studentsIds = group.studentsIds || []; + + return ( + + + + + {name} + + {studentsIds.length > 0 ? ( + + {studentsIds.map(id => ( + + {getStudentName(id)} + + ))} + + ) : ( + + В группе пока нет участников. + + )} + + + + ); + })} + + + + + Создать новую группу + + + + + { + e.persist(); + setFormState(prev => ({ + ...prev, + name: e.target.value + })); + }} + /> + + + formState.memberIds.includes(s.userId!))} + getOptionLabel={(option) => + `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() + } + filterSelectedOptions + onChange={(e, values) => { + e.persist(); + setFormState(prev => ({ + ...prev, + memberIds: values + .map(x => x.userId!) + .filter(Boolean) + })); + }} + renderInput={(params) => ( + + )} + /> + + + + + + + + + + ); +}; + export default CourseGroups; From d54c04585a3a029c86fdc300ca06f5c6d46071e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 6 Mar 2026 11:46:54 +0300 Subject: [PATCH 005/100] fix: groups ui --- .../src/components/Courses/Course.tsx | 7 ++--- .../src/components/Courses/CourseGroups.tsx | 30 +++++++++++-------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index a753459d8..972942cf7 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -328,11 +328,8 @@ const Course: React.FC = () => { label={newStudents.length}/> }/>} {isCourseMentor && -
Группы
- - }/>} +
Группы
}/> + } {tabValue === "homeworks" && = (props) => { const namedGroups = groups.filter(g => g.name && g.name.trim().length > 0); return ( - + @@ -131,27 +133,29 @@ const CourseGroups: FC = (props) => { {!isLoading && namedGroups.length === 0 && !isError && - Пока нет ни одной именованной группы. + На курсе пока нет групп. } - + {namedGroups.map(group => { const name = group.name!; const studentsIds = group.studentsIds || []; return ( - - - - + + + }> + {name} + + {studentsIds.length > 0 ? ( - + {studentsIds.map(id => ( - + {getStudentName(id)} ))} @@ -161,8 +165,8 @@ const CourseGroups: FC = (props) => { В группе пока нет участников. )} - - + + ); })} From 3e9825434e2df529a3c070bf299bae4a3b9ec672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 9 Mar 2026 19:29:47 +0300 Subject: [PATCH 006/100] feat: add group choose for homework --- .../ViewModels/HomeworkViewModels.cs | 4 ++ .../Domains/MappingExtensions.cs | 2 + .../Models/Homework.cs | 2 + .../Services/HomeworksService.cs | 3 +- hwproj.front/src/api/api.ts | 6 ++ .../Homeworks/CourseHomeworkExperimental.tsx | 58 ++++++++++++++++++- 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs index 2c7b0a857..1a196e463 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/HomeworkViewModels.cs @@ -27,6 +27,8 @@ public class CreateHomeworkViewModel public List Tasks { get; set; } = new List(); public ActionOptions? ActionOptions { get; set; } + + public long? GroupId { get; set; } } public class HomeworkViewModel @@ -58,5 +60,7 @@ public class HomeworkViewModel public List Tags { get; set; } = new List(); public List Tasks { get; set; } = new List(); + + public long? GroupId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index a6c321f66..152bf0410 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -32,6 +32,7 @@ public static HomeworkViewModel ToHomeworkViewModel(this Homework homework) IsDeferred = DateTime.UtcNow < homework.PublicationDate, Tasks = homework.Tasks.Select(t => t.ToHomeworkTaskViewModel()).ToList(), Tags = tags.ToList(), + GroupId = homework.GroupId, }; } @@ -147,6 +148,7 @@ public static Homework ToHomework(this CreateHomeworkViewModel homework) PublicationDate = homework.PublicationDate, Tasks = homework.Tasks.Select(t => t.ToHomeworkTask()).ToList(), Tags = string.Join(";", homework.Tags), + GroupId = homework.GroupId, }; public static CourseTemplate ToCourseTemplate(this CreateCourseViewModel createCourseViewModel) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs index 455c411a8..466bacaf2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Homework.cs @@ -25,5 +25,7 @@ public class Homework : IEntity public long CourseId { get; set; } public List Tasks { get; set; } + + public long? GroupId { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 76844defc..29ba73301 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -84,7 +84,8 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV DeadlineDate = update.DeadlineDate, PublicationDate = update.PublicationDate, IsDeadlineStrict = update.IsDeadlineStrict, - Tags = update.Tags + Tags = update.Tags, + GroupId = update.GroupId }); var updatedHomework = await _homeworksRepository.GetWithTasksAsync(homeworkId); diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 04a3e0fae..3c6582c04 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1365,6 +1365,12 @@ export interface HomeworkViewModel { * @memberof HomeworkViewModel */ tasks?: Array; + /** + * + * @type {number} + * @memberof HomeworkViewModel + */ + groupId?: number; } /** * diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 512bf8872..2ff1f47cb 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -10,7 +10,8 @@ Stack, TextField, Tooltip, - Typography + Typography, + Autocomplete } from "@mui/material"; import {MarkdownEditor, MarkdownPreview} from "components/Common/MarkdownEditor"; import FilesPreviewList from "components/Files/FilesPreviewList"; @@ -18,7 +19,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, Group } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -114,6 +115,9 @@ const CourseHomeworkEditor: FC<{ const [title, setTitle] = useState(loadedHomework.title!) const [tags, setTags] = useState(loadedHomework.tags!) const [description, setDescription] = useState(loadedHomework.description!) + const [selectedGroupId, setSelectedGroupId] = useState(loadedHomework.groupId) + const [groups, setGroups] = useState([]) + const [groupsLoading, setGroupsLoading] = useState(false) const [hasErrors, setHasErrors] = useState(false) @@ -124,6 +128,21 @@ const CourseHomeworkEditor: FC<{ const [deadlineSuggestion, setDeadlineSuggestion] = useState(undefined) const [tagSuggestion, setTagSuggestion] = useState(undefined) + const loadGroups = async () => { + setGroupsLoading(true) + try { + const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(courseId) + setGroups(courseGroups) + } catch (error) { + console.error('Failed to load groups:', error) + } finally { + setGroupsLoading(false) + } + } + useEffect(() => { + loadGroups() + }, [courseId]) + useEffect(() => { if (!isNewHomework || !metadata.publicationDate) return const isTest = tags.includes(TestTag) @@ -164,13 +183,14 @@ const CourseHomeworkEditor: FC<{ title: title, description: description, tags: tags, + groupId: selectedGroupId, hasErrors: hasErrors, deadlineDateNotSet: metadata.hasDeadline && !metadata.deadlineDate, isModified: true, } props.onUpdate({homework: update}) - }, [title, description, tags, metadata, hasErrors, filesState.selectedFilesInfo]) + }, [title, description, tags, metadata, hasErrors, filesState.selectedFilesInfo, selectedGroupId]) useEffect(() => { setHasErrors(!title || metadata.hasErrors) @@ -228,6 +248,7 @@ const CourseHomeworkEditor: FC<{ deadlineDate: metadata.deadlineDate, isDeadlineStrict: metadata.isDeadlineStrict, publicationDate: metadata.publicationDate, + groupId: selectedGroupId, actionOptions: editOptions, tasks: isNewHomework ? homework.tasks!.map(t => { const task: PostTaskViewModel = { @@ -287,6 +308,37 @@ const CourseHomeworkEditor: FC<{ + + {!isNewHomework && isPublished ? ( + g.id === loadedHomework.groupId)?.name || "Все студенты"} + variant="outlined" + fullWidth + disabled + /> + ) : ( + option.name || ""} + value={selectedGroupId !== undefined + ? groups.find(g => g.id === selectedGroupId) || null + : { id: undefined, name: "Все студенты" }} + onChange={(event, newValue) => { + setSelectedGroupId(newValue?.id) + }} + loading={groupsLoading} + renderInput={(params) => ( + + )} + /> + )} + {tags.includes(TestTag) && From cb7da7ab5aeed534285e1bf4c8573bbbead835d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 10 Mar 2026 20:33:27 +0300 Subject: [PATCH 007/100] refactor: separate group selector --- .../src/components/Common/GroupSelector.tsx | 70 +++++++++++++++++++ .../Homeworks/CourseHomeworkExperimental.tsx | 58 +++------------ 2 files changed, 78 insertions(+), 50 deletions(-) create mode 100644 hwproj.front/src/components/Common/GroupSelector.tsx diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx new file mode 100644 index 000000000..6723bdef0 --- /dev/null +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -0,0 +1,70 @@ +import {FC, useEffect, useState} from "react"; +import { + Grid, + TextField, + Autocomplete +} from "@mui/material"; +import ApiSingleton from "../../api/ApiSingleton"; +import { Group } from "@/api"; + + +interface GroupSelectorProps { + courseId: number, + onGroupIdChange: (groupId?: number) => void + selectedGroupId?: number , + disabled?: boolean, +} + +const GroupSelector: FC = (props) => { + const [groups, setGroups] = useState([]) + const [groupsLoading, setGroupsLoading] = useState(false) + + const loadGroups = async () => { + setGroupsLoading(true) + try { + const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(props.courseId) + setGroups(courseGroups) + } catch (error) { + console.error('Failed to load groups:', error) + } finally { + setGroupsLoading(false) + } + } + useEffect(() => { + loadGroups() + }, [props.courseId]) + + return ( + + {props.disabled ? ( + g.id === props.selectedGroupId)?.name || "Все студенты"} + variant="outlined" + fullWidth + disabled + /> + ) : ( + option.name || ""} + value={props.selectedGroupId !== undefined + ? groups.find(g => g.id === props.selectedGroupId) || null + : { id: undefined, name: "Все студенты" }} + onChange={(_, newGroup) => props.onGroupIdChange(newGroup?.id)} + loading={groupsLoading} + renderInput={(params) => ( + + )} + /> + )} + + ) +} + +export default GroupSelector \ No newline at end of file diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 2ff1f47cb..d45976cc9 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -11,7 +11,6 @@ TextField, Tooltip, Typography, - Autocomplete } from "@mui/material"; import {MarkdownEditor, MarkdownPreview} from "components/Common/MarkdownEditor"; import FilesPreviewList from "components/Files/FilesPreviewList"; @@ -19,7 +18,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, Group + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -38,6 +37,7 @@ import Lodash from "lodash"; import {CourseUnitType} from "../Files/CourseUnitType" import ProcessFilesUtils from "../Utils/ProcessFilesUtils"; import {FilesHandler} from "@/components/Files/FilesHandler"; +import GroupSelector from "../Common/GroupSelector"; export interface HomeworkAndFilesInfo { homework: HomeworkViewModel & { isModified?: boolean }, @@ -116,8 +116,6 @@ const CourseHomeworkEditor: FC<{ const [tags, setTags] = useState(loadedHomework.tags!) const [description, setDescription] = useState(loadedHomework.description!) const [selectedGroupId, setSelectedGroupId] = useState(loadedHomework.groupId) - const [groups, setGroups] = useState([]) - const [groupsLoading, setGroupsLoading] = useState(false) const [hasErrors, setHasErrors] = useState(false) @@ -128,21 +126,6 @@ const CourseHomeworkEditor: FC<{ const [deadlineSuggestion, setDeadlineSuggestion] = useState(undefined) const [tagSuggestion, setTagSuggestion] = useState(undefined) - const loadGroups = async () => { - setGroupsLoading(true) - try { - const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(courseId) - setGroups(courseGroups) - } catch (error) { - console.error('Failed to load groups:', error) - } finally { - setGroupsLoading(false) - } - } - useEffect(() => { - loadGroups() - }, [courseId]) - useEffect(() => { if (!isNewHomework || !metadata.publicationDate) return const isTest = tags.includes(TestTag) @@ -308,37 +291,12 @@ const CourseHomeworkEditor: FC<{ - - {!isNewHomework && isPublished ? ( - g.id === loadedHomework.groupId)?.name || "Все студенты"} - variant="outlined" - fullWidth - disabled - /> - ) : ( - option.name || ""} - value={selectedGroupId !== undefined - ? groups.find(g => g.id === selectedGroupId) || null - : { id: undefined, name: "Все студенты" }} - onChange={(event, newValue) => { - setSelectedGroupId(newValue?.id) - }} - loading={groupsLoading} - renderInput={(params) => ( - - )} - /> - )} - + setSelectedGroupId(groupId)} + selectedGroupId={selectedGroupId} + disabled={!isNewHomework} + /> {tags.includes(TestTag) && From e02de09cc212723bce2a786390c1342a2311ffb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 13 Mar 2026 13:01:23 +0300 Subject: [PATCH 008/100] feat: add homework update group validation --- .../HwProj.CoursesService.API/Domains/Validations.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs index 2a4dda861..6cade85ea 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/Validations.cs @@ -112,6 +112,11 @@ public static List ValidateHomework(CreateHomeworkViewModel homework, Ho errors.Add("Нельзя изменить дату публикации домашнего задания, если она уже показана студента"); } + if (previousState.GroupId != homework.GroupId) + { + errors.Add("Нельзя изменить группу для домашнего задания, если оно уже опубликовано"); + } + return errors; } } From 707aaf3975fd32cd3e101b9dfca2a008f062cbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 13 Mar 2026 13:05:33 +0300 Subject: [PATCH 009/100] feat: add apply filter subtractively method --- .../Services/CourseFilterService.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index b47139ee0..8bc18ab20 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -1,11 +1,13 @@ using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories; using HwProj.Models.CoursesService; using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; +using System; namespace HwProj.CoursesService.API.Services { @@ -170,5 +172,53 @@ private CourseDTO ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseF : courseDto.Homeworks }; } + + private CourseDTO ApplyFilterSubtractive(CourseDTO courseDto, CourseFilter? courseFilter) + { + var filter = courseFilter?.Filter; + + if (filter == null) + { + return courseDto; + } + + return new CourseDTO + { + Id = courseDto.Id, + Name = courseDto.Name, + GroupName = courseDto.GroupName, + IsCompleted = courseDto.IsCompleted, + IsOpen = courseDto.IsOpen, + InviteCode = courseDto.InviteCode, + Groups = + (filter.StudentIds.Any() + ? courseDto.Groups.Select(gs => + { + var filteredStudentsIds = gs.StudentsIds.Except(filter.StudentIds).ToArray(); + return filteredStudentsIds.Any() + ? new GroupViewModel + { + Id = gs.Id, + StudentsIds = filteredStudentsIds + } + : null; + }) + .Where(t => t != null) + .ToArray() + : courseDto.Groups)!, + MentorIds = filter.MentorIds.Any() + ? courseDto.MentorIds.Except(filter.MentorIds).ToArray() + : courseDto.MentorIds, + CourseMates = + filter.StudentIds.Any() + ? courseDto.CourseMates + .Where(mate => !mate.IsAccepted || !filter.StudentIds.Contains(mate.StudentId)).ToArray() + : courseDto.CourseMates, + Homeworks = + filter.HomeworkIds.Any() + ? courseDto.Homeworks.Where(hw => !filter.HomeworkIds.Contains(hw.Id)).ToArray() + : courseDto.Homeworks + }; + } } } From 623c2740847b7303f2ad5b4c699e0d211d17237a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 13 Mar 2026 13:06:16 +0300 Subject: [PATCH 010/100] feat: add global filter subtraction applying --- .../Services/CourseFilterService.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 8bc18ab20..80f41ba50 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -90,6 +90,32 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) : courseDto; if (isMentor || !isCourseStudent) return course; + // Применение глобального фильтра для вычитания групповых домашних заданий + if (!isMentor) + { + var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); + if (groupFilter != null) + { + // Если домашнее задание у пользователя в персональном фильтре, то не вычитаем его + var userHomeworkIds = userFilter?.Filter.HomeworkIds ?? new List(); + var homeworksToRemove = groupFilter.Filter.HomeworkIds.Except(userHomeworkIds).ToList(); + + if (homeworksToRemove.Any()) + { + var filterToRemove = new CourseFilter + { + Filter = new Filter + { + HomeworkIds = homeworksToRemove, + StudentIds = new List(), + MentorIds = new List() + } + }; + course = ApplyFilterSubtractive(course, filterToRemove); + } + } + } + var mentorIds = course.MentorIds .Where(u => // Фильтрация не настроена вообще From 1d255abfbbd3e0bccb55925a6913a1c2b3cda1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 13 Mar 2026 13:12:35 +0300 Subject: [PATCH 011/100] feat: add filter updating for group homeworks method --- .../Services/HomeworksService.cs | 79 ++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 29ba73301..13059a571 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -8,6 +8,9 @@ using HwProj.Models; using HwProj.Models.CoursesService.ViewModels; using HwProj.NotificationService.Events.CoursesService; +using HwProj.CoursesService.API.Repositories.Groups; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; namespace HwProj.CoursesService.API.Services { @@ -16,13 +19,17 @@ public class HomeworksService : IHomeworksService private readonly IHomeworksRepository _homeworksRepository; private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; + private readonly IGroupMatesRepository _groupMatesRepository; + private readonly ICourseFilterRepository _courseFilterRepository; - public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, - ICoursesRepository coursesRepository) + public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, ICoursesRepository coursesRepository, + IGroupMatesRepository groupMatesRepository, IGroupsService groupsService, ICourseFilterRepository courseFilterRepository) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; + _groupMatesRepository = groupMatesRepository; + _courseFilterRepository = courseFilterRepository; } public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) @@ -92,5 +99,73 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV CourseDomain.FillTasksInHomework(updatedHomework); return updatedHomework; } + + private async Task UpdateGroupFilters(long courseId, long homeworkId, List groupMates) + { + // Добавление группового домашнего задания в глобальный фильтр курса + var globalFilter = await _courseFilterRepository.GetAsync("", courseId); + + if (globalFilter != null) + { + var filter = globalFilter.Filter; + if (!filter.HomeworkIds.Contains(homeworkId)) + { + filter.HomeworkIds.Add(homeworkId); + } + + await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => + new CourseFilter + { + FilterJson = new CourseFilter { Filter = filter }.FilterJson + }); + } + else + { + var newFilter = new Filter + { + StudentIds = new List(), + HomeworkIds = new List { homeworkId }, + MentorIds = new List(), + }; + + await _courseFilterRepository.AddAsync(new CourseFilter { Filter = newFilter }, "", courseId); + } + + // Добавление группового домашнего задания в персональные фильтры участников группы + foreach (var groupMate in groupMates) + { + var studentFilter = await _courseFilterRepository.GetAsync(groupMate.StudentId, courseId); + + if (studentFilter != null) + { + var filter = studentFilter.Filter; + if (!filter.HomeworkIds.Contains(homeworkId)) + { + filter.HomeworkIds.Add(homeworkId); + } + + await _courseFilterRepository.UpdateAsync(studentFilter.Id, f => + new CourseFilter + { + FilterJson = new CourseFilter { Filter = filter }.FilterJson + }); + } + else + { + var newFilter = new Filter + { + StudentIds = new List(), + HomeworkIds = new List { homeworkId }, + MentorIds = new List() + }; + + await _courseFilterRepository.AddAsync( + new CourseFilter { Filter = newFilter }, + groupMate.StudentId, + courseId + ); + } + } + } } } From 7850406586391f887b9105808294007119c94c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 13 Mar 2026 13:13:39 +0300 Subject: [PATCH 012/100] feat: add updating filters for group homeworks --- .../Services/HomeworksService.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 13059a571..4efb7be19 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -39,14 +39,23 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.CourseId = courseId; var course = await _coursesRepository.GetWithCourseMatesAndHomeworksAsync(courseId); - var studentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var notificationStudentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + + await _homeworksRepository.AddAsync(homework); + + if(homework.GroupId != null) + { + var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == homework.GroupId).ToListAsync(); + await UpdateGroupFilters(courseId, homework.Id, groupMates); + notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + } + if (DateTime.UtcNow >= homework.PublicationDate) { - _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, studentIds, + _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, notificationStudentIds, homework.DeadlineDate)); } - await _homeworksRepository.AddAsync(homework); return await GetHomeworkAsync(homework.Id, withCriteria: true); } @@ -81,7 +90,17 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) - _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, studentIds)); + { + var notificationStudentIds = studentIds; + + if (update.GroupId != null) + { + var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToListAsync(); + notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + } + + _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notificationStudentIds)); + } await _homeworksRepository.UpdateAsync(homeworkId, hw => new Homework() { From 501c0025af411872b97b27cb996a7e394d931cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 22 Mar 2026 16:50:08 +0300 Subject: [PATCH 013/100] fix: student filters applying --- .../Services/CourseFilterService.cs | 89 +++++++++++++------ .../Services/HomeworksService.cs | 2 +- 2 files changed, 65 insertions(+), 26 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 80f41ba50..a09426c5f 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -1,6 +1,5 @@ using System.Linq; using System.Threading.Tasks; -using System.Collections.Generic; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories; using HwProj.Models.CoursesService; @@ -8,17 +7,21 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; using System; +using HwProj.CoursesService.API.Domains; namespace HwProj.CoursesService.API.Services { public class CourseFilterService : ICourseFilterService { private readonly ICourseFilterRepository _courseFilterRepository; + private readonly IHomeworksService _homeworksService; public CourseFilterService( - ICourseFilterRepository courseFilterRepository) + ICourseFilterRepository courseFilterRepository, + IHomeworksService homeworksService) { _courseFilterRepository = courseFilterRepository; + _homeworksService = homeworksService; } public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterModel courseFilterModel) @@ -85,37 +88,25 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) .ToDictionary(x => x.UserId, x => x.CourseFilter); - var course = courseFilters.TryGetValue(userId, out var userFilter) - ? ApplyFilterInternal(courseDto, userFilter) - : courseDto; - if (isMentor || !isCourseStudent) return course; - // Применение глобального фильтра для вычитания групповых домашних заданий - if (!isMentor) + if (isCourseStudent) { + var studentCourse = courseDto; var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); if (groupFilter != null) { - // Если домашнее задание у пользователя в персональном фильтре, то не вычитаем его - var userHomeworkIds = userFilter?.Filter.HomeworkIds ?? new List(); - var homeworksToRemove = groupFilter.Filter.HomeworkIds.Except(userHomeworkIds).ToList(); - - if (homeworksToRemove.Any()) - { - var filterToRemove = new CourseFilter - { - Filter = new Filter - { - HomeworkIds = homeworksToRemove, - StudentIds = new List(), - MentorIds = new List() - } - }; - course = ApplyFilterSubtractive(course, filterToRemove); - } + studentCourse = ApplyFilterSubtractive(courseDto, groupFilter); } + return courseFilters.TryGetValue(userId, out var studentFilter) + ? await ApplyFilterAdditive(studentCourse, studentFilter) + : studentCourse; } + var course = courseFilters.TryGetValue(userId, out var userFilter) + ? ApplyFilterInternal(courseDto, userFilter) + : courseDto; + if (isMentor || !isCourseStudent) return course; + var mentorIds = course.MentorIds .Where(u => // Фильтрация не настроена вообще @@ -246,5 +237,53 @@ private CourseDTO ApplyFilterSubtractive(CourseDTO courseDto, CourseFilter? cour : courseDto.Homeworks }; } + + private async Task ApplyFilterAdditive(CourseDTO courseDto, CourseFilter? courseFilter) + { + var filter = courseFilter?.Filter; + + if (filter == null) + { + return courseDto; + } + + var additionalHomeworks = filter.HomeworkIds.Any() + ? (await Task.WhenAll(filter.HomeworkIds.Select(id => _homeworksService.GetHomeworkAsync(id)))) + .Where(hw => hw != null) + .Select(hw => hw.ToHomeworkViewModel()) + .ToArray() + : Array.Empty(); + + return new CourseDTO + { + Id = courseDto.Id, + Name = courseDto.Name, + GroupName = courseDto.GroupName, + IsCompleted = courseDto.IsCompleted, + IsOpen = courseDto.IsOpen, + InviteCode = courseDto.InviteCode, + Groups = + (filter.StudentIds.Any() + ? courseDto.Groups.Select(gs => + { + var filteredStudentsIds = gs.StudentsIds.Union(filter.StudentIds).ToArray(); + return filteredStudentsIds.Any() + ? new GroupViewModel + { + Id = gs.Id, + StudentsIds = filteredStudentsIds + } + : null; + }) + .Where(t => t != null) + .ToArray() + : courseDto.Groups)!, + MentorIds = filter.MentorIds.Any() + ? courseDto.MentorIds.Union(filter.MentorIds).ToArray() + : courseDto.MentorIds, + CourseMates = courseDto.CourseMates, + Homeworks = courseDto.Homeworks.Union(additionalHomeworks).ToArray() + }; + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 4efb7be19..d17eacba2 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -76,7 +76,7 @@ public async Task GetForEditingHomeworkAsync(long homeworkId) public async Task DeleteHomeworkAsync(long homeworkId) { - await _homeworksRepository.DeleteAsync(homeworkId); + await _homeworksRepository.DeleteAsync(homeworkId); //TODO: удалить из фильтров } public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel) From 85acdbf6c1e525fa1350298038ef621ba56e1402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 23 Mar 2026 19:51:28 +0300 Subject: [PATCH 014/100] fix: deleting homework --- .../Services/CourseFilterService.cs | 3 +- .../Services/HomeworksService.cs | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index a09426c5f..7828bedd9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -88,11 +88,10 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) .ToDictionary(x => x.UserId, x => x.CourseFilter); - // Применение глобального фильтра для вычитания групповых домашних заданий if (isCourseStudent) { var studentCourse = courseDto; - var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); + var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий if (groupFilter != null) { studentCourse = ApplyFilterSubtractive(courseDto, groupFilter); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index d17eacba2..abf1ba2ac 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -76,7 +76,34 @@ public async Task GetForEditingHomeworkAsync(long homeworkId) public async Task DeleteHomeworkAsync(long homeworkId) { - await _homeworksRepository.DeleteAsync(homeworkId); //TODO: удалить из фильтров + var homework = await _homeworksRepository.GetAsync(homeworkId); + if (homework == null) return; + + var course = await _coursesRepository.GetWithCourseMates(homework.CourseId); + if (course == null) return; + + var courseUserIds = course.CourseMates.Select(cm => cm.StudentId).ToList(); + courseUserIds.Add(course.MentorIds); + courseUserIds.Add(""); + + // Удаляем homeworkId из фильтров всех участников курса + foreach (var userId in courseUserIds.Distinct()) + { + var userFilter = await _courseFilterRepository.GetAsync(userId, homework.CourseId); + + if (userFilter != null && userFilter.Filter.HomeworkIds.Contains(homeworkId)) + { + userFilter.Filter.HomeworkIds.Remove(homeworkId); + + await _courseFilterRepository.UpdateAsync(userFilter.Id, f => + new CourseFilter + { + FilterJson = new CourseFilter { Filter = userFilter.Filter }.FilterJson + }); + } + } + + await _homeworksRepository.DeleteAsync(homeworkId); } public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkViewModel homeworkViewModel) From 2a994942548116003aa19e75bda8c0a3e3a1c7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 24 Mar 2026 22:26:26 +0300 Subject: [PATCH 015/100] feat: show non included in groups course students count --- .../src/components/Courses/Course.tsx | 47 +++++++++++++++++-- .../src/components/Courses/CourseGroups.tsx | 37 ++++----------- 2 files changed, 54 insertions(+), 30 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 972942cf7..5604b92a1 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} from "react"; +import {FC, useEffect, useState, useMemo} from "react"; import {useNavigate, useParams, useSearchParams} from "react-router-dom"; -import {AccountDataDto, CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, Group, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -35,6 +35,7 @@ import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; import CourseGroups from "./CourseGroups"; +import { group } from "@uiw/react-md-editor"; type TabValue = "homeworks" | "stats" | "applications" | "groups" @@ -171,6 +172,35 @@ const Course: React.FC = () => { const [lecturerStatsState, setLecturerStatsState] = useState(false); + const [groups, setGroups] = useState([]); + const [groupLoadingError, setGroupLoadingError] = useState(false); + + const studentsInGroups = useMemo(() => { + const studentIds = new Set(); + groups.forEach(g => { + g.studentsIds?.forEach(id => studentIds.add(id)); + }); + return studentIds; + }, [groups]); + + const studentsWithoutGroup = useMemo(() => { + return acceptedStudents.filter(s => !studentsInGroups.has(s.userId!)); + }, [acceptedStudents, studentsInGroups]); + + const loadGroups = async () => { + setGroupLoadingError(false); + try { + const result = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(+courseId!); + setGroups(result.filter(g => g.name && g.name.trim().length > 0)); + } catch { + setGroupLoadingError(true); + } + }; + + useEffect(() => { + loadGroups(); + }, [courseId]); + const CourseMenu: FC = () => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -299,6 +329,11 @@ const Course: React.FC = () => { } + {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && !groupLoadingError && + + Студентов, не записанных в группу: {studentsWithoutGroup.length} + + } { label={newStudents.length}/> }/>} {isCourseMentor && Группы}/> + +
Группы
+ +
}/> } {tabValue === "homeworks" && { }
diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index 423091fd2..03658ee21 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -1,4 +1,4 @@ -import {FC, useEffect, useState} from "react"; +import {FC, useState} from "react"; import { Accordion, AccordionSummary, @@ -17,12 +17,14 @@ import { Stack } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import {AccountDataDto, CourseGroupsApi, GroupViewModel, Configuration, Group} from "@/api"; +import {AccountDataDto, Group} from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; interface ICourseGroupsProps { courseId: number; students: AccountDataDto[]; + groups: Group[]; + onGroupsUpdate: () => Promise; } interface ICreateGroupFormState { @@ -31,10 +33,8 @@ interface ICreateGroupFormState { } const CourseGroups: FC = (props) => { - const {courseId, students} = props; + const {courseId, students, groups} = props; - const [groups, setGroups] = useState([]); - const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); @@ -44,23 +44,6 @@ const CourseGroups: FC = (props) => { }); const [isSubmitting, setIsSubmitting] = useState(false); - const loadGroups = async () => { - setIsLoading(true); - setIsError(false); - try { - const result = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(courseId); - setGroups(result); - } catch { - setIsError(true); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - loadGroups(); - }, [courseId]); - const handleOpenDialog = () => { setFormState({ name: "", @@ -87,7 +70,9 @@ const CourseGroups: FC = (props) => { courseId: courseId }); setIsDialogOpen(false); - await loadGroups(); + await props.onGroupsUpdate(); + } catch { + setIsError(true); } finally { setIsSubmitting(false); } @@ -102,8 +87,6 @@ const CourseGroups: FC = (props) => { return `${nameParts.join(" ") || student.email}`; }; - const namedGroups = groups.filter(g => g.name && g.name.trim().length > 0); - return ( @@ -130,7 +113,7 @@ const CourseGroups: FC = (props) => { } - {!isLoading && namedGroups.length === 0 && !isError && + {!isSubmitting && groups.length === 0 && !isError && На курсе пока нет групп. @@ -139,7 +122,7 @@ const CourseGroups: FC = (props) => { } - {namedGroups.map(group => { + {groups.map(group => { const name = group.name!; const studentsIds = group.studentsIds || []; From ae06821927b361c4136c8bf424b8dfd8892e205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 26 Mar 2026 20:48:27 +0300 Subject: [PATCH 016/100] refactor: union apply filter methods by creating apply filter type --- .../Services/CourseFilterService.cs | 146 ++++-------------- 1 file changed, 34 insertions(+), 112 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 7828bedd9..77c3d7e10 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -11,6 +11,12 @@ namespace HwProj.CoursesService.API.Services { + public enum ApplyFilterType + { + Intersect, + Union, + Subtract + } public class CourseFilterService : ICourseFilterService { private readonly ICourseFilterRepository _courseFilterRepository; @@ -66,14 +72,7 @@ public async Task ApplyFiltersToCourses(string userId, CourseDTO[] var filters = (await _courseFilterRepository.GetAsync(userId, courseIds)) .ToDictionary(x => x.CourseId, x => x.CourseFilter); - - return courses - .Select(course => - { - filters.TryGetValue(course.Id, out var courseFilter); - return ApplyFilterInternal(course, courseFilter); - }) - .ToArray(); + return (await Task.WhenAll(courses.Select(course => ApplyFilter(course, userId)))).ToArray(); } public async Task ApplyFilter(CourseDTO courseDto, string userId) @@ -94,15 +93,15 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий if (groupFilter != null) { - studentCourse = ApplyFilterSubtractive(courseDto, groupFilter); + studentCourse = await ApplyFilterInternal(courseDto, groupFilter, ApplyFilterType.Subtract); } return courseFilters.TryGetValue(userId, out var studentFilter) - ? await ApplyFilterAdditive(studentCourse, studentFilter) + ? await ApplyFilterInternal(studentCourse, studentFilter, ApplyFilterType.Union) : studentCourse; } var course = courseFilters.TryGetValue(userId, out var userFilter) - ? ApplyFilterInternal(courseDto, userFilter) + ? await ApplyFilterInternal(courseDto, userFilter, ApplyFilterType.Intersect) : courseDto; if (isMentor || !isCourseStudent) return course; @@ -141,7 +140,7 @@ private async Task AddCourseFilter(Filter filter, long courseId, string us return courseFilterId; } - private CourseDTO ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseFilter) + private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseFilter, ApplyFilterType filterType) { var filter = courseFilter?.Filter; @@ -150,6 +149,28 @@ private CourseDTO ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseF return courseDto; } + var homeworks = filter.HomeworkIds.Any() + ? filterType switch + { + ApplyFilterType.Intersect => courseDto.Homeworks + .Where(hw => filter.HomeworkIds.Contains(hw.Id)) + .ToArray(), + + ApplyFilterType.Subtract => courseDto.Homeworks + .Where(hw => !filter.HomeworkIds.Contains(hw.Id)) + .ToArray(), + + ApplyFilterType.Union => courseDto.Homeworks + .Union((await Task.WhenAll( + filter.HomeworkIds.Select(id => _homeworksService.GetHomeworkAsync(id)))) + .Where(hw => hw != null) + .Select(hw => hw.ToHomeworkViewModel())) + .ToArray(), + + _ => courseDto.Homeworks + } + : courseDto.Homeworks; + return new CourseDTO { Id = courseDto.Id, @@ -182,106 +203,7 @@ private CourseDTO ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseF ? courseDto.CourseMates .Where(mate => !mate.IsAccepted || filter.StudentIds.Contains(mate.StudentId)).ToArray() : courseDto.CourseMates, - Homeworks = - filter.HomeworkIds.Any() - ? courseDto.Homeworks.Where(hw => filter.HomeworkIds.Contains(hw.Id)).ToArray() - : courseDto.Homeworks - }; - } - - private CourseDTO ApplyFilterSubtractive(CourseDTO courseDto, CourseFilter? courseFilter) - { - var filter = courseFilter?.Filter; - - if (filter == null) - { - return courseDto; - } - - return new CourseDTO - { - Id = courseDto.Id, - Name = courseDto.Name, - GroupName = courseDto.GroupName, - IsCompleted = courseDto.IsCompleted, - IsOpen = courseDto.IsOpen, - InviteCode = courseDto.InviteCode, - Groups = - (filter.StudentIds.Any() - ? courseDto.Groups.Select(gs => - { - var filteredStudentsIds = gs.StudentsIds.Except(filter.StudentIds).ToArray(); - return filteredStudentsIds.Any() - ? new GroupViewModel - { - Id = gs.Id, - StudentsIds = filteredStudentsIds - } - : null; - }) - .Where(t => t != null) - .ToArray() - : courseDto.Groups)!, - MentorIds = filter.MentorIds.Any() - ? courseDto.MentorIds.Except(filter.MentorIds).ToArray() - : courseDto.MentorIds, - CourseMates = - filter.StudentIds.Any() - ? courseDto.CourseMates - .Where(mate => !mate.IsAccepted || !filter.StudentIds.Contains(mate.StudentId)).ToArray() - : courseDto.CourseMates, - Homeworks = - filter.HomeworkIds.Any() - ? courseDto.Homeworks.Where(hw => !filter.HomeworkIds.Contains(hw.Id)).ToArray() - : courseDto.Homeworks - }; - } - - private async Task ApplyFilterAdditive(CourseDTO courseDto, CourseFilter? courseFilter) - { - var filter = courseFilter?.Filter; - - if (filter == null) - { - return courseDto; - } - - var additionalHomeworks = filter.HomeworkIds.Any() - ? (await Task.WhenAll(filter.HomeworkIds.Select(id => _homeworksService.GetHomeworkAsync(id)))) - .Where(hw => hw != null) - .Select(hw => hw.ToHomeworkViewModel()) - .ToArray() - : Array.Empty(); - - return new CourseDTO - { - Id = courseDto.Id, - Name = courseDto.Name, - GroupName = courseDto.GroupName, - IsCompleted = courseDto.IsCompleted, - IsOpen = courseDto.IsOpen, - InviteCode = courseDto.InviteCode, - Groups = - (filter.StudentIds.Any() - ? courseDto.Groups.Select(gs => - { - var filteredStudentsIds = gs.StudentsIds.Union(filter.StudentIds).ToArray(); - return filteredStudentsIds.Any() - ? new GroupViewModel - { - Id = gs.Id, - StudentsIds = filteredStudentsIds - } - : null; - }) - .Where(t => t != null) - .ToArray() - : courseDto.Groups)!, - MentorIds = filter.MentorIds.Any() - ? courseDto.MentorIds.Union(filter.MentorIds).ToArray() - : courseDto.MentorIds, - CourseMates = courseDto.CourseMates, - Homeworks = courseDto.Homeworks.Union(additionalHomeworks).ToArray() + Homeworks = homeworks }; } } From cde8d1acfdb643d4b438ea9e8b9e21874786f929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 27 Mar 2026 21:08:27 +0300 Subject: [PATCH 017/100] fix: parallel dbcontext access problem --- .../Services/CourseFilterService.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 77c3d7e10..270496771 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -6,7 +6,7 @@ using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; -using System; +using System.Collections.Generic; using HwProj.CoursesService.API.Domains; namespace HwProj.CoursesService.API.Services @@ -161,10 +161,7 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil .ToArray(), ApplyFilterType.Union => courseDto.Homeworks - .Union((await Task.WhenAll( - filter.HomeworkIds.Select(id => _homeworksService.GetHomeworkAsync(id)))) - .Where(hw => hw != null) - .Select(hw => hw.ToHomeworkViewModel())) + .Union(await GetHomeworksSequentially(filter.HomeworkIds)) .ToArray(), _ => courseDto.Homeworks @@ -206,5 +203,17 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil Homeworks = homeworks }; } + + private async Task> GetHomeworksSequentially(List homeworkIds) + { + var result = new List(); + foreach (var id in homeworkIds) + { + var hw = await _homeworksService.GetHomeworkAsync(id); + if (hw != null) + result.Add(hw.ToHomeworkViewModel()); + } + return result; + } } } From c2b9a4bc77ffdf84ca036ad90097fbb838932ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 27 Mar 2026 21:11:19 +0300 Subject: [PATCH 018/100] feat: mark grouped students --- .../src/components/Courses/Course.tsx | 2 +- .../src/components/Courses/CourseGroups.tsx | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 5604b92a1..ca4da5aa1 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -437,7 +437,7 @@ const Course: React.FC = () => { {tabValue === "groups" && isCourseMentor && diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index 03658ee21..b1d4b8039 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -14,15 +14,18 @@ import { DialogActions, TextField, Autocomplete, - Stack + Stack, + Tooltip, + Box } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import CheckIcon from "@mui/icons-material/Check"; import {AccountDataDto, Group} from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; interface ICourseGroupsProps { courseId: number; - students: AccountDataDto[]; + courseStudents: AccountDataDto[]; groups: Group[]; onGroupsUpdate: () => Promise; } @@ -33,7 +36,7 @@ interface ICreateGroupFormState { } const CourseGroups: FC = (props) => { - const {courseId, students, groups} = props; + const {courseId, courseStudents, groups} = props; const [isError, setIsError] = useState(false); @@ -79,7 +82,7 @@ const CourseGroups: FC = (props) => { }; const getStudentName = (userId: string) => { - const student = students.find(s => s.userId === userId); + const student = courseStudents.find(s => s.userId === userId); if (!student) { return userId; } @@ -87,6 +90,16 @@ const CourseGroups: FC = (props) => { return `${nameParts.join(" ") || student.email}`; }; + const isStudentInGroup = (userId: string): boolean => { + return groups.some(group => group.studentsIds?.includes(userId)); + }; + + const getSortedStudents = (): AccountDataDto[] => { + const studentsInGroups = courseStudents.filter(s => isStudentInGroup(s.userId!)); + const studentsNotInGroups = courseStudents.filter(s => !isStudentInGroup(s.userId!)); + return [...studentsNotInGroups, ...studentsInGroups]; + }; + return ( @@ -184,11 +197,33 @@ const CourseGroups: FC = (props) => { formState.memberIds.includes(s.userId!))} + options={getSortedStudents()} + value={courseStudents.filter(s => formState.memberIds.includes(s.userId!))} getOptionLabel={(option) => `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() } + renderOption={(props, option) => { + return ( + + + + {`${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim()} + + {isStudentInGroup(option.userId!) && ( + + + + )} + + + ); + }} filterSelectedOptions onChange={(e, values) => { e.persist(); From 19257f2bc4055c706f23711df1321562ff4a136d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 29 Mar 2026 12:37:38 +0300 Subject: [PATCH 019/100] fix: add automap for groups --- .../HwProj.CoursesService.API/AutomapperProfile.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs index 845b0e146..58d5effea 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs @@ -21,6 +21,10 @@ public AutomapperProfile() CreateMap(); CreateMap(); + + CreateMap().ReverseMap(); + + CreateMap().ReverseMap(); } } } \ No newline at end of file From b6e2df0207dff4c93fe2fa2f2dbf5633f13f7441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 29 Mar 2026 12:38:47 +0300 Subject: [PATCH 020/100] fix: parallel access error in groups service --- .../Services/GroupsService.cs | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 37b1173d8..3656845aa 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.API.Models; @@ -28,7 +30,10 @@ public GroupsService(IGroupsRepository groupsRepository, public async Task GetAllAsync(long courseId) { - return await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId).ToArrayAsync().ConfigureAwait(false); + return await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId) + .AsNoTracking() + .ToArrayAsync() + .ConfigureAwait(false); } public async Task GetGroupsAsync(params long[] groupIds) @@ -63,19 +68,39 @@ public async Task DeleteGroupAsync(long groupId) public async Task UpdateAsync(long groupId, Group updated) { - var group = await _groupsRepository.GetAsync(groupId); - group.GroupMates.RemoveAll(cm => true); - group.Tasks.RemoveAll(cm => true); + var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })) + .FirstOrDefault() ?? throw new InvalidOperationException($"Group with id {groupId} not found"); + + foreach (var groupMate in group.GroupMates.ToList()) + { + await _groupMatesRepository.DeleteAsync(groupMate.Id); + } + + foreach (var task in group.Tasks.ToList()) + { + await _taskModelsRepository.DeleteAsync(task.Id); + } - updated.GroupMates.ForEach(cm => cm.GroupId = groupId); - updated.Tasks.ForEach(cm => cm.GroupId = groupId); - var mateTasks = updated.GroupMates.Select(cm => _groupMatesRepository.AddAsync(cm)); - var idTasks = updated.Tasks.Select(cm => _taskModelsRepository.AddAsync(cm)); + updated.GroupMates?.ForEach(cm => cm.GroupId = groupId); + updated.Tasks?.ForEach(cm => cm.GroupId = groupId); group.Name = updated.Name; - await Task.WhenAll(mateTasks); - await Task.WhenAll(idTasks); + if (updated.GroupMates != null) + { + foreach (var groupMate in updated.GroupMates) + { + await _groupMatesRepository.AddAsync(groupMate); + } + } + + if (updated.Tasks != null) + { + foreach (var task in updated.Tasks) + { + await _taskModelsRepository.AddAsync(task); + } + } } public async Task DeleteGroupMateAsync(long groupId, string studentId) @@ -107,11 +132,15 @@ public async Task GetStudentGroupsAsync(long courseId, s .ToArrayAsync() .ConfigureAwait(false); - var getStudentGroupsTask = studentGroupsIds - .Select(async id => await _groupsRepository.GetAsync(id).ConfigureAwait(false)) - .Where(cm => cm.Result.CourseId == courseId) - .ToArray(); - var studentGroups = await Task.WhenAll(getStudentGroupsTask).ConfigureAwait(false); + var studentGroups = new List(); + foreach (var id in studentGroupsIds) + { + var group = await _groupsRepository.GetAsync(id).ConfigureAwait(false); + if (group.CourseId == courseId) + { + studentGroups.Add(group); + } + } return studentGroups.Select(c => _mapper.Map(c)).ToArray(); } From 8d43277c53bd3441104867449435d6a2c99fda2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 29 Mar 2026 12:41:06 +0300 Subject: [PATCH 021/100] fix: parallel access error in course filter service --- .../Services/CourseFilterService.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 270496771..207b79dbe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -72,7 +72,13 @@ public async Task ApplyFiltersToCourses(string userId, CourseDTO[] var filters = (await _courseFilterRepository.GetAsync(userId, courseIds)) .ToDictionary(x => x.CourseId, x => x.CourseFilter); - return (await Task.WhenAll(courses.Select(course => ApplyFilter(course, userId)))).ToArray(); + + var result = new List(); + foreach (var course in courses) + { + result.Add(await ApplyFilter(course, userId)); + } + return result.ToArray(); } public async Task ApplyFilter(CourseDTO courseDto, string userId) From adc7da69276385f4fbf8a6d1ee64f53fa1704580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 29 Mar 2026 12:43:03 +0300 Subject: [PATCH 022/100] fix: add hw to group mates filters on update --- .../Services/HomeworksService.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index abf1ba2ac..2850c9614 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -115,17 +115,18 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var homework = await _homeworksRepository.GetAsync(homeworkId); var course = await _coursesRepository.GetWithCourseMates(homework.CourseId); var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var notificationStudentIds = studentIds; - if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) + if (update.GroupId != null) { - var notificationStudentIds = studentIds; + var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToListAsync(); + await UpdateGroupFilters(course.Id, homework.Id, groupMates); - if (update.GroupId != null) - { - var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToListAsync(); - notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); - } + notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + } + if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) + { _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notificationStudentIds)); } From 644ca77b945c026c3dec8701036a76ac7d8ac7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 29 Mar 2026 13:53:52 +0300 Subject: [PATCH 023/100] feat: move creation groups to homework --- .../src/components/Common/GroupSelector.tsx | 319 ++++++++++++++++-- .../src/components/Courses/Course.tsx | 1 + .../components/Courses/CourseExperimental.tsx | 2 + .../src/components/Courses/CourseGroups.tsx | 10 +- .../Homeworks/CourseHomeworkExperimental.tsx | 22 +- 5 files changed, 312 insertions(+), 42 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 6723bdef0..db6bed7e3 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -2,69 +2,314 @@ import {FC, useEffect, useState} from "react"; import { Grid, TextField, - Autocomplete + Autocomplete, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Box, + Stack, + Typography, + Tooltip, + Alert, + AlertTitle, + CircularProgress } from "@mui/material"; +import CheckIcon from "@mui/icons-material/Check"; +import EditIcon from "@mui/icons-material/Edit"; +import AddIcon from "@mui/icons-material/Add"; import ApiSingleton from "../../api/ApiSingleton"; -import { Group } from "@/api"; +import { Group, AccountDataDto } from "@/api"; interface GroupSelectorProps { courseId: number, - onGroupIdChange: (groupId?: number) => void - selectedGroupId?: number , - disabled?: boolean, + courseStudents: AccountDataDto[], + onGroupIdChange: (groupId?: number) => void, + onCreateNewGroup?: () => void, + selectedGroupId?: number, + choiceDisabled?: boolean, + selectedGroupStudentIds?: string[], + onGroupsUpdate: () => void, } const GroupSelector: FC = (props) => { - const [groups, setGroups] = useState([]) - const [groupsLoading, setGroupsLoading] = useState(false) + const [groups, setGroups] = useState([]); + const [groupsLoading, setGroupsLoading] = useState(false); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [formState, setFormState] = useState<{ + name: string, + memberIds: string[] + }>({ + name: "", + memberIds: [] + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isError, setIsError] = useState(false); const loadGroups = async () => { setGroupsLoading(true) try { - const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(props.courseId) - setGroups(courseGroups) + const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(props.courseId); + setGroups(courseGroups); } catch (error) { - console.error('Failed to load groups:', error) + console.error('Failed to load groups:', error); + setIsError(true); } finally { - setGroupsLoading(false) + setGroupsLoading(false); } } useEffect(() => { - loadGroups() - }, [props.courseId]) + loadGroups(); + }, [props.courseId]); + + const handleOpenEditDialog = () => { + const selectedGroup = groups.find(g => g.id === props.selectedGroupId); + setFormState({ + name: selectedGroup?.name || "", + memberIds: selectedGroup?.studentsIds || [] + }) + setIsDialogOpen(true) + } + + const handleCloseEditDialog = () => { + if (isSubmitting) return; + setIsDialogOpen(false); + setIsError(false); + } + + const handleSubmitEdit = async () => { + setIsSubmitting(true); + try { + const selectedGroup = groups.find(g => g.id === props.selectedGroupId); + + if (selectedGroup) { + await ApiSingleton.courseGroupsApi.courseGroupsUpdateCourseGroup( + props.courseId, + selectedGroup.id!, + { + name: formState.name, + groupMates: formState.memberIds.map(studentId => ({ studentId })), + } + ); + await loadGroups(); + props.onGroupsUpdate(); + } else { + if (!formState.name.trim() || formState.memberIds.length === 0) { + return; + } + + const groupId = await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(props.courseId, { + name: formState.name.trim(), + groupMatesIds: formState.memberIds, + courseId: props.courseId, + }); + await loadGroups(); + props.onGroupsUpdate(); + props.onGroupIdChange(groupId); + } + setIsDialogOpen(false); + } catch (error) { + console.error('Failed to update group:', error); + setIsError(true); + } finally { + setIsSubmitting(false); + } + } + + const isStudentInGroup = (userId: string, currentGroupId?: number): boolean => { + return groups.some(group => group.id !== currentGroupId && group.studentsIds?.includes(userId)); + } + + const getSortedStudents = (): AccountDataDto[] => { + if (!props.courseStudents) return []; + const currentGroupId = props.selectedGroupId; + const studentsInOtherGroups = props.courseStudents.filter(s => + isStudentInGroup(s.userId!, currentGroupId) + ); + const studentsInCurrentGroup = props.courseStudents.filter(s => + formState.memberIds.includes(s.userId!) + ); + const studentsNotInAnyGroup = props.courseStudents.filter(s => + !isStudentInGroup(s.userId!, currentGroupId) && + !studentsInCurrentGroup.includes(s) + ); + return [...studentsNotInAnyGroup, ...studentsInCurrentGroup, ...studentsInOtherGroups]; + } + + const selectedGroup = groups.find(g => g.id === props.selectedGroupId); return ( - {props.disabled ? ( - g.id === props.selectedGroupId)?.name || "Все студенты"} - variant="outlined" - fullWidth - disabled - /> + {props.choiceDisabled ? ( + + + {selectedGroup && ( + + )} + ) : ( - option.name || ""} - value={props.selectedGroupId !== undefined - ? groups.find(g => g.id === props.selectedGroupId) || null - : { id: undefined, name: "Все студенты" }} - onChange={(_, newGroup) => props.onGroupIdChange(newGroup?.id)} - loading={groupsLoading} - renderInput={(params) => ( - + option.name || ""} + value={props.selectedGroupId !== undefined + ? groups.find(g => g.id === props.selectedGroupId) || null + : { id: undefined, name: "Все студенты" }} + onChange={(_, newGroup) => { + props.onGroupIdChange(newGroup?.id) + }} + loading={groupsLoading} + renderInput={(params) => ( + + )} + /> + {selectedGroup && ( + + )} + {!selectedGroup && ( + )} - /> + )} + + + + {selectedGroup ? "Редактировать группу" : "Создать группу"} + + + {isError && ( + + Ошибка + Не удалось {selectedGroup ? "создать" : "обновить"} группу. Попробуйте позже. + + )} + + + { + setFormState(prev => ({ + ...prev, + name: e.target.value + })) + }} + disabled={isSubmitting || props.choiceDisabled} + /> + + + formState.memberIds.includes(s.userId!)) || []} + getOptionLabel={(option) => + `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() + } + renderOption={(materialUIProps, option) => { + const isInOtherGroup = isStudentInGroup(option.userId!, props.choiceDisabled ? props.selectedGroupId : undefined) + return ( + + + + {`${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim()} + + {isInOtherGroup && ( + + + + )} + + + ) + }} + filterSelectedOptions + onChange={(e, values) => { + setFormState(prev => ({ + ...prev, + memberIds: values + .map(x => x.userId!) + .filter(Boolean) + })) + }} + disabled={isSubmitting} + renderInput={(params) => ( + + )} + /> + + + + + + + + ) } -export default GroupSelector \ No newline at end of file +export default GroupSelector diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index ca4da5aa1..474b30b33 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -412,6 +412,7 @@ const Course: React.FC = () => { courseHomeworks: homeworks })) }} + onGroupsUpdate={loadGroups} /> } {tabValue === "stats" && diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 138b4f225..6c4650d74 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -60,6 +60,7 @@ interface ICourseExperimentalProps { previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; } interface ICourseExperimentalState { @@ -440,6 +441,7 @@ export const CourseExperimental: FC = (props) => { }} isProcessing={props.processingFiles[homework.id!]?.isLoading || false} onStartProcessing={props.onStartProcessing} + onGroupsUpdate={props.onGroupsUpdate} /> diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index b1d4b8039..7b276d83e 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -1,4 +1,4 @@ -import {FC, useState} from "react"; +import {FC, useState, useEffect} from "react"; import { Accordion, AccordionSummary, @@ -27,7 +27,7 @@ interface ICourseGroupsProps { courseId: number; courseStudents: AccountDataDto[]; groups: Group[]; - onGroupsUpdate: () => Promise; + onGroupsUpdate: () => void; } interface ICreateGroupFormState { @@ -36,10 +36,14 @@ interface ICreateGroupFormState { } const CourseGroups: FC = (props) => { - const {courseId, courseStudents, groups} = props; + const {courseId, courseStudents, groups, onGroupsUpdate} = props; const [isError, setIsError] = useState(false); + useEffect(() => { + onGroupsUpdate(); + }, [courseId]); + const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState({ name: "", diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index d45976cc9..8b3178b59 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -18,7 +18,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -64,6 +64,7 @@ const CourseHomeworkEditor: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework const isNewHomework = homework.id! < 0 @@ -116,6 +117,19 @@ const CourseHomeworkEditor: FC<{ const [tags, setTags] = useState(loadedHomework.tags!) const [description, setDescription] = useState(loadedHomework.description!) const [selectedGroupId, setSelectedGroupId] = useState(loadedHomework.groupId) + const [courseStudents, setCourseStudents] = useState([]) + + useEffect(() => { + const loadCourseStudents = async () => { + try { + const courseData = await ApiSingleton.coursesApi.coursesGetAllCourseData(courseId) + setCourseStudents(courseData.course?.acceptedStudents || []) + } catch (error) { + console.error('Failed to load course students:', error) + } + } + loadCourseStudents() + }, [courseId]) const [hasErrors, setHasErrors] = useState(false) @@ -293,9 +307,11 @@ const CourseHomeworkEditor: FC<{ setSelectedGroupId(groupId)} selectedGroupId={selectedGroupId} - disabled={!isNewHomework} + choiceDisabled={!isNewHomework} + onGroupsUpdate={props.onGroupsUpdate} /> {tags.includes(TestTag) && @@ -404,6 +420,7 @@ const CourseHomeworkExperimental: FC<{ previouslyExistingFilesCount: number, waitingNewFilesCount: number, deletingFilesIds: number[]) => void; + onGroupsUpdate: () => void; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo const deferredTasks = homework.tasks!.filter(t => t.isDeferred!) @@ -424,6 +441,7 @@ const CourseHomeworkExperimental: FC<{ props.onUpdate(update) }} onStartProcessing={props.onStartProcessing} + onGroupsUpdate={props.onGroupsUpdate} /> return Date: Sun, 29 Mar 2026 14:03:47 +0300 Subject: [PATCH 024/100] feat: remove creation groups from groups tab --- .../src/components/Courses/Course.tsx | 1 - .../src/components/Courses/CourseGroups.tsx | 191 +----------------- 2 files changed, 4 insertions(+), 188 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 474b30b33..e10574bfd 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -437,7 +437,6 @@ const Course: React.FC = () => { } {tabValue === "groups" && isCourseMentor && void; } -interface ICreateGroupFormState { - name: string; - memberIds: string[]; -} - const CourseGroups: FC = (props) => { - const {courseId, courseStudents, groups, onGroupsUpdate} = props; - - const [isError, setIsError] = useState(false); + const {courseStudents, groups, onGroupsUpdate} = props; useEffect(() => { onGroupsUpdate(); - }, [courseId]); - - const [isDialogOpen, setIsDialogOpen] = useState(false); - const [formState, setFormState] = useState({ - name: "", - memberIds: [] - }); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleOpenDialog = () => { - setFormState({ - name: "", - memberIds: [] - }); - setIsDialogOpen(true); - }; - - const handleCloseDialog = () => { - if (isSubmitting) return; - setIsDialogOpen(false); - }; - - const handleSubmit = async () => { - if (!formState.name.trim() || formState.memberIds.length === 0) { - return; - } - - setIsSubmitting(true); - try { - await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(courseId, { - name: formState.name.trim(), - groupMatesIds: formState.memberIds, - courseId: courseId - }); - setIsDialogOpen(false); - await props.onGroupsUpdate(); - } catch { - setIsError(true); - } finally { - setIsSubmitting(false); - } - }; + }, []); const getStudentName = (userId: string) => { const student = courseStudents.find(s => s.userId === userId); @@ -94,16 +33,6 @@ const CourseGroups: FC = (props) => { return `${nameParts.join(" ") || student.email}`; }; - const isStudentInGroup = (userId: string): boolean => { - return groups.some(group => group.studentsIds?.includes(userId)); - }; - - const getSortedStudents = (): AccountDataDto[] => { - const studentsInGroups = courseStudents.filter(s => isStudentInGroup(s.userId!)); - const studentsNotInGroups = courseStudents.filter(s => !isStudentInGroup(s.userId!)); - return [...studentsNotInGroups, ...studentsInGroups]; - }; - return ( @@ -111,26 +40,10 @@ const CourseGroups: FC = (props) => { Группы курса - - {isError && - - - Не удалось загрузить группы - Попробуйте обновить страницу позже. - - - } - - {!isSubmitting && groups.length === 0 && !isError && + {groups.length === 0 && На курсе пока нет групп. @@ -171,102 +84,6 @@ const CourseGroups: FC = (props) => { ); })} - - - - Создать новую группу - - - - - { - e.persist(); - setFormState(prev => ({ - ...prev, - name: e.target.value - })); - }} - /> - - - formState.memberIds.includes(s.userId!))} - getOptionLabel={(option) => - `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() - } - renderOption={(props, option) => { - return ( - - - - {`${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim()} - - {isStudentInGroup(option.userId!) && ( - - - - )} - - - ); - }} - filterSelectedOptions - onChange={(e, values) => { - e.persist(); - setFormState(prev => ({ - ...prev, - memberIds: values - .map(x => x.userId!) - .filter(Boolean) - })); - }} - renderInput={(params) => ( - - )} - /> - - - - - - - - ); }; From 7d33478fa74c18d25c57f4e27b8e7938dc21bf65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 30 Mar 2026 12:41:36 +0300 Subject: [PATCH 025/100] fix: forbid remove student after group creation --- .../src/components/Common/GroupSelector.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index db6bed7e3..23c4d3016 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -14,7 +14,8 @@ import { Tooltip, Alert, AlertTitle, - CircularProgress + CircularProgress, + Chip } from "@mui/material"; import CheckIcon from "@mui/icons-material/Check"; import EditIcon from "@mui/icons-material/Edit"; @@ -27,11 +28,10 @@ interface GroupSelectorProps { courseId: number, courseStudents: AccountDataDto[], onGroupIdChange: (groupId?: number) => void, - onCreateNewGroup?: () => void, + onGroupsUpdate: () => void, selectedGroupId?: number, choiceDisabled?: boolean, - selectedGroupStudentIds?: string[], - onGroupsUpdate: () => void, + onCreateNewGroup?: () => void, } const GroupSelector: FC = (props) => { @@ -270,15 +270,34 @@ const GroupSelector: FC = (props) => { ) }} filterSelectedOptions - onChange={(e, values) => { - setFormState(prev => ({ - ...prev, - memberIds: values - .map(x => x.userId!) - .filter(Boolean) - })) + onChange={(_, values) => { + if (selectedGroup) { + // При редактировании выбранной группы можно только добавлять студентов + setFormState(prev => ({ + ...prev, + memberIds: [...formState.memberIds, + ...values.map(x => !formState.memberIds.includes(x.userId!) ? x.userId! : "").filter(Boolean)] + })) + } else { + setFormState(prev => ({ + ...prev, + memberIds: values + .map(x => x.userId!) + .filter(Boolean) + })) + } }} disabled={isSubmitting} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } renderInput={(params) => ( Date: Tue, 31 Mar 2026 21:23:43 +0300 Subject: [PATCH 026/100] fix: only mentor can see all group homeworks --- .../HwProj.CoursesService.API/Services/CourseFilterService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 207b79dbe..f50f3d520 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -93,7 +93,7 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) .ToDictionary(x => x.UserId, x => x.CourseFilter); - if (isCourseStudent) + if (!isMentor) { var studentCourse = courseDto; var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий From ebf7ba2f2c9522241f065bc7a7464528cbd72c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 13:13:11 +0300 Subject: [PATCH 027/100] fix: group homework cell in stats --- .../src/components/Courses/StudentStats.tsx | 17 +++++++++- .../Courses/Styles/StudentStatsCell.css | 15 ++++++++ .../src/components/Tasks/StudentStatsCell.tsx | 34 ++++++++++++++----- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 61e38becb..6c04bf594 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState, useRef} from "react"; -import {CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {CourseViewModel, Group, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import {useNavigate, useParams} from 'react-router-dom'; import {LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; @@ -148,6 +148,19 @@ const StudentStats: React.FC = (props) => { .forEach(x => bestTaskSolutions.set(x.taskId!, x.studentId!)) } + const [groups, setGroups] = useState([]); + useEffect(() => { + const loadGroups = async () => { + try { + const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(+courseId!); + setGroups(courseGroups); + } catch (error) { + console.error('Failed to load groups:', error); + } + }; + loadGroups(); + }, [courseId]); + return (
{props.solutions === undefined && } @@ -356,6 +369,7 @@ const StudentStats: React.FC = (props) => { {homeworks.map((homework, idx) => homework.tasks!.map((task, i) => { const additionalStyles = i === 0 && homeworkStyles(homeworks, idx) + const isDisabled = homework.groupId ? !groups.find(g => g.id === homework.groupId)?.studentsIds?.includes(cm.id!) : false return = (props) => { taskId={task.id!} taskMaxRating={task.maxRating!} isBestSolution={bestTaskSolutions.get(task.id!) === cm.id} + disabled={isDisabled} {...additionalStyles}/>; }) )} diff --git a/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css b/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css index 4bd62d251..4d0d7a5f1 100644 --- a/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css +++ b/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css @@ -22,3 +22,18 @@ .glow-cell { animation: golden-glow-strongest-inset 2.5s infinite ease-in-out; } + +.disabled-cell { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} + +.disabled-cell .red-cross { + color: #d32f2f; + font-size: 1.5rem; + font-weight: bold; + line-height: 1; +} diff --git a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx index a81501135..6b2b6aacd 100644 --- a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx +++ b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx @@ -17,6 +17,7 @@ interface ITaskStudentCellProps { taskMaxRating: number; isBestSolution: boolean; solutions?: SolutionDto[]; + disabled?: boolean; } const StudentStatsCell: FC = (props) => { @@ -27,11 +28,23 @@ const StudentStatsCell: FC const {ratedSolutionsCount, solutionsDescription} = cellState; - const tooltipTitle = ratedSolutionsCount === 0 - ? solutionsDescription - : solutionsDescription - + (props.isBestSolution ? "\n Первое решение с лучшей оценкой" : "") - + `\n\n${Utils.pluralizeHelper(["Проверена", "Проверены", "Проверено"], ratedSolutionsCount)} ${ratedSolutionsCount} ${Utils.pluralizeHelper(["попытка", "попытки", "попыток"], ratedSolutionsCount)}`; + const buildTitle = (): string => { + if (props.disabled) { + return "Задача недоступна для этого студента"; + } + + if (ratedSolutionsCount === 0) { + return solutionsDescription; + } + + const bestSolutionNote = props.isBestSolution + ? "\n Первое решение с лучшей оценкой" + : ""; + + const attemptsInfo = `\n\n${Utils.pluralizeHelper(["Проверена", "Проверены", "Проверено"], ratedSolutionsCount)} ${ratedSolutionsCount} ${Utils.pluralizeHelper(["попытка", "попытки", "попыток"], ratedSolutionsCount)}`; + + return solutionsDescription + bestSolutionNote + attemptsInfo; + }; const result = cellState.lastRatedSolution === undefined ? "" @@ -41,6 +54,7 @@ const StudentStatsCell: FC ; const handleCellClick = (e: React.MouseEvent) => { + if(props.disabled) return; // Формируем URL const url = forMentor ? `/task/${props.taskId}/${props.studentId}` @@ -59,7 +73,7 @@ const StudentStatsCell: FC return ( {tooltipTitle}}> + title={{buildTitle()}}> style={{ backgroundColor: cellState.color, borderLeft: `1px solid ${props.borderLeftColor || grey[300]}`, - cursor: "pointer", + cursor: props.disabled ? "default" : "pointer", }}> - {result} + {props.disabled + ?
+ +
+ : result}
); From a815e53f9d88b523c32a744b13096534df1746f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 13:16:20 +0300 Subject: [PATCH 028/100] fix: max homeworks rate counting --- .../src/components/Courses/StudentStats.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 6c04bf594..ecd83065c 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -104,13 +104,6 @@ const StudentStats: React.FC = (props) => { const notTests = homeworks.filter(h => !h.tags!.includes(TestTag)) - const homeworksMaxSum = notTests - .filter(h => !h.tags!.includes(BonusTag)) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => { - return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) - const testGroups = Lodash(homeworks.filter(h => h.tags!.includes(TestTag))) .groupBy((h: HomeworkViewModel) => { const key = h.tags!.find(t => !DefaultTags.includes(t)) @@ -125,7 +118,10 @@ const StudentStats: React.FC = (props) => { .reduce((sum, task) => sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)), 0) - const hasHomeworks = homeworksMaxSum > 0 + const hasHomeworks = !!notTests + .filter(h => !h.tags!.includes(BonusTag)) + .flatMap(homework => homework.tasks) + .filter(task => !task!.tags!.includes(BonusTag)) const hasTests = testsMaxSum > 0 const showBestSolutions = isMentor && (hasHomeworks || hasTests) @@ -230,7 +226,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - ДЗ ({homeworksMaxSum}) + ДЗ } {hasTests && = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) + const homeworksMaxSum = notTests + .filter(h => !h.tags!.includes(BonusTag) && + (groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) + .flatMap(homework => homework.tasks) + .reduce((sum, task) => { + return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); + }, 0) const testsSum = testGroups .map(group => { From 6a7f382a254ad1f0dd3344809e9c5e9ec26c855d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 13:27:12 +0300 Subject: [PATCH 029/100] fix: sort homeworks after apply filter --- .../HwProj.CoursesService.API/Services/CourseFilterService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index f50f3d520..9dc73d4d1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -206,7 +206,7 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil ? courseDto.CourseMates .Where(mate => !mate.IsAccepted || filter.StudentIds.Contains(mate.StudentId)).ToArray() : courseDto.CourseMates, - Homeworks = homeworks + Homeworks = homeworks.OrderBy(hw => hw.PublicationDate).ToArray() }; } From 03aa2a6d8bdc53e22700d976cabe0b54afbed67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 15:37:49 +0300 Subject: [PATCH 030/100] refactor: optimize sequantial db access for groups --- .../Services/GroupsService.cs | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 3656845aa..ad543f173 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -86,20 +86,14 @@ public async Task UpdateAsync(long groupId, Group updated) group.Name = updated.Name; - if (updated.GroupMates != null) + if (updated.GroupMates != null && updated.GroupMates.Count > 0) { - foreach (var groupMate in updated.GroupMates) - { - await _groupMatesRepository.AddAsync(groupMate); - } + await _groupMatesRepository.AddRangeAsync(updated.GroupMates).ConfigureAwait(false); } - if (updated.Tasks != null) + if (updated.Tasks != null && updated.Tasks.Count > 0) { - foreach (var task in updated.Tasks) - { - await _taskModelsRepository.AddAsync(task); - } + await _taskModelsRepository.AddRangeAsync(updated.Tasks).ConfigureAwait(false); } } @@ -132,17 +126,14 @@ public async Task GetStudentGroupsAsync(long courseId, s .ToArrayAsync() .ConfigureAwait(false); - var studentGroups = new List(); - foreach (var id in studentGroupsIds) - { - var group = await _groupsRepository.GetAsync(id).ConfigureAwait(false); - if (group.CourseId == courseId) - { - studentGroups.Add(group); - } - } + var studentGroups = await _groupsRepository + .GetGroupsWithGroupMatesAsync(studentGroupsIds) + .ConfigureAwait(false); - return studentGroups.Select(c => _mapper.Map(c)).ToArray(); + return studentGroups + .Where(g => g.CourseId == courseId) + .Select(c => _mapper.Map(c)) + .ToArray(); } } } From 33cf41ff11eafb79114b229110bc1aad66182706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 15:39:32 +0300 Subject: [PATCH 031/100] feat: get many homeworks method --- .../Repositories/HomeworksRepository.cs | 8 ++++++++ .../Repositories/IHomeworksRepository.cs | 1 + .../Services/HomeworksService.cs | 12 ++++++++++++ .../Services/IHomeworksService.cs | 2 ++ 4 files changed, 23 insertions(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs index aa663be58..6c41b669b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs @@ -27,6 +27,14 @@ public async Task GetAllWithTasksByCourseAsync(long courseId) .ToArrayAsync(); } + public async Task GetWithTasksAsync(long[] homeworkIds, bool withCriteria = false) + { + var query = Context.Set().AsNoTracking().Include(h => h.Tasks); + return withCriteria + ? await query.ThenInclude(x => x.Criteria).Where(h => homeworkIds.Contains(h.Id)).ToArrayAsync() + : await query.Where(h => homeworkIds.Contains(h.Id)).ToArrayAsync(); + } + public async Task GetWithTasksAsync(long id, bool withCriteria = false) { var query = Context.Set().AsNoTracking().Include(h => h.Tasks); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs index 4c05afdaf..d887b6d21 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs @@ -8,6 +8,7 @@ public interface IHomeworksRepository : ICrudRepository { Task GetAllWithTasksAsync(); Task GetAllWithTasksByCourseAsync(long courseId); + Task GetWithTasksAsync(long[] homeworkIds, bool withCriteria = false); Task GetWithTasksAsync(long id, bool withCriteria = false); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 2850c9614..69e677b88 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -68,6 +68,18 @@ public async Task GetHomeworkAsync(long homeworkId, bool withCriteria return homework; } + public async Task GetHomeworksAsync(long[] homeworkIds, bool withCriteria = false) + { + var homeworks = await _homeworksRepository.GetWithTasksAsync(homeworkIds, withCriteria); + + foreach (var homework in homeworks) + { + CourseDomain.FillTasksInHomework(homework); + } + + return homeworks; + } + public async Task GetForEditingHomeworkAsync(long homeworkId) { var result = await _homeworksRepository.GetWithTasksAsync(homeworkId); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs index 2b2460c37..606fea6d7 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs @@ -10,6 +10,8 @@ public interface IHomeworksService Task GetHomeworkAsync(long homeworkId, bool withCriteria = false); + Task GetHomeworksAsync(long[] homeworkIds, bool withCriteria = false); + Task GetForEditingHomeworkAsync(long homeworkId); Task DeleteHomeworkAsync(long homeworkId); From 075adea1535f1300e8a3d128160760ded2a35533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 4 Apr 2026 15:40:29 +0300 Subject: [PATCH 032/100] refactor: use many homeworks get method in filter service --- .../Services/CourseFilterService.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 9dc73d4d1..1a059fd93 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -70,9 +70,6 @@ public async Task ApplyFiltersToCourses(string userId, CourseDTO[] { var courseIds = courses.Select(c => c.Id).ToArray(); - var filters = (await _courseFilterRepository.GetAsync(userId, courseIds)) - .ToDictionary(x => x.CourseId, x => x.CourseFilter); - var result = new List(); foreach (var course in courses) { @@ -167,7 +164,11 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil .ToArray(), ApplyFilterType.Union => courseDto.Homeworks - .Union(await GetHomeworksSequentially(filter.HomeworkIds)) + .Concat((await _homeworksService.GetHomeworksAsync(filter.HomeworkIds.ToArray())) + .Where(hw => hw != null) + .Select(hw => hw.ToHomeworkViewModel())) + .GroupBy(hw => hw.Id) + .Select(g => g.First()) .ToArray(), _ => courseDto.Homeworks @@ -209,17 +210,5 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil Homeworks = homeworks.OrderBy(hw => hw.PublicationDate).ToArray() }; } - - private async Task> GetHomeworksSequentially(List homeworkIds) - { - var result = new List(); - foreach (var id in homeworkIds) - { - var hw = await _homeworksService.GetHomeworkAsync(id); - if (hw != null) - result.Add(hw.ToHomeworkViewModel()); - } - return result; - } } } From 9769fa95cdc82cf2ae83054f62a95486c75c1303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 5 Apr 2026 21:36:21 +0300 Subject: [PATCH 033/100] fix: can choose only students without group --- .../src/components/Common/GroupSelector.tsx | 54 +++---------------- 1 file changed, 6 insertions(+), 48 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 23c4d3016..be31709b4 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -1,4 +1,4 @@ -import {FC, useEffect, useState} from "react"; +import {FC, useEffect, useMemo, useState} from "react"; import { Grid, TextField, @@ -8,16 +8,12 @@ import { DialogTitle, DialogContent, DialogActions, - Box, Stack, - Typography, - Tooltip, Alert, AlertTitle, CircularProgress, Chip } from "@mui/material"; -import CheckIcon from "@mui/icons-material/Check"; import EditIcon from "@mui/icons-material/Edit"; import AddIcon from "@mui/icons-material/Add"; import ApiSingleton from "../../api/ApiSingleton"; @@ -118,25 +114,10 @@ const GroupSelector: FC = (props) => { } } - const isStudentInGroup = (userId: string, currentGroupId?: number): boolean => { - return groups.some(group => group.id !== currentGroupId && group.studentsIds?.includes(userId)); - } - - const getSortedStudents = (): AccountDataDto[] => { - if (!props.courseStudents) return []; - const currentGroupId = props.selectedGroupId; - const studentsInOtherGroups = props.courseStudents.filter(s => - isStudentInGroup(s.userId!, currentGroupId) - ); - const studentsInCurrentGroup = props.courseStudents.filter(s => - formState.memberIds.includes(s.userId!) - ); - const studentsNotInAnyGroup = props.courseStudents.filter(s => - !isStudentInGroup(s.userId!, currentGroupId) && - !studentsInCurrentGroup.includes(s) - ); - return [...studentsNotInAnyGroup, ...studentsInCurrentGroup, ...studentsInOtherGroups]; - } + const studentsWithousGroup = useMemo(() => { + const studentsInGroups = groups.flatMap(g => g.studentsIds) + return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) + }, [groups, props.courseStudents]); const selectedGroup = groups.find(g => g.id === props.selectedGroupId); @@ -241,34 +222,11 @@ const GroupSelector: FC = (props) => { formState.memberIds.includes(s.userId!)) || []} getOptionLabel={(option) => `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() } - renderOption={(materialUIProps, option) => { - const isInOtherGroup = isStudentInGroup(option.userId!, props.choiceDisabled ? props.selectedGroupId : undefined) - return ( - - - - {`${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim()} - - {isInOtherGroup && ( - - - - )} - - - ) - }} filterSelectedOptions onChange={(_, values) => { if (selectedGroup) { From dc729168aae9165a68590bc2136e60e03afcbb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 5 Apr 2026 21:39:20 +0300 Subject: [PATCH 034/100] refactor: students without group filter --- hwproj.front/src/components/Courses/Course.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index e10574bfd..f54b94e4b 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -175,17 +175,10 @@ const Course: React.FC = () => { const [groups, setGroups] = useState([]); const [groupLoadingError, setGroupLoadingError] = useState(false); - const studentsInGroups = useMemo(() => { - const studentIds = new Set(); - groups.forEach(g => { - g.studentsIds?.forEach(id => studentIds.add(id)); - }); - return studentIds; - }, [groups]); - const studentsWithoutGroup = useMemo(() => { - return acceptedStudents.filter(s => !studentsInGroups.has(s.userId!)); - }, [acceptedStudents, studentsInGroups]); + const inGroupIds = new Set(groups.flatMap(g => g.studentsIds)); + return acceptedStudents.filter(s => !inGroupIds.has(s.userId!)); + }, [groups, acceptedStudents]); const loadGroups = async () => { setGroupLoadingError(false); From 1b47e5de99f1a69a1674f93509bed95909e85279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 5 Apr 2026 21:41:45 +0300 Subject: [PATCH 035/100] refactor: styles calculation --- .../Courses/Styles/StudentStatsCell.css | 15 --------------- hwproj.front/src/services/StudentStatsUtils.ts | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css b/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css index 4d0d7a5f1..4bd62d251 100644 --- a/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css +++ b/hwproj.front/src/components/Courses/Styles/StudentStatsCell.css @@ -22,18 +22,3 @@ .glow-cell { animation: golden-glow-strongest-inset 2.5s infinite ease-in-out; } - -.disabled-cell { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; -} - -.disabled-cell .red-cross { - color: #d32f2f; - font-size: 1.5rem; - font-weight: bold; - line-height: 1; -} diff --git a/hwproj.front/src/services/StudentStatsUtils.ts b/hwproj.front/src/services/StudentStatsUtils.ts index 3597c9948..eefdebebd 100644 --- a/hwproj.front/src/services/StudentStatsUtils.ts +++ b/hwproj.front/src/services/StudentStatsUtils.ts @@ -22,7 +22,7 @@ export default class StudentStatsUtils { return ratedSolutions.slice(-1)[0] } - static calculateLastRatedSolutionInfo(solutions: SolutionDto[], taskMaxRating: number) { + static calculateLastRatedSolutionInfo(solutions: SolutionDto[], taskMaxRating: number, disabled: boolean = false) { const ratedSolutions = solutions!.filter(x => x.state !== SolutionState.NUMBER_0) const ratedSolutionsCount = ratedSolutions.length const isFirstUnratedTry = ratedSolutionsCount === 0 @@ -30,7 +30,9 @@ export default class StudentStatsUtils { const lastRatedSolution = ratedSolutions.slice(-1)[0] let solutionsDescription: string - if (lastSolution === undefined) + if(disabled) + solutionsDescription = "Задача недоступна для этого студента" + else if (lastSolution === undefined) solutionsDescription = "Решение отсутствует" else if (isFirstUnratedTry) solutionsDescription = "Решение ожидает проверки" @@ -38,11 +40,17 @@ export default class StudentStatsUtils { solutionsDescription = `${lastSolution.rating}/${taskMaxRating} ${Utils.pluralizeHelper(["балл", "балла", "баллов"], taskMaxRating)}` else solutionsDescription = "Последняя оценка — " + `${lastRatedSolution.rating}/${taskMaxRating} ${Utils.pluralizeHelper(["балл", "балла", "баллов"], taskMaxRating)}\nНовое решение ожидает проверки` + let color: string + if(disabled) + color = "#d1d1d1" + else if (lastRatedSolution == undefined) + color = "#ffffff" + else + color = StudentStatsUtils.getCellBackgroundColor(lastRatedSolution.state, lastRatedSolution.rating, taskMaxRating, isFirstUnratedTry) + return { lastRatedSolution: lastRatedSolution, - color: lastSolution === undefined - ? "#ffffff" - : StudentStatsUtils.getCellBackgroundColor(lastSolution.state, lastSolution.rating, taskMaxRating, isFirstUnratedTry), + color: color, ratedSolutionsCount, lastSolution, solutionsDescription } } From 3a4269034bb9cb169cba5d4841fe8cf5d0ca7138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sun, 5 Apr 2026 21:42:25 +0300 Subject: [PATCH 036/100] feat: show max homework sum --- hwproj.front/src/components/Courses/StudentStats.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index ecd83065c..e5c0df109 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -337,7 +337,7 @@ const StudentStats: React.FC = (props) => { backgroundColor: StudentStatsUtils.getRatingColor(homeworksSum, homeworksMaxSum), fontSize: 16 }} - label={homeworksSum}/> + label={`${homeworksSum} / ${homeworksMaxSum}`}/> } {hasTests && Date: Sun, 5 Apr 2026 21:43:43 +0300 Subject: [PATCH 037/100] fix: gray cell instead of red cross --- .../src/components/Tasks/StudentStatsCell.tsx | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx index 6b2b6aacd..dc27b864e 100644 --- a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx +++ b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx @@ -24,27 +24,16 @@ const StudentStatsCell: FC const navigate = useNavigate() const {solutions, taskMaxRating, forMentor} = props - const cellState = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, taskMaxRating) + const cellState = StudentStatsUtils.calculateLastRatedSolutionInfo(solutions!, taskMaxRating, props.disabled) const {ratedSolutionsCount, solutionsDescription} = cellState; - const buildTitle = (): string => { - if (props.disabled) { - return "Задача недоступна для этого студента"; - } - - if (ratedSolutionsCount === 0) { - return solutionsDescription; - } - - const bestSolutionNote = props.isBestSolution - ? "\n Первое решение с лучшей оценкой" - : ""; + const tooltipTitle = ratedSolutionsCount === 0 + ? solutionsDescription + : solutionsDescription + + (props.isBestSolution ? "\n Первое решение с лучшей оценкой" : "") + + `\n\n${Utils.pluralizeHelper(["Проверена", "Проверены", "Проверено"], ratedSolutionsCount)} ${ratedSolutionsCount} ${Utils.pluralizeHelper(["попытка", "попытки", "попыток"], ratedSolutionsCount)}`; - const attemptsInfo = `\n\n${Utils.pluralizeHelper(["Проверена", "Проверены", "Проверено"], ratedSolutionsCount)} ${ratedSolutionsCount} ${Utils.pluralizeHelper(["попытка", "попытки", "попыток"], ratedSolutionsCount)}`; - - return solutionsDescription + bestSolutionNote + attemptsInfo; - }; const result = cellState.lastRatedSolution === undefined ? "" @@ -55,6 +44,7 @@ const StudentStatsCell: FC const handleCellClick = (e: React.MouseEvent) => { if(props.disabled) return; + // Формируем URL const url = forMentor ? `/task/${props.taskId}/${props.studentId}` @@ -73,7 +63,7 @@ const StudentStatsCell: FC return ( {buildTitle()}}> + title={{tooltipTitle}}> borderLeft: `1px solid ${props.borderLeftColor || grey[300]}`, cursor: props.disabled ? "default" : "pointer", }}> - {props.disabled - ?
- -
- : result} + {result}
); From f2dcc7d2a944aa6b96f034e199619b00cb62a418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 9 Apr 2026 18:35:01 +0300 Subject: [PATCH 038/100] refactor: remove excess api calls (groups in stats) --- .../src/components/Common/GroupSelector.tsx | 2 +- .../src/components/Courses/Course.tsx | 1 + .../src/components/Courses/StudentStats.tsx | 20 +++++-------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index be31709b4..e5808a2cf 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -158,7 +158,7 @@ const GroupSelector: FC = (props) => { renderInput={(params) => ( diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index f54b94e4b..1b77bad77 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -417,6 +417,7 @@ const Course: React.FC = () => { isMentor={isCourseMentor} course={courseState.course} solutions={studentSolutions} + groups={groups} /> } diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index e5c0df109..36cdd85b4 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -19,6 +19,7 @@ interface IStudentStatsProps { isMentor: boolean; userId: string; solutions: StatisticsCourseMatesModel[] | undefined; + groups: Group[]; } interface IStudentStatsState { @@ -144,19 +145,6 @@ const StudentStats: React.FC = (props) => { .forEach(x => bestTaskSolutions.set(x.taskId!, x.studentId!)) } - const [groups, setGroups] = useState([]); - useEffect(() => { - const loadGroups = async () => { - try { - const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(+courseId!); - setGroups(courseGroups); - } catch (error) { - console.error('Failed to load groups:', error); - } - }; - loadGroups(); - }, [courseId]); - return (
{props.solutions === undefined && } @@ -270,7 +258,7 @@ const StudentStats: React.FC = (props) => { .reduce((sum, rating) => sum + rating, 0) const homeworksMaxSum = notTests .filter(h => !h.tags!.includes(BonusTag) && - (groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) + (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) .flatMap(homework => homework.tasks) .reduce((sum, task) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); @@ -372,7 +360,9 @@ const StudentStats: React.FC = (props) => { {homeworks.map((homework, idx) => homework.tasks!.map((task, i) => { const additionalStyles = i === 0 && homeworkStyles(homeworks, idx) - const isDisabled = homework.groupId ? !groups.find(g => g.id === homework.groupId)?.studentsIds?.includes(cm.id!) : false + const isDisabled = homework.groupId + ? !props.groups.find(g => g.id === homework.groupId)?.studentsIds?.includes(cm.id!) + : false return Date: Thu, 9 Apr 2026 18:46:47 +0300 Subject: [PATCH 039/100] refactor: remove excess api calls (group selector) --- .../src/components/Common/GroupSelector.tsx | 25 +++---------------- .../src/components/Courses/Course.tsx | 1 + .../components/Courses/CourseExperimental.tsx | 3 +++ .../Homeworks/CourseHomeworkExperimental.tsx | 7 +++++- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index e5808a2cf..b92b916ed 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -1,4 +1,4 @@ -import {FC, useEffect, useMemo, useState} from "react"; +import {FC, useMemo, useState} from "react"; import { Grid, TextField, @@ -23,6 +23,7 @@ import { Group, AccountDataDto } from "@/api"; interface GroupSelectorProps { courseId: number, courseStudents: AccountDataDto[], + groups: Group[], onGroupIdChange: (groupId?: number) => void, onGroupsUpdate: () => void, selectedGroupId?: number, @@ -31,8 +32,7 @@ interface GroupSelectorProps { } const GroupSelector: FC = (props) => { - const [groups, setGroups] = useState([]); - const [groupsLoading, setGroupsLoading] = useState(false); + const groups = props.groups || []; const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState<{ name: string, @@ -44,22 +44,6 @@ const GroupSelector: FC = (props) => { const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); - const loadGroups = async () => { - setGroupsLoading(true) - try { - const courseGroups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(props.courseId); - setGroups(courseGroups); - } catch (error) { - console.error('Failed to load groups:', error); - setIsError(true); - } finally { - setGroupsLoading(false); - } - } - useEffect(() => { - loadGroups(); - }, [props.courseId]); - const handleOpenEditDialog = () => { const selectedGroup = groups.find(g => g.id === props.selectedGroupId); setFormState({ @@ -89,7 +73,6 @@ const GroupSelector: FC = (props) => { groupMates: formState.memberIds.map(studentId => ({ studentId })), } ); - await loadGroups(); props.onGroupsUpdate(); } else { if (!formState.name.trim() || formState.memberIds.length === 0) { @@ -101,7 +84,6 @@ const GroupSelector: FC = (props) => { groupMatesIds: formState.memberIds, courseId: props.courseId, }); - await loadGroups(); props.onGroupsUpdate(); props.onGroupIdChange(groupId); } @@ -154,7 +136,6 @@ const GroupSelector: FC = (props) => { onChange={(_, newGroup) => { props.onGroupIdChange(newGroup?.id) }} - loading={groupsLoading} renderInput={(params) => ( { })) }} onGroupsUpdate={loadGroups} + groups={groups} /> } {tabValue === "stats" && diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 6c4650d74..a060359fc 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import { FileInfoDTO, + Group, HomeworkTaskViewModel, HomeworkViewModel, SolutionDto, StatisticsCourseMatesModel, } from "@/api"; @@ -61,6 +62,7 @@ interface ICourseExperimentalProps { waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; + groups: Group[]; } interface ICourseExperimentalState { @@ -442,6 +444,7 @@ export const CourseExperimental: FC = (props) => { isProcessing={props.processingFiles[homework.id!]?.isLoading || false} onStartProcessing={props.onStartProcessing} onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} /> diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 8b3178b59..add5dbfdd 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -18,7 +18,8 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, + Group } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -65,6 +66,7 @@ const CourseHomeworkEditor: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; + groups: Group[]; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework const isNewHomework = homework.id! < 0 @@ -312,6 +314,7 @@ const CourseHomeworkEditor: FC<{ selectedGroupId={selectedGroupId} choiceDisabled={!isNewHomework} onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} /> {tags.includes(TestTag) && @@ -421,6 +424,7 @@ const CourseHomeworkExperimental: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; + groups: Group[]; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo const deferredTasks = homework.tasks!.filter(t => t.isDeferred!) @@ -442,6 +446,7 @@ const CourseHomeworkExperimental: FC<{ }} onStartProcessing={props.onStartProcessing} onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} /> return Date: Fri, 10 Apr 2026 13:37:06 +0300 Subject: [PATCH 040/100] refactor: rename named group type --- .../Controllers/CourseGroupsController.cs | 3 +-- .../HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs | 9 --------- .../CoursesService/ViewModels/GroupViewModel.cs | 7 +++++++ .../Controllers/CourseGroupsController.cs | 5 ++--- .../HwProj.CoursesService.Client/CoursesServiceClient.cs | 4 ++-- .../ICoursesServiceClient.cs | 2 +- 6 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs index 7225b64d7..524fa0a1f 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using HwProj.CoursesService.Client; using HwProj.Models.CoursesService.ViewModels; -using HwProj.Models.CoursesService.DTO; using HwProj.Models.Roles; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -32,7 +31,7 @@ public async Task GetAllCourseGroups(long courseId) } [HttpGet("{courseId}/getAllWithNames")] - [ProducesResponseType(typeof(GroupWithNameDTO[]), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(NamedGroupViewModel[]), (int)HttpStatusCode.OK)] public async Task GetAllCourseGroupsWithNames(long courseId) { var result = await _coursesClient.GetAllCourseGroupsWithNames(courseId); diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs deleted file mode 100644 index 89baefaf3..000000000 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/GroupWithNameDTO.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace HwProj.Models.CoursesService.DTO -{ - public class GroupWithNameDTO - { - public long Id { get; set; } - public string Name { get; set; } - public string[] StudentsIds { get; set; } - } -} \ No newline at end of file diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs index 5f654924a..32ce94894 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs @@ -9,6 +9,13 @@ public class GroupViewModel public string[] StudentsIds { get; set; } } + public class NamedGroupViewModel + { + public long Id { get; set; } + public string Name { get; set; } + public string[] StudentsIds { get; set; } + } + public class CreateGroupViewModel { public string Name { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index d46c1a450..6f897febd 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -5,7 +5,6 @@ using HwProj.CoursesService.API.Filters; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Services; -using HwProj.Models.CoursesService.DTO; using HwProj.Models.CoursesService.ViewModels; using Microsoft.AspNetCore.Mvc; @@ -39,10 +38,10 @@ public async Task GetAll(long courseId) } [HttpGet("{courseId}/getAllWithNames")] - public async Task GetAllWithNames(long courseId) + public async Task GetAllWithNames(long courseId) { var groups = await _groupsService.GetAllAsync(courseId); - var result = groups.Select(t => new GroupWithNameDTO + var result = groups.Select(t => new NamedGroupViewModel { Id = t.Id, Name = t.Name, diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index 68d3c6d39..8e527c76b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -414,14 +414,14 @@ public async Task GetAllCourseGroups(long courseId) return await response.DeserializeAsync(); } - public async Task GetAllCourseGroupsWithNames(long courseId) + public async Task GetAllCourseGroupsWithNames(long courseId) { using var httpRequest = new HttpRequestMessage( HttpMethod.Get, _coursesServiceUri + $"api/CourseGroups/{courseId}/getAllWithNames"); var response = await _httpClient.SendAsync(httpRequest); - return await response.DeserializeAsync(); + return await response.DeserializeAsync(); } public async Task CreateCourseGroup(CreateGroupViewModel model, long courseId) diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index ddcde0582..84413bbbe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -37,7 +37,7 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task DeleteTask(long taskId); Task> UpdateTask(long taskId, PostTaskViewModel taskViewModel); Task GetAllCourseGroups(long courseId); - Task GetAllCourseGroupsWithNames(long courseId); + Task GetAllCourseGroupsWithNames(long courseId); Task CreateCourseGroup(CreateGroupViewModel model, long courseId); Task DeleteCourseGroup(long courseId, long groupId); Task UpdateCourseGroup(UpdateGroupViewModel model, long courseId, long groupId); From e1f76f019f396d07662bdd06a0cae14a84f4a9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 10 Apr 2026 13:39:19 +0300 Subject: [PATCH 041/100] refactor: rename named groups type in front-end --- hwproj.front/src/api/api.ts | 4 ++-- hwproj.front/src/components/Common/GroupSelector.tsx | 4 ++-- hwproj.front/src/components/Courses/Course.tsx | 4 ++-- hwproj.front/src/components/Courses/CourseExperimental.tsx | 4 ++-- hwproj.front/src/components/Courses/CourseGroups.tsx | 4 ++-- hwproj.front/src/components/Courses/StudentStats.tsx | 4 ++-- .../src/components/Homeworks/CourseHomeworkExperimental.tsx | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 3c6582c04..7db10c1ed 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -1070,7 +1070,7 @@ export interface GroupViewModel { * @export * @interface Group */ -export interface Group { +export interface NamedGroupViewModel { /** * * @type {string} @@ -4323,7 +4323,7 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { + courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { const localVarFetchArgs = CourseGroupsApiFetchParamCreator(configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options); return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index b92b916ed..ebb8e33f2 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -17,13 +17,13 @@ import { import EditIcon from "@mui/icons-material/Edit"; import AddIcon from "@mui/icons-material/Add"; import ApiSingleton from "../../api/ApiSingleton"; -import { Group, AccountDataDto } from "@/api"; +import { NamedGroupViewModel, AccountDataDto } from "@/api"; interface GroupSelectorProps { courseId: number, courseStudents: AccountDataDto[], - groups: Group[], + groups: NamedGroupViewModel[], onGroupIdChange: (groupId?: number) => void, onGroupsUpdate: () => void, selectedGroupId?: number, diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index a10e56164..b103c340f 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, Group, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, NamedGroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -172,7 +172,7 @@ const Course: React.FC = () => { const [lecturerStatsState, setLecturerStatsState] = useState(false); - const [groups, setGroups] = useState([]); + const [groups, setGroups] = useState([]); const [groupLoadingError, setGroupLoadingError] = useState(false); const studentsWithoutGroup = useMemo(() => { diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index a060359fc..9f7f78e3f 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { FileInfoDTO, - Group, + NamedGroupViewModel, HomeworkTaskViewModel, HomeworkViewModel, SolutionDto, StatisticsCourseMatesModel, } from "@/api"; @@ -62,7 +62,7 @@ interface ICourseExperimentalProps { waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: Group[]; + groups: NamedGroupViewModel[]; } interface ICourseExperimentalState { diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index 3d074a16b..a22edefb8 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -9,11 +9,11 @@ import { Stack, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import {AccountDataDto, Group} from "@/api"; +import {AccountDataDto, NamedGroupViewModel} from "@/api"; interface ICourseGroupsProps { courseStudents: AccountDataDto[]; - groups: Group[]; + groups: NamedGroupViewModel[]; onGroupsUpdate: () => void; } diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 36cdd85b4..d07961061 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState, useRef} from "react"; -import {CourseViewModel, Group, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {CourseViewModel, NamedGroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import {useNavigate, useParams} from 'react-router-dom'; import {LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; @@ -19,7 +19,7 @@ interface IStudentStatsProps { isMentor: boolean; userId: string; solutions: StatisticsCourseMatesModel[] | undefined; - groups: Group[]; + groups: NamedGroupViewModel[]; } interface IStudentStatsState { diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index add5dbfdd..1355370d8 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -19,7 +19,7 @@ import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, - Group + NamedGroupViewModel } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -66,7 +66,7 @@ const CourseHomeworkEditor: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: Group[]; + groups: NamedGroupViewModel[]; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework const isNewHomework = homework.id! < 0 @@ -424,7 +424,7 @@ const CourseHomeworkExperimental: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: Group[]; + groups: NamedGroupViewModel[]; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo const deferredTasks = homework.tasks!.filter(t => t.isDeferred!) From d85c77fe6e396da930293eadd303577bd636ad30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 10 Apr 2026 14:44:54 +0300 Subject: [PATCH 042/100] fix: update group name --- .../Services/GroupsService.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index ad543f173..868884ce4 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; @@ -73,19 +72,17 @@ public async Task UpdateAsync(long groupId, Group updated) foreach (var groupMate in group.GroupMates.ToList()) { - await _groupMatesRepository.DeleteAsync(groupMate.Id); + await _groupMatesRepository.DeleteAsync(groupMate.Id).ConfigureAwait(false); } foreach (var task in group.Tasks.ToList()) { - await _taskModelsRepository.DeleteAsync(task.Id); + await _taskModelsRepository.DeleteAsync(task.Id).ConfigureAwait(false); } updated.GroupMates?.ForEach(cm => cm.GroupId = groupId); updated.Tasks?.ForEach(cm => cm.GroupId = groupId); - group.Name = updated.Name; - if (updated.GroupMates != null && updated.GroupMates.Count > 0) { await _groupMatesRepository.AddRangeAsync(updated.GroupMates).ConfigureAwait(false); @@ -95,6 +92,11 @@ public async Task UpdateAsync(long groupId, Group updated) { await _taskModelsRepository.AddRangeAsync(updated.Tasks).ConfigureAwait(false); } + + await _groupsRepository.UpdateAsync(groupId, g => new Group + { + Name = updated.Name + }).ConfigureAwait(false); } public async Task DeleteGroupMateAsync(long groupId, string studentId) From bdb7536f6d0eaff60d35bd1f9dd492ad938ecedd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 10 Apr 2026 19:14:01 +0300 Subject: [PATCH 043/100] little style fix --- .../Controllers/CourseGroupsController.cs | 3 +-- .../HwProj.CoursesService.API/Services/GroupsService.cs | 6 ++---- .../Services/HomeworksService.cs | 9 ++------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index 6f897febd..d924ae49d 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.API.Filters; diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 868884ce4..4b4135339 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Threading.Tasks; using AutoMapper; using HwProj.CoursesService.API.Models; @@ -67,8 +66,7 @@ public async Task DeleteGroupAsync(long groupId) public async Task UpdateAsync(long groupId, Group updated) { - var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })) - .FirstOrDefault() ?? throw new InvalidOperationException($"Group with id {groupId} not found"); + var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })).FirstOrDefault(); foreach (var groupMate in group.GroupMates.ToList()) { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 69e677b88..49c76fcb1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -138,9 +138,7 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV } if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) - { _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notificationStudentIds)); - } await _homeworksRepository.UpdateAsync(homeworkId, hw => new Homework() { @@ -151,7 +149,7 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV PublicationDate = update.PublicationDate, IsDeadlineStrict = update.IsDeadlineStrict, Tags = update.Tags, - GroupId = update.GroupId + GroupId = update.GroupId, }); var updatedHomework = await _homeworksRepository.GetWithTasksAsync(homeworkId); @@ -167,10 +165,9 @@ private async Task UpdateGroupFilters(long courseId, long homeworkId, List new CourseFilter @@ -199,9 +196,7 @@ await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => { var filter = studentFilter.Filter; if (!filter.HomeworkIds.Contains(homeworkId)) - { filter.HomeworkIds.Add(homeworkId); - } await _courseFilterRepository.UpdateAsync(studentFilter.Id, f => new CourseFilter From 328b65e3eb18c1ffccb170fffc8494a826c7b76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Fri, 10 Apr 2026 22:36:08 +0300 Subject: [PATCH 044/100] add migration with homework's group id field --- ...20260410190811_HomeworkGroupId.Designer.cs | 485 ++++++++++++++++++ .../20260410190811_HomeworkGroupId.cs | 28 + .../Migrations/CourseContextModelSnapshot.cs | 3 + 3 files changed, 516 insertions(+) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs new file mode 100644 index 000000000..191ea7625 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.Designer.cs @@ -0,0 +1,485 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260410190811_HomeworkGroupId")] + partial class HomeworkGroupId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("MentorId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("InviteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsOpen") + .HasColumnType("bit"); + + b.Property("MentorIds") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FilterJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("MaxPoints") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Criteria"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("StudentId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("HomeworkId") + .HasColumnType("bigint"); + + b.Property("IsBonusExplicit") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("MaxRating") + .HasColumnType("int"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId") + .HasColumnType("bigint"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsPrivate") + .HasColumnType("bit"); + + b.Property("LecturerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Text") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("CourseFilterId") + .HasColumnType("bigint"); + + b.HasKey("CourseId", "UserId"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") + .WithMany("Criteria") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Homework"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate", null) + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseFilter"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Navigation("Assignments"); + + b.Navigation("CourseMates"); + + b.Navigation("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Navigation("Characteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Navigation("GroupMates"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Navigation("Criteria"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs new file mode 100644 index 000000000..d0990772a --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260410190811_HomeworkGroupId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + /// + public partial class HomeworkGroupId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "GroupId", + table: "Homeworks", + type: "bigint", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "GroupId", + table: "Homeworks"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 106df5d62..3bb2a5bb9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -202,6 +202,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Description") .HasColumnType("nvarchar(max)"); + b.Property("GroupId") + .HasColumnType("bigint"); + b.Property("HasDeadline") .HasColumnType("bit"); From fa9607e034afdba5ead96c450bfb6867290efdc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 11:40:36 +0300 Subject: [PATCH 045/100] refactor: union creation of selected group --- .../src/components/Common/GroupSelector.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index ebb8e33f2..e2e77c2ac 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -44,8 +44,16 @@ const GroupSelector: FC = (props) => { const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); + const selectedGroup = useMemo(() => + groups.find(g => g.id === props.selectedGroupId), + [groups, props.selectedGroupId]); + + const studentsWithousGroup = useMemo(() => { + const studentsInGroups = groups.flatMap(g => g.studentsIds) + return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) + }, [groups, props.courseStudents]); + const handleOpenEditDialog = () => { - const selectedGroup = groups.find(g => g.id === props.selectedGroupId); setFormState({ name: selectedGroup?.name || "", memberIds: selectedGroup?.studentsIds || [] @@ -62,8 +70,6 @@ const GroupSelector: FC = (props) => { const handleSubmitEdit = async () => { setIsSubmitting(true); try { - const selectedGroup = groups.find(g => g.id === props.selectedGroupId); - if (selectedGroup) { await ApiSingleton.courseGroupsApi.courseGroupsUpdateCourseGroup( props.courseId, @@ -96,13 +102,6 @@ const GroupSelector: FC = (props) => { } } - const studentsWithousGroup = useMemo(() => { - const studentsInGroups = groups.flatMap(g => g.studentsIds) - return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) - }, [groups, props.courseStudents]); - - const selectedGroup = groups.find(g => g.id === props.selectedGroupId); - return ( {props.choiceDisabled ? ( From 68541fa8d4e5274f95b228f08119ae09ac22724b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 11:46:46 +0300 Subject: [PATCH 046/100] little style fix --- hwproj.front/src/components/Tasks/StudentStatsCell.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx index dc27b864e..22405d4f6 100644 --- a/hwproj.front/src/components/Tasks/StudentStatsCell.tsx +++ b/hwproj.front/src/components/Tasks/StudentStatsCell.tsx @@ -28,13 +28,12 @@ const StudentStatsCell: FC const {ratedSolutionsCount, solutionsDescription} = cellState; - const tooltipTitle = ratedSolutionsCount === 0 + const tooltipTitle = ratedSolutionsCount === 0 ? solutionsDescription : solutionsDescription + (props.isBestSolution ? "\n Первое решение с лучшей оценкой" : "") + `\n\n${Utils.pluralizeHelper(["Проверена", "Проверены", "Проверено"], ratedSolutionsCount)} ${ratedSolutionsCount} ${Utils.pluralizeHelper(["попытка", "попытки", "попыток"], ratedSolutionsCount)}`; - const result = cellState.lastRatedSolution === undefined ? "" : From f7226a7514257d0dc755d09695dd58496f0db3b4 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sat, 11 Apr 2026 16:46:26 +0300 Subject: [PATCH 047/100] wip --- .../Models/CourseContext.cs | 4 +++- .../HwProj.CoursesService.API/Models/Group.cs | 4 +++- .../Repositories/Groups/GroupsRepository.cs | 2 -- .../Repositories/Groups/ITaskModelsRepository.cs | 9 --------- .../Repositories/Groups/TaskModelsRepository.cs | 13 ------------- .../Services/GroupsService.cs | 15 --------------- .../HwProj.CoursesService.API/Startup.cs | 1 - 7 files changed, 6 insertions(+), 42 deletions(-) delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs delete mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index 42587af31..261f4e09f 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System; +using Microsoft.EntityFrameworkCore; namespace HwProj.CoursesService.API.Models { @@ -8,6 +9,7 @@ public sealed class CourseContext : DbContext public DbSet CourseMates { get; set; } public DbSet Groups { get; set; } public DbSet GroupMates { get; set; } + [Obsolete("Не используется")] public DbSet TasksModels { get; set; } public DbSet Homeworks { get; set; } public DbSet Tasks { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs index 7aa8229a2..d5e2f6f49 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Group.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using HwProj.Repositories.Net8; @@ -15,6 +16,7 @@ public class Group : IEntity public List GroupMates { get; set; } = new List(); + [Obsolete("Не используется")] public List Tasks { get; set; } = new List(); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs index 6b0c8df45..3ed37a429 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/GroupsRepository.cs @@ -27,8 +27,6 @@ public IQueryable GetGroupsWithGroupMatesByCourse(long courseId) return Context.Set() .Where(c => c.CourseId == courseId) .Include(c => c.GroupMates) - .AsNoTracking() - .Include(c => c.Tasks) .AsNoTracking(); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs deleted file mode 100644 index 3d5813b2d..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/ITaskModelsRepository.cs +++ /dev/null @@ -1,9 +0,0 @@ -using HwProj.CoursesService.API.Models; -using HwProj.Repositories.Net8; - -namespace HwProj.CoursesService.API.Repositories.Groups -{ - public interface ITaskModelsRepository : ICrudRepository - { - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs deleted file mode 100644 index 28609be69..000000000 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/TaskModelsRepository.cs +++ /dev/null @@ -1,13 +0,0 @@ -using HwProj.CoursesService.API.Models; -using HwProj.Repositories.Net8; - -namespace HwProj.CoursesService.API.Repositories.Groups -{ - public class TaskModelsRepository : CrudRepository, ITaskModelsRepository - { - public TaskModelsRepository(CourseContext context) - : base(context) - { - } - } -} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 4b4135339..968ac94e8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -12,17 +12,14 @@ public class GroupsService : IGroupsService { private readonly IGroupsRepository _groupsRepository; private readonly IGroupMatesRepository _groupMatesRepository; - private readonly ITaskModelsRepository _taskModelsRepository; private readonly IMapper _mapper; public GroupsService(IGroupsRepository groupsRepository, IGroupMatesRepository groupMatesRepository, - ITaskModelsRepository taskModelsRepository, IMapper mapper) { _groupsRepository = groupsRepository; _groupMatesRepository = groupMatesRepository; - _taskModelsRepository = taskModelsRepository; _mapper = mapper; } @@ -59,7 +56,6 @@ public async Task DeleteGroupAsync(long groupId) { var group = await _groupsRepository.GetAsync(groupId); group.GroupMates.RemoveAll(cm => true); - group.Tasks.RemoveAll(cm => true); await _groupsRepository.DeleteAsync(groupId).ConfigureAwait(false); } @@ -73,24 +69,13 @@ public async Task UpdateAsync(long groupId, Group updated) await _groupMatesRepository.DeleteAsync(groupMate.Id).ConfigureAwait(false); } - foreach (var task in group.Tasks.ToList()) - { - await _taskModelsRepository.DeleteAsync(task.Id).ConfigureAwait(false); - } - updated.GroupMates?.ForEach(cm => cm.GroupId = groupId); - updated.Tasks?.ForEach(cm => cm.GroupId = groupId); if (updated.GroupMates != null && updated.GroupMates.Count > 0) { await _groupMatesRepository.AddRangeAsync(updated.GroupMates).ConfigureAwait(false); } - if (updated.Tasks != null && updated.Tasks.Count > 0) - { - await _taskModelsRepository.AddRangeAsync(updated.Tasks).ConfigureAwait(false); - } - await _groupsRepository.UpdateAsync(groupId, g => new Group { Name = updated.Name diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs index f6bb3f1d1..62461edc6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs @@ -33,7 +33,6 @@ public void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); From 3f40064f7b04deddbc38a53d8083072ce849f832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 16:46:58 +0300 Subject: [PATCH 048/100] refactor: create global filter id constant --- .../HwProj.CoursesService.API/Services/CourseFilterService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 1a059fd93..0b8db2b3a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -19,6 +19,7 @@ public enum ApplyFilterType } public class CourseFilterService : ICourseFilterService { + private const string GlobalFilterUserId = ""; private readonly ICourseFilterRepository _courseFilterRepository; private readonly IHomeworksService _homeworksService; From a63f1c1e30aaa38f6c51e884e2219aa945ed16c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 16:54:48 +0300 Subject: [PATCH 049/100] refactor: filter service responsibility --- .../Services/CourseFilterService.cs | 124 +++++++++++++----- .../Services/HomeworksService.cs | 107 +-------------- .../Services/ICourseFilterService.cs | 1 + 3 files changed, 99 insertions(+), 133 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 0b8db2b3a..8bb89b21e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -7,7 +7,6 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; using System.Collections.Generic; -using HwProj.CoursesService.API.Domains; namespace HwProj.CoursesService.API.Services { @@ -21,14 +20,11 @@ public class CourseFilterService : ICourseFilterService { private const string GlobalFilterUserId = ""; private readonly ICourseFilterRepository _courseFilterRepository; - private readonly IHomeworksService _homeworksService; public CourseFilterService( - ICourseFilterRepository courseFilterRepository, - IHomeworksService homeworksService) + ICourseFilterRepository courseFilterRepository) { _courseFilterRepository = courseFilterRepository; - _homeworksService = homeworksService; } public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterModel courseFilterModel) @@ -94,18 +90,18 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) if (!isMentor) { var studentCourse = courseDto; - var groupFilter = await _courseFilterRepository.GetAsync("", courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий + var groupFilter = await _courseFilterRepository.GetAsync(GlobalFilterUserId, courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий if (groupFilter != null) { - studentCourse = await ApplyFilterInternal(courseDto, groupFilter, ApplyFilterType.Subtract); + studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterType.Subtract); } return courseFilters.TryGetValue(userId, out var studentFilter) - ? await ApplyFilterInternal(studentCourse, studentFilter, ApplyFilterType.Union) + ? ApplyFilterInternal(courseDto, studentCourse, studentFilter, ApplyFilterType.Union) : studentCourse; } var course = courseFilters.TryGetValue(userId, out var userFilter) - ? await ApplyFilterInternal(courseDto, userFilter, ApplyFilterType.Intersect) + ? ApplyFilterInternal(courseDto, courseDto, userFilter, ApplyFilterType.Intersect) : courseDto; if (isMentor || !isCourseStudent) return course; @@ -144,49 +140,46 @@ private async Task AddCourseFilter(Filter filter, long courseId, string us return courseFilterId; } - private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFilter? courseFilter, ApplyFilterType filterType) + private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, CourseFilter? courseFilter, ApplyFilterType filterType) { var filter = courseFilter?.Filter; if (filter == null) { - return courseDto; + return editingCourseDto; } var homeworks = filter.HomeworkIds.Any() ? filterType switch { - ApplyFilterType.Intersect => courseDto.Homeworks + ApplyFilterType.Intersect => editingCourseDto.Homeworks .Where(hw => filter.HomeworkIds.Contains(hw.Id)) .ToArray(), - ApplyFilterType.Subtract => courseDto.Homeworks + ApplyFilterType.Subtract => editingCourseDto.Homeworks .Where(hw => !filter.HomeworkIds.Contains(hw.Id)) .ToArray(), - ApplyFilterType.Union => courseDto.Homeworks - .Concat((await _homeworksService.GetHomeworksAsync(filter.HomeworkIds.ToArray())) - .Where(hw => hw != null) - .Select(hw => hw.ToHomeworkViewModel())) - .GroupBy(hw => hw.Id) - .Select(g => g.First()) + ApplyFilterType.Union => editingCourseDto.Homeworks + .Concat(initialCourseDto.Homeworks + .Where(hw => filter.HomeworkIds.Contains(hw.Id))) .ToArray(), - _ => courseDto.Homeworks + _ => editingCourseDto.Homeworks } - : courseDto.Homeworks; + : editingCourseDto.Homeworks; return new CourseDTO { - Id = courseDto.Id, - Name = courseDto.Name, - GroupName = courseDto.GroupName, - IsCompleted = courseDto.IsCompleted, - IsOpen = courseDto.IsOpen, - InviteCode = courseDto.InviteCode, + Id = editingCourseDto.Id, + Name = editingCourseDto.Name, + GroupName = editingCourseDto.GroupName, + IsCompleted = editingCourseDto.IsCompleted, + IsOpen = editingCourseDto.IsOpen, + InviteCode = editingCourseDto.InviteCode, Groups = (filter.StudentIds.Any() - ? courseDto.Groups.Select(gs => + ? editingCourseDto.Groups.Select(gs => { var filteredStudentsIds = gs.StudentsIds.Intersect(filter.StudentIds).ToArray(); return filteredStudentsIds.Any() @@ -199,17 +192,82 @@ private async Task ApplyFilterInternal(CourseDTO courseDto, CourseFil }) .Where(t => t != null) .ToArray() - : courseDto.Groups)!, + : editingCourseDto.Groups)!, MentorIds = filter.MentorIds.Any() - ? courseDto.MentorIds.Intersect(filter.MentorIds).ToArray() - : courseDto.MentorIds, + ? editingCourseDto.MentorIds.Intersect(filter.MentorIds).ToArray() + : editingCourseDto.MentorIds, CourseMates = filter.StudentIds.Any() - ? courseDto.CourseMates + ? editingCourseDto.CourseMates .Where(mate => !mate.IsAccepted || filter.StudentIds.Contains(mate.StudentId)).ToArray() - : courseDto.CourseMates, + : editingCourseDto.CourseMates, Homeworks = homeworks.OrderBy(hw => hw.PublicationDate).ToArray() }; } + + public async Task UpdateGroupFilters(long courseId, long homeworkId, GroupMate[] groupMates) + { + // Добавление группового домашнего задания в глобальный фильтр курса + var globalFilter = await _courseFilterRepository.GetAsync(GlobalFilterUserId, courseId); + + if (globalFilter != null) + { + var filter = globalFilter.Filter; + + if (!filter.HomeworkIds.Contains(homeworkId)) + filter.HomeworkIds.Add(homeworkId); + + await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => + new CourseFilter + { + FilterJson = new CourseFilter { Filter = filter }.FilterJson + }); + } + else + { + var newFilter = new Filter + { + StudentIds = new List(), + HomeworkIds = new List { homeworkId }, + MentorIds = new List(), + }; + + await _courseFilterRepository.AddAsync(new CourseFilter { Filter = newFilter }, GlobalFilterUserId, courseId); + } + + // Добавление группового домашнего задания в персональные фильтры участников группы + foreach (var groupMate in groupMates) + { + var studentFilter = await _courseFilterRepository.GetAsync(groupMate.StudentId, courseId); + + if (studentFilter != null) + { + var filter = studentFilter.Filter; + if (!filter.HomeworkIds.Contains(homeworkId)) + filter.HomeworkIds.Add(homeworkId); + + await _courseFilterRepository.UpdateAsync(studentFilter.Id, f => + new CourseFilter + { + FilterJson = new CourseFilter { Filter = filter }.FilterJson + }); + } + else + { + var newFilter = new Filter + { + StudentIds = new List(), + HomeworkIds = new List { homeworkId }, + MentorIds = new List() + }; + + await _courseFilterRepository.AddAsync( + new CourseFilter { Filter = newFilter }, + groupMate.StudentId, + courseId + ); + } + } + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 49c76fcb1..4b6f3e779 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -10,7 +10,6 @@ using HwProj.NotificationService.Events.CoursesService; using HwProj.CoursesService.API.Repositories.Groups; using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; namespace HwProj.CoursesService.API.Services { @@ -20,16 +19,16 @@ public class HomeworksService : IHomeworksService private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; private readonly IGroupMatesRepository _groupMatesRepository; - private readonly ICourseFilterRepository _courseFilterRepository; + private readonly ICourseFilterService _courseFilterService; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, ICoursesRepository coursesRepository, - IGroupMatesRepository groupMatesRepository, IGroupsService groupsService, ICourseFilterRepository courseFilterRepository) + IGroupMatesRepository groupMatesRepository, IGroupsService groupsService, ICourseFilterService courseFilterService) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; _groupMatesRepository = groupMatesRepository; - _courseFilterRepository = courseFilterRepository; + _courseFilterService = courseFilterService; } public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewModel homeworkViewModel) @@ -45,8 +44,8 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo if(homework.GroupId != null) { - var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == homework.GroupId).ToListAsync(); - await UpdateGroupFilters(courseId, homework.Id, groupMates); + var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == homework.GroupId).ToArrayAsync(); + await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, groupMates); notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } @@ -88,33 +87,6 @@ public async Task GetForEditingHomeworkAsync(long homeworkId) public async Task DeleteHomeworkAsync(long homeworkId) { - var homework = await _homeworksRepository.GetAsync(homeworkId); - if (homework == null) return; - - var course = await _coursesRepository.GetWithCourseMates(homework.CourseId); - if (course == null) return; - - var courseUserIds = course.CourseMates.Select(cm => cm.StudentId).ToList(); - courseUserIds.Add(course.MentorIds); - courseUserIds.Add(""); - - // Удаляем homeworkId из фильтров всех участников курса - foreach (var userId in courseUserIds.Distinct()) - { - var userFilter = await _courseFilterRepository.GetAsync(userId, homework.CourseId); - - if (userFilter != null && userFilter.Filter.HomeworkIds.Contains(homeworkId)) - { - userFilter.Filter.HomeworkIds.Remove(homeworkId); - - await _courseFilterRepository.UpdateAsync(userFilter.Id, f => - new CourseFilter - { - FilterJson = new CourseFilter { Filter = userFilter.Filter }.FilterJson - }); - } - } - await _homeworksRepository.DeleteAsync(homeworkId); } @@ -131,8 +103,8 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV if (update.GroupId != null) { - var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToListAsync(); - await UpdateGroupFilters(course.Id, homework.Id, groupMates); + var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToArrayAsync(); + await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates); notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } @@ -156,70 +128,5 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV CourseDomain.FillTasksInHomework(updatedHomework); return updatedHomework; } - - private async Task UpdateGroupFilters(long courseId, long homeworkId, List groupMates) - { - // Добавление группового домашнего задания в глобальный фильтр курса - var globalFilter = await _courseFilterRepository.GetAsync("", courseId); - - if (globalFilter != null) - { - var filter = globalFilter.Filter; - - if (!filter.HomeworkIds.Contains(homeworkId)) - filter.HomeworkIds.Add(homeworkId); - - await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => - new CourseFilter - { - FilterJson = new CourseFilter { Filter = filter }.FilterJson - }); - } - else - { - var newFilter = new Filter - { - StudentIds = new List(), - HomeworkIds = new List { homeworkId }, - MentorIds = new List(), - }; - - await _courseFilterRepository.AddAsync(new CourseFilter { Filter = newFilter }, "", courseId); - } - - // Добавление группового домашнего задания в персональные фильтры участников группы - foreach (var groupMate in groupMates) - { - var studentFilter = await _courseFilterRepository.GetAsync(groupMate.StudentId, courseId); - - if (studentFilter != null) - { - var filter = studentFilter.Filter; - if (!filter.HomeworkIds.Contains(homeworkId)) - filter.HomeworkIds.Add(homeworkId); - - await _courseFilterRepository.UpdateAsync(studentFilter.Id, f => - new CourseFilter - { - FilterJson = new CourseFilter { Filter = filter }.FilterJson - }); - } - else - { - var newFilter = new Filter - { - StudentIds = new List(), - HomeworkIds = new List { homeworkId }, - MentorIds = new List() - }; - - await _courseFilterRepository.AddAsync( - new CourseFilter { Filter = newFilter }, - groupMate.StudentId, - courseId - ); - } - } - } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs index 36d5a2945..9c0fda18c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs @@ -14,5 +14,6 @@ public interface ICourseFilterService Task ApplyFiltersToCourses(string userId, CourseDTO[] courses); Task ApplyFilter(CourseDTO courseDto, string userId); Task GetAssignedStudentsIds(long courseId, string[] mentorsIds); + Task UpdateGroupFilters(long courseId, long homeworkId, GroupMate[] groupMates); } } \ No newline at end of file From face16baf058bcfaf08369f7a27f4aab53e40eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 17:04:01 +0300 Subject: [PATCH 050/100] refactor: db access in groupmates filters --- .../Services/CourseFilterService.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 8bb89b21e..6abe60c44 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -236,11 +236,13 @@ await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => } // Добавление группового домашнего задания в персональные фильтры участников группы + var studentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + var studentFilters = (await _courseFilterRepository.GetAsync(studentIds, courseId)) + .ToDictionary(x => x.UserId, x => x.CourseFilter); + foreach (var groupMate in groupMates) { - var studentFilter = await _courseFilterRepository.GetAsync(groupMate.StudentId, courseId); - - if (studentFilter != null) + if (studentFilters.TryGetValue(groupMate.StudentId, out var studentFilter)) { var filter = studentFilter.Filter; if (!filter.HomeworkIds.Contains(homeworkId)) From 52074467d0ef090e8db0ac9b70263a48163ad36c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 17:08:07 +0300 Subject: [PATCH 051/100] refactor: applyFilterType -> operation --- .../Services/CourseFilterService.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 6abe60c44..42e28629d 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -10,7 +10,7 @@ namespace HwProj.CoursesService.API.Services { - public enum ApplyFilterType + public enum ApplyFilterOperation { Intersect, Union, @@ -93,15 +93,15 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) var groupFilter = await _courseFilterRepository.GetAsync(GlobalFilterUserId, courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий if (groupFilter != null) { - studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterType.Subtract); + studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterOperation.Subtract); } return courseFilters.TryGetValue(userId, out var studentFilter) - ? ApplyFilterInternal(courseDto, studentCourse, studentFilter, ApplyFilterType.Union) + ? ApplyFilterInternal(courseDto, studentCourse, studentFilter, ApplyFilterOperation.Union) : studentCourse; } var course = courseFilters.TryGetValue(userId, out var userFilter) - ? ApplyFilterInternal(courseDto, courseDto, userFilter, ApplyFilterType.Intersect) + ? ApplyFilterInternal(courseDto, courseDto, userFilter, ApplyFilterOperation.Intersect) : courseDto; if (isMentor || !isCourseStudent) return course; @@ -140,7 +140,7 @@ private async Task AddCourseFilter(Filter filter, long courseId, string us return courseFilterId; } - private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, CourseFilter? courseFilter, ApplyFilterType filterType) + private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, CourseFilter? courseFilter, ApplyFilterOperation filterType) { var filter = courseFilter?.Filter; @@ -152,15 +152,15 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit var homeworks = filter.HomeworkIds.Any() ? filterType switch { - ApplyFilterType.Intersect => editingCourseDto.Homeworks + ApplyFilterOperation.Intersect => editingCourseDto.Homeworks .Where(hw => filter.HomeworkIds.Contains(hw.Id)) .ToArray(), - ApplyFilterType.Subtract => editingCourseDto.Homeworks + ApplyFilterOperation.Subtract => editingCourseDto.Homeworks .Where(hw => !filter.HomeworkIds.Contains(hw.Id)) .ToArray(), - ApplyFilterType.Union => editingCourseDto.Homeworks + ApplyFilterOperation.Union => editingCourseDto.Homeworks .Concat(initialCourseDto.Homeworks .Where(hw => filter.HomeworkIds.Contains(hw.Id))) .ToArray(), From 8228af8040d5acbd9e779c9132c97e05c4b1a054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 17:23:02 +0300 Subject: [PATCH 052/100] refactor: remove nullable group access --- .../Services/CourseFilterService.cs | 7 +++---- .../HwProj.CoursesService.API/Services/GroupsService.cs | 7 +++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 42e28629d..ab88fd313 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -80,8 +80,8 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) var isMentor = courseDto.MentorIds.Contains(userId); var isCourseStudent = courseDto.AcceptedStudents.Any(t => t.StudentId == userId); var findFiltersFor = isMentor || !isCourseStudent - ? new[] { userId } - : courseDto.MentorIds.Concat(new[] { userId }).ToArray(); + ? new[] { userId, GlobalFilterUserId } + : courseDto.MentorIds.Concat(new[] { userId, GlobalFilterUserId }).ToArray(); var courseFilters = (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) @@ -90,8 +90,7 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) if (!isMentor) { var studentCourse = courseDto; - var groupFilter = await _courseFilterRepository.GetAsync(GlobalFilterUserId, courseDto.Id); // Глобальный фильтр для вычитания групповых домашних заданий - if (groupFilter != null) + if (courseFilters.TryGetValue(GlobalFilterUserId, out var groupFilter)) { studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterOperation.Subtract); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 968ac94e8..68b04bfce 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -62,9 +62,12 @@ public async Task DeleteGroupAsync(long groupId) public async Task UpdateAsync(long groupId, Group updated) { - var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })).FirstOrDefault(); + var existingGroupMates = await _groupMatesRepository + .FindAll(cm => cm.GroupId == groupId) + .ToArrayAsync() + .ConfigureAwait(false); - foreach (var groupMate in group.GroupMates.ToList()) + foreach (var groupMate in existingGroupMates) { await _groupMatesRepository.DeleteAsync(groupMate.Id).ConfigureAwait(false); } From 177bebd27f26de00ef0cc6150602cd1630d64904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 17:41:35 +0300 Subject: [PATCH 053/100] refactor: get groupmates --- .../Services/CourseFilterService.cs | 5 +---- .../Services/HomeworksService.cs | 17 +++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index ab88fd313..83525a683 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -91,9 +91,8 @@ public async Task ApplyFilter(CourseDTO courseDto, string userId) { var studentCourse = courseDto; if (courseFilters.TryGetValue(GlobalFilterUserId, out var groupFilter)) - { studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterOperation.Subtract); - } + return courseFilters.TryGetValue(userId, out var studentFilter) ? ApplyFilterInternal(courseDto, studentCourse, studentFilter, ApplyFilterOperation.Union) : studentCourse; @@ -144,9 +143,7 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit var filter = courseFilter?.Filter; if (filter == null) - { return editingCourseDto; - } var homeworks = filter.HomeworkIds.Any() ? filterType switch diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 4b6f3e779..2ed006269 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -8,8 +8,6 @@ using HwProj.Models; using HwProj.Models.CoursesService.ViewModels; using HwProj.NotificationService.Events.CoursesService; -using HwProj.CoursesService.API.Repositories.Groups; -using Microsoft.EntityFrameworkCore; namespace HwProj.CoursesService.API.Services { @@ -18,16 +16,16 @@ public class HomeworksService : IHomeworksService private readonly IHomeworksRepository _homeworksRepository; private readonly IEventBus _eventBus; private readonly ICoursesRepository _coursesRepository; - private readonly IGroupMatesRepository _groupMatesRepository; + private readonly IGroupsService _groupsService; private readonly ICourseFilterService _courseFilterService; public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, ICoursesRepository coursesRepository, - IGroupMatesRepository groupMatesRepository, IGroupsService groupsService, ICourseFilterService courseFilterService) + IGroupsService groupsService, ICourseFilterService courseFilterService) { _homeworksRepository = homeworksRepository; _eventBus = eventBus; _coursesRepository = coursesRepository; - _groupMatesRepository = groupMatesRepository; + _groupsService = groupsService; _courseFilterService = courseFilterService; } @@ -44,7 +42,9 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo if(homework.GroupId != null) { - var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == homework.GroupId).ToArrayAsync(); + var group = (await _groupsService.GetGroupsAsync(new [] { homework.GroupId.Value })).FirstOrDefault(); + var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); + await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, groupMates); notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } @@ -103,9 +103,10 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV if (update.GroupId != null) { - var groupMates = await _groupMatesRepository.FindAll(gm => gm.GroupId == update.GroupId).ToArrayAsync(); - await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates); + var group = (await _groupsService.GetGroupsAsync(new [] { update.GroupId.Value })).FirstOrDefault(); + var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); + await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates); notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } From 5d339e934bcd4a75d34f5c69d4e6afcaa5b52624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Sat, 11 Apr 2026 17:50:27 +0300 Subject: [PATCH 054/100] refactor: return group access way in update --- .../Services/GroupsService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 68b04bfce..4496df674 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using System.Collections.Generic; using AutoMapper; using HwProj.CoursesService.API.Models; using HwProj.CoursesService.API.Repositories.Groups; @@ -62,12 +63,11 @@ public async Task DeleteGroupAsync(long groupId) public async Task UpdateAsync(long groupId, Group updated) { - var existingGroupMates = await _groupMatesRepository - .FindAll(cm => cm.GroupId == groupId) - .ToArrayAsync() - .ConfigureAwait(false); + var groupMates = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })) + .FirstOrDefault() + .GroupMates ?? new List(); - foreach (var groupMate in existingGroupMates) + foreach (var groupMate in groupMates) { await _groupMatesRepository.DeleteAsync(groupMate.Id).ConfigureAwait(false); } From efedae429af962a336476944ad00e74bbcc0a80e Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sat, 11 Apr 2026 20:21:13 +0300 Subject: [PATCH 055/100] wip --- .../Controllers/CourseGroupsController.cs | 18 -- .../Controllers/CoursesController.cs | 1 + .../ViewModels/CourseViewModels.cs | 1 + .../ViewModels/GroupViewModel.cs | 6 - .../Controllers/CourseGroupsController.cs | 24 +-- .../Repositories/Groups/IGroupsRepository.cs | 2 +- .../Repositories/HomeworksRepository.cs | 8 - .../Repositories/IHomeworksRepository.cs | 1 - .../Services/CourseFilterService.cs | 154 ++++++-------- .../Services/CoursesService.cs | 3 +- .../Services/GroupsService.cs | 51 ++--- .../Services/HomeworksService.cs | 40 ++-- .../Services/ICourseFilterService.cs | 3 +- .../Services/IGroupsService.cs | 4 +- .../Services/IHomeworksService.cs | 2 - .../CoursesServiceClient.cs | 21 -- .../ICoursesServiceClient.cs | 2 - hwproj.front/src/api/api.ts | 194 ++---------------- .../src/components/Common/GroupSelector.tsx | 4 +- .../src/components/Courses/Course.tsx | 25 +-- .../components/Courses/CourseExperimental.tsx | 5 +- .../src/components/Courses/CourseGroups.tsx | 4 +- .../src/components/Courses/StudentStats.tsx | 4 +- .../Homeworks/CourseHomeworkExperimental.tsx | 7 +- 24 files changed, 141 insertions(+), 443 deletions(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs index 524fa0a1f..ef02af168 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CourseGroupsController.cs @@ -30,16 +30,6 @@ public async Task GetAllCourseGroups(long courseId) : Ok(result); } - [HttpGet("{courseId}/getAllWithNames")] - [ProducesResponseType(typeof(NamedGroupViewModel[]), (int)HttpStatusCode.OK)] - public async Task GetAllCourseGroupsWithNames(long courseId) - { - var result = await _coursesClient.GetAllCourseGroupsWithNames(courseId); - return result == null - ? NotFound() - : Ok(result); - } - [HttpPost("{courseId}/create")] [Authorize(Roles = Roles.LecturerRole)] [ProducesResponseType(typeof(long), (int)HttpStatusCode.OK)] @@ -84,14 +74,6 @@ public async Task AddStudentInGroup(long courseId, long groupId, return Ok(); } - [HttpPost("{courseId}/removeStudentFromGroup/{groupId}")] - [Authorize(Roles = Roles.LecturerRole)] - public async Task RemoveStudentFromGroup(long courseId, long groupId, [FromQuery] string userId) - { - await _coursesClient.RemoveStudentFromGroup(courseId, groupId, userId); - return Ok(); - } - [HttpGet("get/{groupId}")] [ProducesResponseType(typeof(GroupViewModel), (int)HttpStatusCode.OK)] public async Task GetGroup(long groupId) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 7ad6bf87a..2a256b888 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -308,6 +308,7 @@ private async Task ToCourseViewModel(CourseDTO course) AcceptedStudents = acceptedStudents.ToArray(), NewStudents = newStudents.ToArray(), Homeworks = course.Homeworks, + Groups = course.Groups, IsCompleted = course.IsCompleted, IsOpen = course.IsOpen }; diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs index 18e2b51b6..d61937109 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CourseViewModels.cs @@ -52,6 +52,7 @@ public class CourseViewModel public bool IsOpen { get; set; } public bool IsCompleted { get; set; } + public GroupViewModel[] Groups { get; set; } public AccountDataDto[] Mentors { get; set; } public AccountDataDto[] AcceptedStudents { get; set; } public AccountDataDto[] NewStudents { get; set; } diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs index 32ce94894..29794f467 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/GroupViewModel.cs @@ -4,12 +4,6 @@ namespace HwProj.Models.CoursesService.ViewModels { public class GroupViewModel - { - public long Id { get; set; } - public string[] StudentsIds { get; set; } - } - - public class NamedGroupViewModel { public long Id { get; set; } public string Name { get; set; } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs index d924ae49d..d248e5098 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Controllers/CourseGroupsController.cs @@ -28,24 +28,12 @@ public async Task GetAll(long courseId) var groups = await _groupsService.GetAllAsync(courseId); var result = groups.Select(t => new GroupViewModel - { - Id = t.Id, - StudentsIds = t.GroupMates.Select(s => s.StudentId).ToArray() - }).ToArray(); - - return result; - } - - [HttpGet("{courseId}/getAllWithNames")] - public async Task GetAllWithNames(long courseId) - { - var groups = await _groupsService.GetAllAsync(courseId); - var result = groups.Select(t => new NamedGroupViewModel { Id = t.Id, Name = t.Name, StudentsIds = t.GroupMates.Select(s => s.StudentId).ToArray() }).ToArray(); + return result; } @@ -93,15 +81,6 @@ public async Task AddStudentInGroup(long groupId, [FromQuery] str return Ok(); } - [HttpPost("{courseId}/removeStudentFromGroup/{groupId}")] - [ServiceFilter(typeof(CourseMentorOnlyAttribute))] - public async Task RemoveStudentFromGroup(long groupId, [FromQuery] string userId) - { - return await _groupsService.DeleteGroupMateAsync(groupId, userId) - ? Ok() - : NotFound() as IActionResult; - } - [HttpGet] public async Task Get([FromBody] long[] groupIds) { @@ -109,6 +88,7 @@ public async Task Get([FromBody] long[] groupIds) var result = groups.Select(group => new GroupViewModel { Id = group.Id, + Name = group.Name, StudentsIds = group.GroupMates.Select(g => g.StudentId).ToArray() }).ToArray(); return Ok(result) as IActionResult; diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs index d25c28238..375d2f953 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/Groups/IGroupsRepository.cs @@ -7,7 +7,7 @@ namespace HwProj.CoursesService.API.Repositories.Groups { public interface IGroupsRepository : ICrudRepository { - Task GetGroupsWithGroupMatesAsync(long[] ids); + Task GetGroupsWithGroupMatesAsync(params long[] ids); IQueryable GetGroupsWithGroupMatesByCourse(long courseId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs index 6c41b669b..aa663be58 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/HomeworksRepository.cs @@ -27,14 +27,6 @@ public async Task GetAllWithTasksByCourseAsync(long courseId) .ToArrayAsync(); } - public async Task GetWithTasksAsync(long[] homeworkIds, bool withCriteria = false) - { - var query = Context.Set().AsNoTracking().Include(h => h.Tasks); - return withCriteria - ? await query.ThenInclude(x => x.Criteria).Where(h => homeworkIds.Contains(h.Id)).ToArrayAsync() - : await query.Where(h => homeworkIds.Contains(h.Id)).ToArrayAsync(); - } - public async Task GetWithTasksAsync(long id, bool withCriteria = false) { var query = Context.Set().AsNoTracking().Include(h => h.Tasks); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs index d887b6d21..4c05afdaf 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/IHomeworksRepository.cs @@ -8,7 +8,6 @@ public interface IHomeworksRepository : ICrudRepository { Task GetAllWithTasksAsync(); Task GetAllWithTasksByCourseAsync(long courseId); - Task GetWithTasksAsync(long[] homeworkIds, bool withCriteria = false); Task GetWithTasksAsync(long id, bool withCriteria = false); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 83525a683..16df85273 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -16,6 +16,7 @@ public enum ApplyFilterOperation Union, Subtract } + public class CourseFilterService : ICourseFilterService { private const string GlobalFilterUserId = ""; @@ -65,56 +66,56 @@ await _courseFilterRepository.UpdateAsync(courseFilterId, f => public async Task ApplyFiltersToCourses(string userId, CourseDTO[] courses) { - var courseIds = courses.Select(c => c.Id).ToArray(); - var result = new List(); foreach (var course in courses) { result.Add(await ApplyFilter(course, userId)); } + return result.ToArray(); } - public async Task ApplyFilter(CourseDTO courseDto, string userId) + public async Task ApplyFilter(CourseDTO course, string userId) { - var isMentor = courseDto.MentorIds.Contains(userId); - var isCourseStudent = courseDto.AcceptedStudents.Any(t => t.StudentId == userId); + var isMentor = course.MentorIds.Contains(userId); + var isCourseStudent = course.AcceptedStudents.Any(t => t.StudentId == userId); + var findFiltersFor = isMentor || !isCourseStudent ? new[] { userId, GlobalFilterUserId } - : courseDto.MentorIds.Concat(new[] { userId, GlobalFilterUserId }).ToArray(); + : course.MentorIds.Concat(new[] { userId, GlobalFilterUserId }).ToArray(); var courseFilters = - (await _courseFilterRepository.GetAsync(findFiltersFor, courseDto.Id)) + (await _courseFilterRepository.GetAsync(findFiltersFor, course.Id)) .ToDictionary(x => x.UserId, x => x.CourseFilter); if (!isMentor) { - var studentCourse = courseDto; + var studentCourse = course; if (courseFilters.TryGetValue(GlobalFilterUserId, out var groupFilter)) - studentCourse = ApplyFilterInternal(courseDto, studentCourse, groupFilter, ApplyFilterOperation.Subtract); - - return courseFilters.TryGetValue(userId, out var studentFilter) - ? ApplyFilterInternal(courseDto, studentCourse, studentFilter, ApplyFilterOperation.Union) - : studentCourse; + studentCourse = ApplyFilterInternal(course, studentCourse, groupFilter, + ApplyFilterOperation.Subtract); + + studentCourse = courseFilters.TryGetValue(userId, out var studentFilter) + ? ApplyFilterInternal(course, studentCourse, studentFilter, ApplyFilterOperation.Union) + : studentCourse; + + var mentorIds = course.MentorIds + .Where(u => + // Фильтрация не настроена вообще + !courseFilters.TryGetValue(u, out var courseFilter) || + // Не отфильтрованы студенты + !courseFilter.Filter.StudentIds.Any() || + // Фильтр содержит студента + courseFilter.Filter.StudentIds.Contains(userId)) + .ToArray(); + + studentCourse.MentorIds = mentorIds; + return studentCourse; } - var course = courseFilters.TryGetValue(userId, out var userFilter) - ? ApplyFilterInternal(courseDto, courseDto, userFilter, ApplyFilterOperation.Intersect) - : courseDto; - if (isMentor || !isCourseStudent) return course; - - var mentorIds = course.MentorIds - .Where(u => - // Фильтрация не настроена вообще - !courseFilters.TryGetValue(u, out var courseFilter) || - // Не отфильтрованы студенты - !courseFilter.Filter.StudentIds.Any() || - // Фильтр содержит студента - courseFilter.Filter.StudentIds.Contains(userId)) - .ToArray(); - - courseDto.MentorIds = mentorIds; - return course; + return courseFilters.TryGetValue(userId, out var userFilter) + ? ApplyFilterInternal(course, course, userFilter, ApplyFilterOperation.Intersect) + : course; } public async Task GetAssignedStudentsIds(long courseId, string[] mentorsIds) @@ -138,14 +139,15 @@ private async Task AddCourseFilter(Filter filter, long courseId, string us return courseFilterId; } - private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, CourseFilter? courseFilter, ApplyFilterOperation filterType) + private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO editingCourseDto, + CourseFilter? courseFilter, ApplyFilterOperation filterType) { var filter = courseFilter?.Filter; if (filter == null) return editingCourseDto; - var homeworks = filter.HomeworkIds.Any() + var homeworks = filter.HomeworkIds.Count != 0 ? filterType switch { ApplyFilterOperation.Intersect => editingCourseDto.Homeworks @@ -182,6 +184,7 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit ? new GroupViewModel { Id = gs.Id, + Name = gs.Name, StudentsIds = filteredStudentsIds } : null; @@ -201,71 +204,50 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit }; } - public async Task UpdateGroupFilters(long courseId, long homeworkId, GroupMate[] groupMates) + public async Task UpdateGroupFilters(long courseId, long homeworkId, IEnumerable studentIds) { - // Добавление группового домашнего задания в глобальный фильтр курса - var globalFilter = await _courseFilterRepository.GetAsync(GlobalFilterUserId, courseId); + var filterIds = studentIds.Union(new[] { GlobalFilterUserId }).ToArray(); + var filters = (await _courseFilterRepository.GetAsync(filterIds, courseId)) + .ToDictionary(x => x.UserId, x => x.CourseFilter); - if (globalFilter != null) + foreach (var filterId in filterIds) { - var filter = globalFilter.Filter; - - if (!filter.HomeworkIds.Contains(homeworkId)) - filter.HomeworkIds.Add(homeworkId); + await AddOrUpdateHomeworkToFilter(filters.GetValueOrDefault(filterId), filterId, courseId, homeworkId); + } + } - await _courseFilterRepository.UpdateAsync(globalFilter.Id, f => - new CourseFilter - { - FilterJson = new CourseFilter { Filter = filter }.FilterJson - }); + private async Task AddOrUpdateHomeworkToFilter(CourseFilter filter, string userId, long courseId, + long homeworkId) + { + if (filter != null) + { + await UpdateFilterWithHomework(filter, homeworkId); } else { - var newFilter = new Filter - { - StudentIds = new List(), - HomeworkIds = new List { homeworkId }, - MentorIds = new List(), - }; - - await _courseFilterRepository.AddAsync(new CourseFilter { Filter = newFilter }, GlobalFilterUserId, courseId); + await CreateFilterWithHomework(userId, courseId, homeworkId); } + } - // Добавление группового домашнего задания в персональные фильтры участников группы - var studentIds = groupMates.Select(gm => gm.StudentId).ToArray(); - var studentFilters = (await _courseFilterRepository.GetAsync(studentIds, courseId)) - .ToDictionary(x => x.UserId, x => x.CourseFilter); - - foreach (var groupMate in groupMates) + private async Task UpdateFilterWithHomework(CourseFilter courseFilter, long homeworkId) + { + if (!courseFilter.Filter.HomeworkIds.Contains(homeworkId)) { - if (studentFilters.TryGetValue(groupMate.StudentId, out var studentFilter)) - { - var filter = studentFilter.Filter; - if (!filter.HomeworkIds.Contains(homeworkId)) - filter.HomeworkIds.Add(homeworkId); - - await _courseFilterRepository.UpdateAsync(studentFilter.Id, f => - new CourseFilter - { - FilterJson = new CourseFilter { Filter = filter }.FilterJson - }); - } - else - { - var newFilter = new Filter - { - StudentIds = new List(), - HomeworkIds = new List { homeworkId }, - MentorIds = new List() - }; - - await _courseFilterRepository.AddAsync( - new CourseFilter { Filter = newFilter }, - groupMate.StudentId, - courseId - ); - } + courseFilter.Filter.HomeworkIds.Add(homeworkId); + await UpdateAsync(courseFilter.Id, courseFilter.Filter); } } + + private async Task CreateFilterWithHomework(string userId, long courseId, long homeworkId) + { + var newFilter = new Filter + { + StudentIds = new List(), + HomeworkIds = new List { homeworkId }, + MentorIds = new List() + }; + + await AddCourseFilter(newFilter, courseId, userId); + } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs index 9293160d4..dca5492ec 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CoursesService.cs @@ -74,12 +74,13 @@ public async Task GetAllAsync() CourseDomain.FillTasksInCourses(course); - var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToArrayAsync(); + var groups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(course.Id).ToListAsync(); var courseDto = course.ToCourseDto(); courseDto.Groups = groups.Select(g => new GroupViewModel { Id = g.Id, + Name = g.Name, StudentsIds = g.GroupMates.Select(t => t.StudentId).ToArray() }).ToArray(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 4496df674..f5688a8ad 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -63,47 +63,32 @@ public async Task DeleteGroupAsync(long groupId) public async Task UpdateAsync(long groupId, Group updated) { - var groupMates = (await _groupsRepository.GetGroupsWithGroupMatesAsync(new[] { groupId })) - .FirstOrDefault() - .GroupMates ?? new List(); + var group = (await _groupsRepository.GetGroupsWithGroupMatesAsync(groupId)).SingleOrDefault(); - foreach (var groupMate in groupMates) - { - await _groupMatesRepository.DeleteAsync(groupMate.Id).ConfigureAwait(false); - } + if (group == null) return; - updated.GroupMates?.ForEach(cm => cm.GroupId = groupId); + var updatedGroupMates = updated.GroupMates ?? new List(); - if (updated.GroupMates != null && updated.GroupMates.Count > 0) - { - await _groupMatesRepository.AddRangeAsync(updated.GroupMates).ConfigureAwait(false); - } + var currentStudentIds = (group.GroupMates?.Select(gm => gm.StudentId) ?? Enumerable.Empty()).ToHashSet(); + var updatedStudentIds = updatedGroupMates.Select(gm => gm.StudentId).ToHashSet(); - await _groupsRepository.UpdateAsync(groupId, g => new Group - { - Name = updated.Name - }).ConfigureAwait(false); - } + var studentsToAdd = updatedStudentIds.Except(currentStudentIds).ToList(); + var studentsToRemove = currentStudentIds.Except(updatedStudentIds).ToList(); - public async Task DeleteGroupMateAsync(long groupId, string studentId) - { - var group = await _groupsRepository.GetAsync(groupId).ConfigureAwait(false); - if (group == null) - { - return false; - } - - var getGroupMateTask = - await _groupMatesRepository.FindAsync(cm => cm.GroupId == groupId && cm.StudentId == studentId).ConfigureAwait(false); - if (getGroupMateTask == null) - { - return false; - } + var groupMatesToAdd = updatedGroupMates.Where(x => studentsToAdd.Contains(x.StudentId)).ToArray(); + foreach (var groupMate in groupMatesToAdd) + groupMate.GroupId = groupId; + await _groupMatesRepository.AddRangeAsync(groupMatesToAdd); + await _groupMatesRepository + .FindAll(x => x.GroupId == groupId && studentsToRemove.Contains(x.StudentId)) + .DeleteFromQueryAsync(); - await _groupMatesRepository.DeleteAsync(getGroupMateTask.Id); - return true; + await _groupsRepository.UpdateAsync(groupId, g => new Group + { + Name = updated.Name + }); } public async Task GetStudentGroupsAsync(long courseId, string studentId) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 2ed006269..92bd924a0 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -19,7 +19,8 @@ public class HomeworksService : IHomeworksService private readonly IGroupsService _groupsService; private readonly ICourseFilterService _courseFilterService; - public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, ICoursesRepository coursesRepository, + public HomeworksService(IHomeworksRepository homeworksRepository, IEventBus eventBus, + ICoursesRepository coursesRepository, IGroupsService groupsService, ICourseFilterService courseFilterService) { _homeworksRepository = homeworksRepository; @@ -36,22 +37,23 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo homework.CourseId = courseId; var course = await _coursesRepository.GetWithCourseMatesAndHomeworksAsync(courseId); - var notificationStudentIds = course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); + var notifyStudentIds = + course.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); await _homeworksRepository.AddAsync(homework); - if(homework.GroupId != null) + if (homework.GroupId is { } groupId) { - var group = (await _groupsService.GetGroupsAsync(new [] { homework.GroupId.Value })).FirstOrDefault(); + var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); - await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, groupMates); - notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, groupMates.Select(gm => gm.StudentId)); + notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } if (DateTime.UtcNow >= homework.PublicationDate) { - _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, notificationStudentIds, + _eventBus.Publish(new NewHomeworkEvent(homework.Title, course.Name, course.Id, notifyStudentIds, homework.DeadlineDate)); } @@ -67,18 +69,6 @@ public async Task GetHomeworkAsync(long homeworkId, bool withCriteria return homework; } - public async Task GetHomeworksAsync(long[] homeworkIds, bool withCriteria = false) - { - var homeworks = await _homeworksRepository.GetWithTasksAsync(homeworkIds, withCriteria); - - foreach (var homework in homeworks) - { - CourseDomain.FillTasksInHomework(homework); - } - - return homeworks; - } - public async Task GetForEditingHomeworkAsync(long homeworkId) { var result = await _homeworksRepository.GetWithTasksAsync(homeworkId); @@ -99,19 +89,19 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var homework = await _homeworksRepository.GetAsync(homeworkId); var course = await _coursesRepository.GetWithCourseMates(homework.CourseId); var studentIds = course!.CourseMates.Where(cm => cm.IsAccepted).Select(cm => cm.StudentId).ToArray(); - var notificationStudentIds = studentIds; + var notifyStudentIds = studentIds; - if (update.GroupId != null) + if (update.GroupId is { } groupId) { - var group = (await _groupsService.GetGroupsAsync(new [] { update.GroupId.Value })).FirstOrDefault(); + var group = (await _groupsService.GetGroupsAsync(groupId)).FirstOrDefault(); var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); - await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates); - notificationStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); + await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates.Select(gm => gm.StudentId)); + notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } if (options.SendNotification && update.PublicationDate <= DateTime.UtcNow) - _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notificationStudentIds)); + _eventBus.Publish(new UpdateHomeworkEvent(update.Title, course.Id, course.Name, notifyStudentIds)); await _homeworksRepository.UpdateAsync(homeworkId, hw => new Homework() { diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs index 9c0fda18c..9a599e4f5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using HwProj.CoursesService.API.Models; using HwProj.Models.CoursesService; @@ -14,6 +15,6 @@ public interface ICourseFilterService Task ApplyFiltersToCourses(string userId, CourseDTO[] courses); Task ApplyFilter(CourseDTO courseDto, string userId); Task GetAssignedStudentsIds(long courseId, string[] mentorsIds); - Task UpdateGroupFilters(long courseId, long homeworkId, GroupMate[] groupMates); + Task UpdateGroupFilters(long courseId, long homeworkId, IEnumerable studentIds); } } \ No newline at end of file diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs index 2a3ce818c..60862f33e 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IGroupsService.cs @@ -8,7 +8,7 @@ public interface IGroupsService { Task GetAllAsync(long courseId); - Task GetGroupsAsync(long[] groupIds); + Task GetGroupsAsync(params long[] groupIds); Task AddGroupAsync(Group group); @@ -18,8 +18,6 @@ public interface IGroupsService Task AddGroupMateAsync(long groupId, string studentId); - Task DeleteGroupMateAsync(long groupId, string studentId); - Task GetStudentGroupsAsync(long courseId, string studentId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs index 606fea6d7..2b2460c37 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/IHomeworksService.cs @@ -10,8 +10,6 @@ public interface IHomeworksService Task GetHomeworkAsync(long homeworkId, bool withCriteria = false); - Task GetHomeworksAsync(long[] homeworkIds, bool withCriteria = false); - Task GetForEditingHomeworkAsync(long homeworkId); Task DeleteHomeworkAsync(long homeworkId); diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs index 8e527c76b..c82dcdcbe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/CoursesServiceClient.cs @@ -414,16 +414,6 @@ public async Task GetAllCourseGroups(long courseId) return await response.DeserializeAsync(); } - public async Task GetAllCourseGroupsWithNames(long courseId) - { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Get, - _coursesServiceUri + $"api/CourseGroups/{courseId}/getAllWithNames"); - - var response = await _httpClient.SendAsync(httpRequest); - return await response.DeserializeAsync(); - } - public async Task CreateCourseGroup(CreateGroupViewModel model, long courseId) { using var httpRequest = new HttpRequestMessage( @@ -491,17 +481,6 @@ public async Task AddStudentInGroup(long courseId, long groupId, string userId) await _httpClient.SendAsync(httpRequest); } - public async Task RemoveStudentFromGroup(long courseId, long groupId, string userId) - { - using var httpRequest = new HttpRequestMessage( - HttpMethod.Post, - _coursesServiceUri + $"api/CourseGroups/{courseId}/removeStudentFromGroup/{groupId}?userId={userId}"); - - httpRequest.TryAddUserId(_httpContextAccessor); - - await _httpClient.SendAsync(httpRequest); - } - public async Task GetGroupsById(params long[] groupIds) { if (groupIds.Length == 0) return Array.Empty(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs index 84413bbbe..9bcb031c1 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.Client/ICoursesServiceClient.cs @@ -37,13 +37,11 @@ Task UpdateStudentCharacteristics(long courseId, string studentId, Task DeleteTask(long taskId); Task> UpdateTask(long taskId, PostTaskViewModel taskViewModel); Task GetAllCourseGroups(long courseId); - Task GetAllCourseGroupsWithNames(long courseId); Task CreateCourseGroup(CreateGroupViewModel model, long courseId); Task DeleteCourseGroup(long courseId, long groupId); Task UpdateCourseGroup(UpdateGroupViewModel model, long courseId, long groupId); Task GetCourseGroupsById(long courseId, string userId); Task AddStudentInGroup(long courseId, long groupId, string userId); - Task RemoveStudentFromGroup(long courseId, long groupId, string userId); Task GetGroupsById(params long[] groupIds); Task GetGroupTasks(long groupId); Task AcceptLecturer(long courseId, string lecturerEmail, string lecturerId); diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 7db10c1ed..4e0c6dfec 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -477,6 +477,12 @@ export interface CourseViewModel { * @memberof CourseViewModel */ isCompleted?: boolean; + /** + * + * @type {Array} + * @memberof CourseViewModel + */ + groups?: Array; /** * * @type {Array} @@ -636,6 +642,12 @@ export interface CreateHomeworkViewModel { * @memberof CreateHomeworkViewModel */ actionOptions?: ActionOptions; + /** + * + * @type {number} + * @memberof CreateHomeworkViewModel + */ + groupId?: number; } /** * @@ -1058,35 +1070,16 @@ export interface GroupViewModel { * @memberof GroupViewModel */ id?: number; - /** - * - * @type {Array} - * @memberof GroupViewModel - */ - studentsIds?: Array; -} -/** - * - * @export - * @interface Group - */ -export interface NamedGroupViewModel { /** * * @type {string} - * @memberof Group + * @memberof GroupViewModel */ name?: string; - /** - * - * @type {number} - * @memberof Group - */ - id?: number; /** * * @type {Array} - * @memberof Group + * @memberof GroupViewModel */ studentsIds?: Array; } @@ -4000,36 +3993,6 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config options: localVarRequestOptions, }; }, - courseGroupsGetAllCourseGroupsWithNames(courseId: number, options: any = {}): FetchArgs { - // verify required parameter 'courseId' is not null or undefined - if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling courseGroupsGetAllCourseGroupsWithNames.'); - } - const localVarPath = `/api/CourseGroups/{courseId}/getAllWithNames` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); - const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'GET' }, options); - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication Bearer required - if (configuration && configuration.apiKey) { - const localVarApiKeyValue = typeof configuration.apiKey === 'function' - ? configuration.apiKey("Authorization") - : configuration.apiKey; - localVarHeaderParameter["Authorization"] = localVarApiKeyValue; - } - - localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); - // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - localVarUrlObj.search = null; - localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - - return { - url: url.format(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {number} courseId @@ -4138,53 +4101,6 @@ export const CourseGroupsApiFetchParamCreator = function (configuration?: Config options: localVarRequestOptions, }; }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options: any = {}): FetchArgs { - // verify required parameter 'courseId' is not null or undefined - if (courseId === null || courseId === undefined) { - throw new RequiredError('courseId','Required parameter courseId was null or undefined when calling courseGroupsRemoveStudentFromGroup.'); - } - // verify required parameter 'groupId' is not null or undefined - if (groupId === null || groupId === undefined) { - throw new RequiredError('groupId','Required parameter groupId was null or undefined when calling courseGroupsRemoveStudentFromGroup.'); - } - const localVarPath = `/api/CourseGroups/{courseId}/removeStudentFromGroup/{groupId}` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))) - .replace(`{${"groupId"}}`, encodeURIComponent(String(groupId))); - const localVarUrlObj = url.parse(localVarPath, true); - const localVarRequestOptions = Object.assign({ method: 'POST' }, options); - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication Bearer required - if (configuration && configuration.apiKey) { - const localVarApiKeyValue = typeof configuration.apiKey === 'function' - ? configuration.apiKey("Authorization") - : configuration.apiKey; - localVarHeaderParameter["Authorization"] = localVarApiKeyValue; - } - - if (userId !== undefined) { - localVarQueryParameter['userId'] = userId; - } - - localVarUrlObj.query = Object.assign({}, localVarUrlObj.query, localVarQueryParameter, options.query); - // fix override query string Detail: https://stackoverflow.com/a/7517673/1077943 - localVarUrlObj.search = null; - localVarRequestOptions.headers = Object.assign({}, localVarHeaderParameter, options.headers); - - return { - url: url.format(localVarUrlObj), - options: localVarRequestOptions, - }; - }, /** * * @param {number} courseId @@ -4317,24 +4233,6 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }); }; }, - /** - * - * @param {number} courseId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise> { - const localVarFetchArgs = CourseGroupsApiFetchParamCreator(configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options); - return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { - return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { - if (response.status >= 200 && response.status < 300) { - return response.json(); - } else { - throw response; - } - }); - }; - }, /** * * @param {number} courseId @@ -4389,26 +4287,6 @@ export const CourseGroupsApiFp = function(configuration?: Configuration) { }); }; }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any): (fetch?: FetchAPI, basePath?: string) => Promise { - const localVarFetchArgs = CourseGroupsApiFetchParamCreator(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options); - return (fetch: FetchAPI = isomorphicFetch, basePath: string = BASE_PATH) => { - return fetch(basePath + localVarFetchArgs.url, localVarFetchArgs.options).then((response) => { - if (response.status >= 200 && response.status < 300) { - return response; - } else { - throw response; - } - }); - }; - }, /** * * @param {number} courseId @@ -4478,15 +4356,6 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f courseGroupsGetAllCourseGroups(courseId: number, options?: any) { return CourseGroupsApiFp(configuration).courseGroupsGetAllCourseGroups(courseId, options)(fetch, basePath); }, - /** - * - * @param {number} courseId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any) { - return CourseGroupsApiFp(configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options)(fetch, basePath); - }, /** * * @param {number} courseId @@ -4514,17 +4383,6 @@ export const CourseGroupsApiFactory = function (configuration?: Configuration, f courseGroupsGetGroupTasks(groupId: number, options?: any) { return CourseGroupsApiFp(configuration).courseGroupsGetGroupTasks(groupId, options)(fetch, basePath); }, - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any) { - return CourseGroupsApiFp(configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(fetch, basePath); - }, /** * * @param {number} courseId @@ -4594,17 +4452,6 @@ export class CourseGroupsApi extends BaseAPI { return CourseGroupsApiFp(this.configuration).courseGroupsGetAllCourseGroups(courseId, options)(this.fetch, this.basePath); } - /** - * - * @param {number} courseId - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CourseGroupsApi - */ - public courseGroupsGetAllCourseGroupsWithNames(courseId: number, options?: any) { - return CourseGroupsApiFp(this.configuration).courseGroupsGetAllCourseGroupsWithNames(courseId, options)(this.fetch, this.basePath); - } - /** * * @param {number} courseId @@ -4638,19 +4485,6 @@ export class CourseGroupsApi extends BaseAPI { return CourseGroupsApiFp(this.configuration).courseGroupsGetGroupTasks(groupId, options)(this.fetch, this.basePath); } - /** - * - * @param {number} courseId - * @param {number} groupId - * @param {string} [userId] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CourseGroupsApi - */ - public courseGroupsRemoveStudentFromGroup(courseId: number, groupId: number, userId?: string, options?: any) { - return CourseGroupsApiFp(this.configuration).courseGroupsRemoveStudentFromGroup(courseId, groupId, userId, options)(this.fetch, this.basePath); - } - /** * * @param {number} courseId diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index e2e77c2ac..26d93f369 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -17,13 +17,13 @@ import { import EditIcon from "@mui/icons-material/Edit"; import AddIcon from "@mui/icons-material/Add"; import ApiSingleton from "../../api/ApiSingleton"; -import { NamedGroupViewModel, AccountDataDto } from "@/api"; +import { GroupViewModel, AccountDataDto } from "@/api"; interface GroupSelectorProps { courseId: number, courseStudents: AccountDataDto[], - groups: NamedGroupViewModel[], + groups: GroupViewModel[], onGroupIdChange: (groupId?: number) => void, onGroupsUpdate: () => void, selectedGroupId?: number, diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index b103c340f..d20b6b8e7 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, NamedGroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -35,7 +35,6 @@ import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; import CourseGroups from "./CourseGroups"; -import { group } from "@uiw/react-md-editor"; type TabValue = "homeworks" | "stats" | "applications" | "groups" @@ -88,6 +87,9 @@ const Course: React.FC = () => { courseHomeworks, } = courseState + const groups = course.groups || [] + const loadGroups = async () => setCurrentState(); + const userId = ApiSingleton.authService.getUserId() const isLecturer = ApiSingleton.authService.isLecturer() @@ -172,28 +174,11 @@ const Course: React.FC = () => { const [lecturerStatsState, setLecturerStatsState] = useState(false); - const [groups, setGroups] = useState([]); - const [groupLoadingError, setGroupLoadingError] = useState(false); - const studentsWithoutGroup = useMemo(() => { const inGroupIds = new Set(groups.flatMap(g => g.studentsIds)); return acceptedStudents.filter(s => !inGroupIds.has(s.userId!)); }, [groups, acceptedStudents]); - const loadGroups = async () => { - setGroupLoadingError(false); - try { - const result = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroupsWithNames(+courseId!); - setGroups(result.filter(g => g.name && g.name.trim().length > 0)); - } catch { - setGroupLoadingError(true); - } - }; - - useEffect(() => { - loadGroups(); - }, [courseId]); - const CourseMenu: FC = () => { const [anchorEl, setAnchorEl] = React.useState(null); const open = Boolean(anchorEl); @@ -322,7 +307,7 @@ const Course: React.FC = () => { } - {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && !groupLoadingError && + {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && Студентов, не записанных в группу: {studentsWithoutGroup.length} diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 9f7f78e3f..6402d4893 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import { - FileInfoDTO, - NamedGroupViewModel, + FileInfoDTO, GroupViewModel, HomeworkTaskViewModel, HomeworkViewModel, SolutionDto, StatisticsCourseMatesModel, } from "@/api"; @@ -62,7 +61,7 @@ interface ICourseExperimentalProps { waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: NamedGroupViewModel[]; + groups: GroupViewModel[]; } interface ICourseExperimentalState { diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx index a22edefb8..d6e55e0a7 100644 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ b/hwproj.front/src/components/Courses/CourseGroups.tsx @@ -9,11 +9,11 @@ import { Stack, } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import {AccountDataDto, NamedGroupViewModel} from "@/api"; +import {AccountDataDto, GroupViewModel} from "@/api"; interface ICourseGroupsProps { courseStudents: AccountDataDto[]; - groups: NamedGroupViewModel[]; + groups: GroupViewModel[]; onGroupsUpdate: () => void; } diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index d07961061..5880e3fae 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState, useRef} from "react"; -import {CourseViewModel, NamedGroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import {useNavigate, useParams} from 'react-router-dom'; import {LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; @@ -19,7 +19,7 @@ interface IStudentStatsProps { isMentor: boolean; userId: string; solutions: StatisticsCourseMatesModel[] | undefined; - groups: NamedGroupViewModel[]; + groups: GroupViewModel[]; } interface IStudentStatsState { diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 1355370d8..da4dfd997 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -18,8 +18,7 @@ import {IFileInfo} from "components/Files/IFileInfo"; import {FC, useEffect, useState} from "react" import Utils from "services/Utils"; import { - HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, - NamedGroupViewModel + HomeworkViewModel, ActionOptions, HomeworkTaskViewModel, PostTaskViewModel, AccountDataDto, GroupViewModel } from "@/api"; import ApiSingleton from "../../api/ApiSingleton"; import Tags from "../Common/Tags"; @@ -66,7 +65,7 @@ const CourseHomeworkEditor: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: NamedGroupViewModel[]; + groups: GroupViewModel[]; }> = (props) => { const homework = props.homeworkAndFilesInfo.homework const isNewHomework = homework.id! < 0 @@ -424,7 +423,7 @@ const CourseHomeworkExperimental: FC<{ waitingNewFilesCount: number, deletingFilesIds: number[]) => void; onGroupsUpdate: () => void; - groups: NamedGroupViewModel[]; + groups: GroupViewModel[]; }> = (props) => { const {homework, filesInfo} = props.homeworkAndFilesInfo const deferredTasks = homework.tasks!.filter(t => t.isDeferred!) From 1e2de3a4217b391e38ca967eb32e2e1d412ae57c Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sat, 11 Apr 2026 21:33:27 +0300 Subject: [PATCH 056/100] wip --- .../Homeworks/CourseHomeworkExperimental.tsx | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index da4dfd997..f9602276c 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -9,6 +9,8 @@ IconButton, Stack, TextField, + ToggleButton, + ToggleButtonGroup, Tooltip, Typography, } from "@mui/material"; @@ -38,6 +40,8 @@ import {CourseUnitType} from "../Files/CourseUnitType" import ProcessFilesUtils from "../Utils/ProcessFilesUtils"; import {FilesHandler} from "@/components/Files/FilesHandler"; import GroupSelector from "../Common/GroupSelector"; +import GroupIcon from '@mui/icons-material/Group'; +import AssignmentIcon from '@mui/icons-material/Assignment'; export interface HomeworkAndFilesInfo { homework: HomeworkViewModel & { isModified?: boolean }, @@ -119,6 +123,7 @@ const CourseHomeworkEditor: FC<{ const [description, setDescription] = useState(loadedHomework.description!) const [selectedGroupId, setSelectedGroupId] = useState(loadedHomework.groupId) const [courseStudents, setCourseStudents] = useState([]) + const [page, setPage] = useState<"homework" | "group">("homework") useEffect(() => { const loadCourseStudents = async () => { @@ -278,8 +283,21 @@ const CourseHomeworkEditor: FC<{ const isDisabled = hasErrors || !isLoaded || taskHasErrors - return ( - + return + setPage(x)} + > + + + + + + + + {page === "homework" && @@ -306,15 +324,6 @@ const CourseHomeworkEditor: FC<{ - setSelectedGroupId(groupId)} - selectedGroupId={selectedGroupId} - choiceDisabled={!isNewHomework} - onGroupsUpdate={props.onGroupsUpdate} - groups={props.groups} - /> {tags.includes(TestTag) && @@ -402,8 +411,19 @@ const CourseHomeworkEditor: FC<{ confirmationWord={''} confirmationText={''} /> - - ) + } + {page === "group" && + setSelectedGroupId(groupId)} + selectedGroupId={selectedGroupId} + choiceDisabled={!isNewHomework} + onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} + /> + } + } const CourseHomeworkExperimental: FC<{ From afe4a62d97e60676295f8e2904fe809ed3f26aa4 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sat, 11 Apr 2026 21:39:19 +0300 Subject: [PATCH 057/100] wip --- .../src/components/Homeworks/CourseHomeworkExperimental.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index f9602276c..78b790a62 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -286,6 +286,7 @@ const CourseHomeworkEditor: FC<{ return setPage(x)} @@ -412,7 +413,7 @@ const CourseHomeworkEditor: FC<{ confirmationText={''} /> } - {page === "group" && + {page === "group" && Date: Sat, 11 Apr 2026 21:41:41 +0300 Subject: [PATCH 058/100] wip --- .../src/components/Homeworks/CourseHomeworkExperimental.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 78b790a62..023ee65f1 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -292,10 +292,10 @@ const CourseHomeworkEditor: FC<{ onChange={(_, x) => setPage(x)} > - + - + {page === "homework" && From 131448a76eefba87cba15ac21f11deaf0153a5de Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 02:20:03 +0300 Subject: [PATCH 059/100] wip --- .../src/components/Common/GroupSelector.tsx | 195 +++++++----------- 1 file changed, 77 insertions(+), 118 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 26d93f369..1e7a63c24 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -1,4 +1,4 @@ -import {FC, useMemo, useState} from "react"; +import {FC, useEffect, useMemo, useState} from "react"; import { Grid, TextField, @@ -17,7 +17,7 @@ import { import EditIcon from "@mui/icons-material/Edit"; import AddIcon from "@mui/icons-material/Add"; import ApiSingleton from "../../api/ApiSingleton"; -import { GroupViewModel, AccountDataDto } from "@/api"; +import {GroupViewModel, AccountDataDto} from "@/api"; interface GroupSelectorProps { @@ -31,8 +31,9 @@ interface GroupSelectorProps { onCreateNewGroup?: () => void, } -const GroupSelector: FC = (props) => { - const groups = props.groups || []; +const GroupSelector: FC = (props) => { + const groups = [...(props.groups || []), {id: undefined, name: "Все студенты"}] + const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState<{ name: string, @@ -44,11 +45,9 @@ const GroupSelector: FC = (props) => { const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); - const selectedGroup = useMemo(() => - groups.find(g => g.id === props.selectedGroupId), - [groups, props.selectedGroupId]); + const selectedGroup = groups.find(g => g.id === props.selectedGroupId) - const studentsWithousGroup = useMemo(() => { + const studentsWithoutGroup = useMemo(() => { const studentsInGroups = groups.flatMap(g => g.studentsIds) return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) }, [groups, props.courseStudents]); @@ -76,7 +75,7 @@ const GroupSelector: FC = (props) => { selectedGroup.id!, { name: formState.name, - groupMates: formState.memberIds.map(studentId => ({ studentId })), + groupMates: formState.memberIds.map(studentId => ({studentId})), } ); props.onGroupsUpdate(); @@ -103,70 +102,85 @@ const GroupSelector: FC = (props) => { } return ( - - {props.choiceDisabled ? ( - - - {selectedGroup && ( - + /> )} - - ) : ( - + /> + + {selectedGroup && + option.name || ""} - value={props.selectedGroupId !== undefined - ? groups.find(g => g.id === props.selectedGroupId) || null - : { id: undefined, name: "Все студенты" }} - onChange={(_, newGroup) => { - props.onGroupIdChange(newGroup?.id) + multiple + fullWidth + options={studentsWithoutGroup} + value={props.courseStudents?.filter(s => formState.memberIds.includes(s.userId!)) || []} + getOptionLabel={(option) => + `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() + } + filterSelectedOptions + onChange={(_, values) => { + if (selectedGroup) { + // При редактировании выбранной группы можно только добавлять студентов + setFormState(prev => ({ + ...prev, + memberIds: [...formState.memberIds, + ...values.map(x => !formState.memberIds.includes(x.userId!) ? x.userId! : "").filter(Boolean)] + })) + } else { + setFormState(prev => ({ + ...prev, + memberIds: values + .map(x => x.userId!) + .filter(Boolean) + })) + } }} + disabled={isSubmitting} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => ( + + )) + } renderInput={(params) => ( )} /> - {selectedGroup && ( - - )} - {!selectedGroup && ( - - )} + - )} - + } = (props) => { disabled={isSubmitting || props.choiceDisabled} /> - - formState.memberIds.includes(s.userId!)) || []} - getOptionLabel={(option) => - `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() - } - filterSelectedOptions - onChange={(_, values) => { - if (selectedGroup) { - // При редактировании выбранной группы можно только добавлять студентов - setFormState(prev => ({ - ...prev, - memberIds: [...formState.memberIds, - ...values.map(x => !formState.memberIds.includes(x.userId!) ? x.userId! : "").filter(Boolean)] - })) - } else { - setFormState(prev => ({ - ...prev, - memberIds: values - .map(x => x.userId!) - .filter(Boolean) - })) - } - }} - disabled={isSubmitting} - renderTags={(tagValue, getTagProps) => - tagValue.map((option, index) => ( - - )) - } - renderInput={(params) => ( - - )} - /> - @@ -255,18 +223,9 @@ const GroupSelector: FC = (props) => { > Отменить - - - ) + ) } export default GroupSelector From 62b8485089ead211aedaa5316450fac42ceef835 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 14:35:08 +0300 Subject: [PATCH 060/100] wip --- .../src/components/Common/GroupSelector.tsx | 29 ++++++++++++++----- .../Homeworks/CourseHomeworkExperimental.tsx | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 1e7a63c24..006e89c6f 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -33,20 +33,27 @@ interface GroupSelectorProps { const GroupSelector: FC = (props) => { const groups = [...(props.groups || []), {id: undefined, name: "Все студенты"}] + const selectedGroup = groups.find(g => g.id === props.selectedGroupId) const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState<{ name: string, memberIds: string[] }>({ - name: "", - memberIds: [] + name: selectedGroup?.name || "", + memberIds: selectedGroup?.studentsIds || [] }); + + useEffect(() => { + setFormState({ + name: selectedGroup?.name || "", + memberIds: selectedGroup?.studentsIds || [] + }) + }, [props.selectedGroupId]) + const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); - const selectedGroup = groups.find(g => g.id === props.selectedGroupId) - const studentsWithoutGroup = useMemo(() => { const studentsInGroups = groups.flatMap(g => g.studentsIds) return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) @@ -105,7 +112,8 @@ const GroupSelector: FC = (props) => { typeof option === 'string' ? option : option?.name || "Все студенты"} @@ -114,6 +122,11 @@ const GroupSelector: FC = (props) => { if (typeof newGroup === 'string') return props.onGroupIdChange(newGroup?.id) }} + onInputChange={(_, newInputValue, reason) => { + if (reason === 'input' && props.selectedGroupId !== undefined) { + setFormState(prevState => ({...prevState, name: newInputValue})) + } + }} renderInput={(params) => ( = (props) => { )} /> - {selectedGroup && - + {props.selectedGroupId && selectedGroup && + = (props) => { } - - - {selectedGroup ? "Редактировать группу" : "Создать группу"} - - - {isError && ( - - Ошибка - Не удалось {selectedGroup ? "создать" : "обновить"} группу. Попробуйте позже. - - )} - - - { - setFormState(prev => ({ - ...prev, - name: e.target.value - })) - }} - disabled={isSubmitting || props.choiceDisabled} - /> - - - - - - - ) } From 047a05ba3d1ae76768f4a9a25df9810ec0bb2898 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 16:01:48 +0300 Subject: [PATCH 065/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 10 +++++----- .../Homeworks/CourseHomeworkExperimental.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 50c9026d8..988431f39 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -31,7 +31,7 @@ interface GroupSelectorProps { const GroupSelector: FC = (props) => { const groups = [{id: -1, name: ""}, {id: undefined, name: "Все студенты"}, ...(props.groups || [])] - const selectedGroup = groups.find(g => g.id === props.selectedGroupId) + const selectedGroup = groups.find(g => g.id == props.selectedGroupId) const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState<{ @@ -110,15 +110,15 @@ const GroupSelector: FC = (props) => { { if (option.id === -1) return
  • + Добавить новую группу
  • - if (option.id === undefined) + if (option.id == undefined) return
  • {option.name}
  • return
  • {option.name}
  • }} @@ -129,7 +129,7 @@ const GroupSelector: FC = (props) => { if (props.selectedGroupId !== newGroup?.id) props.onGroupIdChange(newGroup?.id) }} onInputChange={(_, newInputValue, reason) => { - if (reason === 'input' && props.selectedGroupId !== undefined) { + if (reason === 'input' && props.selectedGroupId != undefined) { setFormState(prevState => ({...prevState, name: newInputValue})) } }} diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 881084071..88d924e55 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -296,7 +296,7 @@ const CourseHomeworkEditor: FC<{ - From 79ef2f5ca47edb0e4018d7656711ea536d2cbf20 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 16:11:01 +0300 Subject: [PATCH 066/100] wip --- .../src/components/Common/GroupSelector.tsx | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 988431f39..c9e0cb86d 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -4,13 +4,7 @@ import { TextField, Autocomplete, Button, - Dialog, - DialogTitle, - DialogContent, - DialogActions, Stack, - Alert, - AlertTitle, CircularProgress, Chip } from "@mui/material"; @@ -32,8 +26,6 @@ interface GroupSelectorProps { const GroupSelector: FC = (props) => { const groups = [{id: -1, name: ""}, {id: undefined, name: "Все студенты"}, ...(props.groups || [])] const selectedGroup = groups.find(g => g.id == props.selectedGroupId) - - const [isDialogOpen, setIsDialogOpen] = useState(false); const [formState, setFormState] = useState<{ name: string, memberIds: string[] @@ -57,20 +49,6 @@ const GroupSelector: FC = (props) => { return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) }, [groups, props.courseStudents]); - const handleOpenEditDialog = () => { - setFormState({ - name: selectedGroup?.name || "", - memberIds: selectedGroup?.studentsIds || [] - }) - setIsDialogOpen(true) - } - - const handleCloseEditDialog = () => { - if (isSubmitting) return; - setIsDialogOpen(false); - setIsError(false); - } - const handleSubmitEdit = async () => { setIsSubmitting(true); try { @@ -97,7 +75,6 @@ const GroupSelector: FC = (props) => { props.onGroupsUpdate(); props.onGroupIdChange(groupId); } - setIsDialogOpen(false); } catch (error) { console.error('Failed to update group:', error); setIsError(true); From 08928d15bdb98c453e0a9b807d8ac31f7b26f288 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 16:17:30 +0300 Subject: [PATCH 067/100] wip --- hwproj.front/src/components/Courses/Course.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index d20b6b8e7..e2c565a86 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, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; +import {AccountDataDto, CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMatesModel} from "@/api"; import StudentStats from "./StudentStats"; import NewCourseStudents from "./NewCourseStudents"; import ApiSingleton from "../../api/ApiSingleton"; @@ -46,6 +46,7 @@ interface ICourseState { isFound: boolean; course: CourseViewModel; courseHomeworks: HomeworkViewModel[]; + groups: GroupViewModel[]; mentors: AccountDataDto[]; acceptedStudents: AccountDataDto[]; newStudents: AccountDataDto[]; @@ -67,6 +68,7 @@ const Course: React.FC = () => { course: {}, courseHomeworks: [], mentors: [], + groups: [], acceptedStudents: [], newStudents: [], studentSolutions: [], @@ -85,10 +87,16 @@ const Course: React.FC = () => { newStudents, acceptedStudents, courseHomeworks, + groups } = courseState - const groups = course.groups || [] - const loadGroups = async () => setCurrentState(); + const loadGroups = async () => { + const groups = await ApiSingleton.courseGroupsApi.courseGroupsGetAllCourseGroups(course.id!) + setCourseState(prevState => ({ + ...prevState, + groups: groups + })) + }; const userId = ApiSingleton.authService.getUserId() @@ -142,6 +150,7 @@ const Course: React.FC = () => { courseHomeworks: course.homeworks!, createHomework: false, mentors: course.mentors!, + groups: course.groups || [], acceptedStudents: course.acceptedStudents!, newStudents: course.newStudents!, })) From 9589d0f06b2ca144d3676747d722893239236543 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 16:19:16 +0300 Subject: [PATCH 068/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index c9e0cb86d..85b08ce60 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -24,7 +24,7 @@ interface GroupSelectorProps { } const GroupSelector: FC = (props) => { - const groups = [{id: -1, name: ""}, {id: undefined, name: "Все студенты"}, ...(props.groups || [])] + const groups = [{id: -1, name: ""}, {id: undefined, name: "Все студенты"}, ...(props.groups || []).filter(x => x.name)] const selectedGroup = groups.find(g => g.id == props.selectedGroupId) const [formState, setFormState] = useState<{ name: string, From eed7e01fdba84cec69d4edbfcc37fc1f88513ed6 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Sun, 12 Apr 2026 16:23:18 +0300 Subject: [PATCH 069/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 85b08ce60..415f35cd0 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -24,7 +24,10 @@ interface GroupSelectorProps { } const GroupSelector: FC = (props) => { - const groups = [{id: -1, name: ""}, {id: undefined, name: "Все студенты"}, ...(props.groups || []).filter(x => x.name)] + const groups = [{id: -1, name: ""}, { + id: undefined, + name: "Все студенты" + }, ...(props.groups || []).filter(x => x.name)] const selectedGroup = groups.find(g => g.id == props.selectedGroupId) const [formState, setFormState] = useState<{ name: string, @@ -90,7 +93,7 @@ const GroupSelector: FC = (props) => { freeSolo={props.selectedGroupId != undefined} disableClearable={props.selectedGroupId == undefined} fullWidth - options={[...groups]} + options={props.selectedGroupId == undefined ? groups : []} renderOption={(props, option) => { if (option.id === -1) return
  • + Добавить новую From 0e7d9301e03c3516c039164abef29c763e11338e Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 13 Apr 2026 01:41:17 +0300 Subject: [PATCH 070/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 12 +++++++++++- .../Homeworks/CourseHomeworkExperimental.tsx | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 415f35cd0..a20626cf7 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -6,7 +6,9 @@ import { Button, Stack, CircularProgress, - Chip + Chip, + Alert, + AlertTitle } from "@mui/material"; import ApiSingleton from "../../api/ApiSingleton"; import {GroupViewModel, AccountDataDto} from "@/api"; @@ -180,6 +182,14 @@ const GroupSelector: FC = (props) => { } + {props.selectedGroupId == undefined && + + Создайте или выберите группу + • Задание будет доступно только студентам из группы +
    + • Вы можете изменить состав группы в любое время +
    +
    } ) } diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 88d924e55..aff6ceadd 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -417,7 +417,7 @@ const CourseHomeworkEditor: FC<{ confirmationText={''} /> } - {page === "group" && + {page === "group" && Date: Mon, 13 Apr 2026 03:38:48 +0300 Subject: [PATCH 071/100] wip --- .../src/components/Common/GroupSelector.tsx | 7 +- .../components/Courses/CourseExperimental.tsx | 19 +- .../src/components/Courses/StudentStats.tsx | 34 ++- .../Homeworks/CourseHomeworkExperimental.tsx | 205 ++++++++++-------- 4 files changed, 159 insertions(+), 106 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index a20626cf7..722e89538 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -126,7 +126,7 @@ const GroupSelector: FC = (props) => { /> {props.selectedGroupId && selectedGroup && - + = (props) => { } diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index 6402d4893..c8af487ed 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -36,6 +36,7 @@ import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import SwitchAccessShortcutIcon from '@mui/icons-material/SwitchAccessShortcut'; import Lodash from "lodash"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import GroupIcon from '@mui/icons-material/Group'; interface ICourseExperimentalProps { homeworks: HomeworkViewModel[] @@ -573,13 +574,17 @@ export const CourseExperimental: FC = (props) => { } })) }}> - - {isMentor && renderHomeworkStatus(x)} - {x.title}{getTip(x)} - + + {x.groupId && } + + {isMentor && renderHomeworkStatus(x)} + {x.title}{getTip(x)} + + {x.isDeferred && !x.publicationDateNotSet && {"🕘 " + renderDate(x.publicationDate!) + " " + renderTime(x.publicationDate!)} diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 5880e3fae..c2e8e16d1 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -3,7 +3,7 @@ import {CourseViewModel, GroupViewModel, HomeworkViewModel, StatisticsCourseMate import {useNavigate, useParams} from 'react-router-dom'; import {LinearProgress, Table, TableBody, TableCell, TableContainer, TableHead, TableRow} from "@material-ui/core"; import StudentStatsCell from "../Tasks/StudentStatsCell"; -import {Alert, Button, Chip, IconButton, Typography} from "@mui/material"; +import {Alert, Button, Chip, IconButton, Stack, Typography} from "@mui/material"; import {grey} from "@material-ui/core/colors"; import StudentStatsUtils from "../../services/StudentStatsUtils"; import ShowChartIcon from "@mui/icons-material/ShowChart"; @@ -12,6 +12,7 @@ import Lodash from "lodash" import ApiSingleton from "@/api/ApiSingleton"; import FullscreenIcon from '@mui/icons-material/Fullscreen'; import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'; +import GroupIcon from '@mui/icons-material/Group'; interface IStudentStatsProps { course: CourseViewModel; @@ -116,7 +117,7 @@ const StudentStats: React.FC = (props) => { const testsMaxSum = testGroups .map(h => h[0]) .flatMap(homework => homework.tasks) - .reduce((sum, task) => + .reduce((sum, task) => sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)), 0) const hasHomeworks = !!notTests @@ -257,12 +258,12 @@ const StudentStats: React.FC = (props) => { ) .reduce((sum, rating) => sum + rating, 0) const homeworksMaxSum = notTests - .filter(h => !h.tags!.includes(BonusTag) && - (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => { - return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) + .filter(h => !h.tags!.includes(BonusTag) && + (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) + .flatMap(homework => homework.tasks) + .reduce((sum, task) => { + return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); + }, 0) const testsSum = testGroups .map(group => { @@ -285,6 +286,8 @@ const StudentStats: React.FC = (props) => { .filter(x => x === cm.id) .toArray().length + const studentGroups = props.groups.filter(x => x.studentsIds!.includes(cm.id!)) + return ( = (props) => { variant={"head"} > {cm.surname} {cm.name} + {studentGroups.length > 0 && + + +
    {studentGroups + .map(r => r.name) + .join(', ')}
    +
    +
    } - {page === "homework" && - - - { - e.persist() - setHasErrors(prevState => prevState || !e.target.value) - setTitle(e.target.value) - }} - /> - - - apiSingleton.coursesApi.coursesGetAllTagsForCourse(courseId)}/> - - - - {tags.includes(TestTag) && + {page === "homework" &&
    + + - - Вы можете сгруппировать контрольные работы и переписывания с помощью - дополнительного тега. Например, 'КР 1' - - } - - { - setDescription(value) - }} - /> - - - - { - setFilesState((prevState) => ({ - ...prevState, - selectedFilesInfo: filesInfo - })); + { + e.persist() + setHasErrors(prevState => prevState || !e.target.value) + setTitle(e.target.value) }} - courseUnitType={CourseUnitType.Homework} - courseUnitId={homeworkId}/> - { - const conflictsWithTasks = changedTaskPublicationDates.some(d => d < metadata.publicationDate!) - setMetadata({ - hasDeadline: state.hasDeadline, - isDeadlineStrict: state.isDeadlineStrict, - publicationDate: state.publicationDate, - deadlineDate: state.deadlineDate, - hasErrors: state.hasErrors || conflictsWithTasks, - }) + /> + + + apiSingleton.coursesApi.coursesGetAllTagsForCourse(courseId)}/> + + + + {tags.includes(TestTag) && + + + Вы можете сгруппировать контрольные работы и переписывания с помощью + дополнительного тега. Например, 'КР 1' + + } + + { + setDescription(value) }} /> + + + { + setFilesState((prevState) => ({ + ...prevState, + selectedFilesInfo: filesInfo + })); + }} + courseUnitType={CourseUnitType.Homework} + courseUnitId={homeworkId}/> + { + const conflictsWithTasks = changedTaskPublicationDates.some(d => d < metadata.publicationDate!) + setMetadata({ + hasDeadline: state.hasDeadline, + isDeadlineStrict: state.isDeadlineStrict, + publicationDate: state.publicationDate, + deadlineDate: state.deadlineDate, + hasErrors: state.hasErrors || conflictsWithTasks, + }) + }} + /> + + + {taskHasErrors && + Одна или более вложенных задач содержат ошибки + } - {taskHasErrors && - Одна или более вложенных задач содержат ошибки - } - + {metadata.publicationDate && new Date() >= new Date(metadata.publicationDate) && - } - {page === "group" && - setSelectedGroupId(groupId)} - selectedGroupId={selectedGroupId} - choiceDisabled={!isNewHomework} - onGroupsUpdate={props.onGroupsUpdate} - groups={props.groups} - /> - } +
    } + {page === "group" &&
    + + setSelectedGroupId(groupId)} + selectedGroupId={selectedGroupId} + choiceDisabled={!isNewHomework} + onGroupsUpdate={props.onGroupsUpdate} + groups={props.groups} + /> + + {!isNewHomework && + + } + loading={handleSubmitLoading} + > + {"Редактировать задание"} + + } +
    } } @@ -455,6 +476,7 @@ const CourseHomeworkExperimental: FC<{ const tasksCount = homework.tasks!.length const [showEditMode, setShowEditMode] = useState(false) const [editMode, setEditMode] = useState(false) + const group = props.groups.find(g => g.id === homework.groupId) useEffect(() => { setEditMode(props.initialEditMode) @@ -514,6 +536,13 @@ const CourseHomeworkExperimental: FC<{
    } + {group && + + + +
    {group.name}
    +
    +
    } From 4ebd59df87ebab8309385f36296af8208f8b2270 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 13 Apr 2026 03:40:52 +0300 Subject: [PATCH 072/100] wip --- .../src/components/Homeworks/CourseHomeworkExperimental.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 518d179b1..cddde7ee3 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -290,7 +290,9 @@ const CourseHomeworkEditor: FC<{ style={{paddingTop: 85}} value={page} exclusive - onChange={(_, x) => setPage(x)} + onChange={(_, x) => { + if (x === "homework" || x === "group") setPage(x) + }} > From 224c1e67c24f6a34970824a074b8aa57aa820380 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 13 Apr 2026 03:44:23 +0300 Subject: [PATCH 073/100] wip --- .../src/components/Courses/Course.tsx | 19 +--- .../src/components/Courses/CourseGroups.tsx | 91 ------------------- 2 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 hwproj.front/src/components/Courses/CourseGroups.tsx diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index e2c565a86..6613ba9e1 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -34,12 +34,11 @@ import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; -import CourseGroups from "./CourseGroups"; -type TabValue = "homeworks" | "stats" | "applications" | "groups" +type TabValue = "homeworks" | "stats" | "applications" function isAcceptableTabValue(str: string): str is TabValue { - return str === "homeworks" || str === "stats" || str === "applications" || str === "groups"; + return str === "homeworks" || str === "stats" || str === "applications"; } interface ICourseState { @@ -349,13 +348,6 @@ const Course: React.FC = () => { }/>} - {isCourseMentor && -
    Группы
    - - }/> - } {tabValue === "homeworks" && { courseId={courseId!} /> } - {tabValue === "groups" && isCourseMentor && - - }
  • ); diff --git a/hwproj.front/src/components/Courses/CourseGroups.tsx b/hwproj.front/src/components/Courses/CourseGroups.tsx deleted file mode 100644 index d6e55e0a7..000000000 --- a/hwproj.front/src/components/Courses/CourseGroups.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import {FC, useEffect} from "react"; -import { - Accordion, - AccordionSummary, - AccordionDetails, - Grid, - Typography, - Alert, - Stack, -} from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import {AccountDataDto, GroupViewModel} from "@/api"; - -interface ICourseGroupsProps { - courseStudents: AccountDataDto[]; - groups: GroupViewModel[]; - onGroupsUpdate: () => void; -} - -const CourseGroups: FC = (props) => { - const {courseStudents, groups, onGroupsUpdate} = props; - - useEffect(() => { - onGroupsUpdate(); - }, []); - - const getStudentName = (userId: string) => { - const student = courseStudents.find(s => s.userId === userId); - if (!student) { - return userId; - } - const nameParts = [student.surname, student.name, student.middleName].filter(Boolean); - return `${nameParts.join(" ") || student.email}`; - }; - - return ( - - - - - Группы курса - - - - - {groups.length === 0 && - - - На курсе пока нет групп. - - - } - - - {groups.map(group => { - const name = group.name!; - const studentsIds = group.studentsIds || []; - - return ( - - - }> - - {name} - - - - {studentsIds.length > 0 ? ( - - {studentsIds.map(id => ( - - {getStudentName(id)} - - ))} - - ) : ( - - В группе пока нет участников. - - )} - - - - ); - })} - - - ); -}; - -export default CourseGroups; From cb451a16c1b4835df866166cd5324ae3b5e41ab9 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 13 Apr 2026 03:46:51 +0300 Subject: [PATCH 074/100] wip --- hwproj.front/src/components/Courses/CourseExperimental.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Courses/CourseExperimental.tsx b/hwproj.front/src/components/Courses/CourseExperimental.tsx index c8af487ed..d16fbb3a1 100644 --- a/hwproj.front/src/components/Courses/CourseExperimental.tsx +++ b/hwproj.front/src/components/Courses/CourseExperimental.tsx @@ -576,7 +576,7 @@ export const CourseExperimental: FC = (props) => { }}> {x.groupId && } + color={x.isDeferred ? "disabled" : x.tags!.includes(TestTag) ? "primary" : "inherit"}/>} Date: Mon, 13 Apr 2026 13:21:31 +0300 Subject: [PATCH 075/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 722e89538..9614648ef 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -44,7 +44,7 @@ const GroupSelector: FC = (props) => { name: selectedGroup?.name || "", memberIds: selectedGroup?.studentsIds || [] }) - }, [props.selectedGroupId]) + }, [props.selectedGroupId, props.groups]) const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); @@ -68,10 +68,6 @@ const GroupSelector: FC = (props) => { ); props.onGroupsUpdate(); } else { - if (!formState.name.trim() || formState.memberIds.length === 0) { - return; - } - const groupId = await ApiSingleton.courseGroupsApi.courseGroupsCreateCourseGroup(props.courseId, { name: formState.name.trim(), groupMatesIds: formState.memberIds, From b398355638a33628e0ab985950061a610cb0f571 Mon Sep 17 00:00:00 2001 From: Semyon Date: Mon, 13 Apr 2026 13:37:26 +0300 Subject: [PATCH 076/100] fix: rename userId -> Id in filter --- .../HwProj.Models/CoursesService/CourseFilterModels.cs | 8 ++++---- .../CoursesService/DTO/CreateCourseFilterDTO.cs | 2 +- .../HwProj.CoursesService.API/Models/CourseContext.cs | 2 +- .../Models/UserToCourseFilter.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs index 6ca399364..b1612eabe 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/CourseFilterModels.cs @@ -4,14 +4,14 @@ namespace HwProj.Models.CoursesService { public class CreateCourseFilterModel { - public string UserId { get; set; } + public string Id { get; set; } public long CourseId { get; set; } - + public List StudentIds { get; set; } = new List(); - + public List HomeworkIds { get; set; } = new List(); - + public List MentorIds { get; set; } = new List(); } } \ No newline at end of file diff --git a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs index a8c8a0fa2..a0838be69 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/DTO/CreateCourseFilterDTO.cs @@ -4,7 +4,7 @@ namespace HwProj.Models.CoursesService.DTO { public class CreateCourseFilterDTO { - public string UserId { get; set; } + public string Id { get; set; } public List StudentIds { get; set; } = new List(); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs index 261f4e09f..c32220254 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/CourseContext.cs @@ -28,7 +28,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasAlternateKey(u => new { u.GroupId, u.StudentId }); modelBuilder.Entity().HasIndex(a => a.CourseId); - modelBuilder.Entity().HasKey(u => new { u.CourseId, u.UserId }); + modelBuilder.Entity().HasKey(u => new { u.CourseId, u.Id }); modelBuilder.Entity().HasIndex(t => t.TaskId); } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs index 4f7ed5fb3..1b4431a9a 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/UserToCourseFilter.cs @@ -5,7 +5,7 @@ namespace HwProj.CoursesService.API.Models public class UserToCourseFilter { public long CourseId { get; set; } - public string UserId { get; set; } + public string Id { get; set; } public CourseFilter CourseFilter { get; set; } From dfd264af9ee768c494335c2ce55fbbd0340dd4ea Mon Sep 17 00:00:00 2001 From: Semyon Date: Mon, 13 Apr 2026 13:46:37 +0300 Subject: [PATCH 077/100] fix: filter if usage --- .../Repositories/CourseFilterRepository.cs | 8 ++++---- .../Services/CourseFilterService.cs | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs index 0ed63aa38..7f47e4f77 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/CourseFilterRepository.cs @@ -18,7 +18,7 @@ public CourseFilterRepository(CourseContext context) : base(context) var userToCourseFilter = await Context.Set() .Include(ucf => ucf.CourseFilter) .AsNoTracking() - .FirstOrDefaultAsync(u => u.UserId == userId && u.CourseId == courseId); + .FirstOrDefaultAsync(u => u.Id == userId && u.CourseId == courseId); return userToCourseFilter?.CourseFilter; } @@ -27,7 +27,7 @@ public async Task> GetAsync(string[] userIds, long cour { return await Context.Set() .AsNoTracking() - .Where(u => userIds.Contains(u.UserId) && u.CourseId == courseId) + .Where(u => userIds.Contains(u.Id) && u.CourseId == courseId) .Include(ucf => ucf.CourseFilter) .ToListAsync(); } @@ -36,7 +36,7 @@ public async Task> GetAsync(string userId, long[] cours { return await Context.Set() .AsNoTracking() - .Where(u => u.UserId == userId && courseIds.Contains(u.CourseId)) + .Where(u => u.Id == userId && courseIds.Contains(u.CourseId)) .Include(ucf => ucf.CourseFilter) .ToListAsync(); } @@ -53,7 +53,7 @@ public async Task AddAsync(CourseFilter courseFilter, string userId, long { CourseFilterId = filterId, CourseId = courseId, - UserId = userId + Id = userId }; Context.Set().Add(userToCourseFilter); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 16df85273..387c147d9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -33,14 +33,14 @@ public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterMod var filter = CourseFilterUtils.CreateFilter(courseFilterModel); var existingCourseFilter = - await _courseFilterRepository.GetAsync(courseFilterModel.UserId, courseFilterModel.CourseId); + await _courseFilterRepository.GetAsync(courseFilterModel.Id, courseFilterModel.CourseId); if (existingCourseFilter != null) { await UpdateAsync(existingCourseFilter.Id, filter); return Result.Success(existingCourseFilter.Id); } - var filterId = await AddCourseFilter(filter, courseFilterModel.CourseId, courseFilterModel.UserId); + var filterId = await AddCourseFilter(filter, courseFilterModel.CourseId, courseFilterModel.Id); if (filterId == -1) { return Result.Failed(); @@ -86,7 +86,7 @@ public async Task ApplyFilter(CourseDTO course, string userId) var courseFilters = (await _courseFilterRepository.GetAsync(findFiltersFor, course.Id)) - .ToDictionary(x => x.UserId, x => x.CourseFilter); + .ToDictionary(x => x.Id, x => x.CourseFilter); if (!isMentor) { @@ -126,7 +126,7 @@ public async Task GetAssignedStudentsIds(long cou .Where(u => u.CourseFilter.Filter.HomeworkIds.Count == 0) .Select(u => new MentorToAssignedStudentsDTO { - MentorId = u.UserId, + MentorId = u.Id, SelectedStudentsIds = u.CourseFilter.Filter.StudentIds }) .ToArray(); @@ -208,7 +208,7 @@ public async Task UpdateGroupFilters(long courseId, long homeworkId, IEnumerable { var filterIds = studentIds.Union(new[] { GlobalFilterUserId }).ToArray(); var filters = (await _courseFilterRepository.GetAsync(filterIds, courseId)) - .ToDictionary(x => x.UserId, x => x.CourseFilter); + .ToDictionary(x => x.Id, x => x.CourseFilter); foreach (var filterId in filterIds) { From ef94ce4472fdc9461f21da1f775c1ae517439adb Mon Sep 17 00:00:00 2001 From: Semyon Date: Mon, 13 Apr 2026 13:50:31 +0300 Subject: [PATCH 078/100] little fix --- .../HwProj.APIGateway.API/Controllers/CoursesController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs index 2a256b888..c3c94084a 100644 --- a/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs +++ b/HwProj.APIGateway/HwProj.APIGateway.API/Controllers/CoursesController.cs @@ -243,7 +243,7 @@ public async Task EditMentorWorkspace( return BadRequest("Пользователь с такой почтой не является преподавателем или экспертом"); var courseFilterModel = _mapper.Map(editMentorWorkspaceDto); - courseFilterModel.UserId = mentorId; + courseFilterModel.Id = mentorId; var courseFilterCreationResult = await _coursesClient.CreateOrUpdateCourseFilter(courseId, courseFilterModel); From 90d370a3b1307029781992342dee54eb48a7ee4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 20:19:50 +0300 Subject: [PATCH 079/100] add migration --- .../20260413171549_FilterId.Designer.cs | 485 ++++++++++++++++++ .../Migrations/20260413171549_FilterId.cs | 28 + .../Migrations/CourseContextModelSnapshot.cs | 4 +- 3 files changed, 515 insertions(+), 2 deletions(-) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs new file mode 100644 index 000000000..35446d065 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.Designer.cs @@ -0,0 +1,485 @@ +// +using System; +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260413171549_FilterId")] + partial class FilterId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("MentorId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Assignments"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupName") + .HasColumnType("nvarchar(max)"); + + b.Property("InviteCode") + .HasColumnType("nvarchar(max)"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("IsOpen") + .HasColumnType("bit"); + + b.Property("MentorIds") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Courses"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseFilter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("FilterJson") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("CourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("IsAccepted") + .HasColumnType("bit"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("CourseMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("MaxPoints") + .HasColumnType("int"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Criteria"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("StudentId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasAlternateKey("GroupId", "StudentId"); + + b.ToTable("GroupMates"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CourseId"); + + b.ToTable("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("DeadlineDate") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("HasDeadline") + .HasColumnType("bit"); + + b.Property("HomeworkId") + .HasColumnType("bigint"); + + b.Property("IsBonusExplicit") + .HasColumnType("bit"); + + b.Property("IsDeadlineStrict") + .HasColumnType("bit"); + + b.Property("MaxRating") + .HasColumnType("int"); + + b.Property("PublicationDate") + .HasColumnType("datetime2"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("HomeworkId"); + + b.ToTable("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.Property("CourseMateId") + .HasColumnType("bigint"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CourseMateId"); + + b.ToTable("StudentCharacteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("GroupId") + .HasColumnType("bigint"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("TasksModels"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Answer") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsPrivate") + .HasColumnType("bit"); + + b.Property("LecturerId") + .HasColumnType("nvarchar(max)"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.Property("TaskId") + .HasColumnType("bigint"); + + b.Property("Text") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.HasKey("Id"); + + b.HasIndex("TaskId"); + + b.ToTable("Questions"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.Property("CourseId") + .HasColumnType("bigint"); + + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("CourseFilterId") + .HasColumnType("bigint"); + + b.HasKey("CourseId", "Id"); + + b.HasIndex("CourseFilterId"); + + b.ToTable("UserToCourseFilters"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Assignment", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Assignments") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("CourseMates") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Criterion", b => + { + b.HasOne("HwProj.CoursesService.API.Models.HomeworkTask", "Task") + .WithMany("Criteria") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Task"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.GroupMate", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("GroupMates") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Course", null) + .WithMany("Homeworks") + .HasForeignKey("CourseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Homework", "Homework") + .WithMany("Tasks") + .HasForeignKey("HomeworkId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Homework"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.StudentCharacteristics", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseMate", null) + .WithOne("Characteristics") + .HasForeignKey("HwProj.CoursesService.API.Models.StudentCharacteristics", "CourseMateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.TaskModel", b => + { + b.HasOne("HwProj.CoursesService.API.Models.Group", null) + .WithMany("Tasks") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.UserToCourseFilter", b => + { + b.HasOne("HwProj.CoursesService.API.Models.CourseFilter", "CourseFilter") + .WithMany() + .HasForeignKey("CourseFilterId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CourseFilter"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Course", b => + { + b.Navigation("Assignments"); + + b.Navigation("CourseMates"); + + b.Navigation("Homeworks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.CourseMate", b => + { + b.Navigation("Characteristics"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Group", b => + { + b.Navigation("GroupMates"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.Homework", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("HwProj.CoursesService.API.Models.HomeworkTask", b => + { + b.Navigation("Criteria"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs new file mode 100644 index 000000000..ae885fc27 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260413171549_FilterId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + /// + public partial class FilterId : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "UserId", + table: "UserToCourseFilters", + newName: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "Id", + table: "UserToCourseFilters", + newName: "UserId"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 3bb2a5bb9..4b3bb83b5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -346,13 +346,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CourseId") .HasColumnType("bigint"); - b.Property("UserId") + b.Property("Id") .HasColumnType("nvarchar(450)"); b.Property("CourseFilterId") .HasColumnType("bigint"); - b.HasKey("CourseId", "UserId"); + b.HasKey("CourseId", "Id"); b.HasIndex("CourseFilterId"); From 21930a0d50b74530afd0b73d37c0d224fac9c19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 20:22:40 +0300 Subject: [PATCH 080/100] feat: change to work on group filters --- .../AutomapperProfile.cs | 1 + .../Services/CourseFilterService.cs | 78 +++++++++---------- .../Services/GroupsService.cs | 15 ++++ .../Services/HomeworksService.cs | 6 +- .../Services/ICourseFilterService.cs | 2 +- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs index 58d5effea..43646b989 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/AutomapperProfile.cs @@ -23,6 +23,7 @@ public AutomapperProfile() CreateMap(); CreateMap().ReverseMap(); + CreateMap(); CreateMap().ReverseMap(); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 387c147d9..a8dc451fe 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -7,6 +7,7 @@ using HwProj.Models.CoursesService.ViewModels; using HwProj.Models.Result; using System.Collections.Generic; +using System; namespace HwProj.CoursesService.API.Services { @@ -19,13 +20,15 @@ public enum ApplyFilterOperation public class CourseFilterService : ICourseFilterService { - private const string GlobalFilterUserId = ""; + 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) @@ -80,9 +83,13 @@ public async Task ApplyFilter(CourseDTO course, string userId) var isMentor = course.MentorIds.Contains(userId); var isCourseStudent = course.AcceptedStudents.Any(t => t.StudentId == userId); + // Получаем группы пользователя, чтобы найти фильтры для них + var studentGroups = await _groupsService.GetStudentGroupsAsync(course.Id, userId); + var groupIds = studentGroups.Select(g => g.Id.ToString()).ToArray(); + var findFiltersFor = isMentor || !isCourseStudent - ? new[] { userId, GlobalFilterUserId } - : course.MentorIds.Concat(new[] { userId, GlobalFilterUserId }).ToArray(); + ? new[] { userId, GlobalFilterId } + : course.MentorIds.Concat(new[] { userId, GlobalFilterId }).Concat(groupIds).ToArray(); var courseFilters = (await _courseFilterRepository.GetAsync(findFiltersFor, course.Id)) @@ -91,13 +98,16 @@ public async Task ApplyFilter(CourseDTO course, string userId) if (!isMentor) { var studentCourse = course; - if (courseFilters.TryGetValue(GlobalFilterUserId, out var groupFilter)) + if (courseFilters.TryGetValue(GlobalFilterId, out var groupFilter)) studentCourse = ApplyFilterInternal(course, studentCourse, groupFilter, ApplyFilterOperation.Subtract); - studentCourse = courseFilters.TryGetValue(userId, out var studentFilter) - ? ApplyFilterInternal(course, studentCourse, studentFilter, ApplyFilterOperation.Union) - : studentCourse; + // Применяем фильтры всех групп студента + foreach (var group in studentGroups) + { + if (courseFilters.TryGetValue(group.Id.ToString(), out var groupCourseFilter)) + studentCourse = ApplyFilterInternal(course, studentCourse, groupCourseFilter, ApplyFilterOperation.Union); + } var mentorIds = course.MentorIds .Where(u => @@ -204,50 +214,36 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit }; } - public async Task UpdateGroupFilters(long courseId, long homeworkId, IEnumerable studentIds) + public async Task UpdateGroupFilters(long courseId, long homeworkId, Group group) { - var filterIds = studentIds.Union(new[] { GlobalFilterUserId }).ToArray(); - var filters = (await _courseFilterRepository.GetAsync(filterIds, courseId)) + var groupMates = (group?.GroupMates.ToArray() ?? Array.Empty()).Select(gm => gm.StudentId).ToList(); + + var existingFilters = (await _courseFilterRepository.GetAsync(new[] { GlobalFilterId, group.Id.ToString() }, courseId)) .ToDictionary(x => x.Id, x => x.CourseFilter); - foreach (var filterId in filterIds) - { - await AddOrUpdateHomeworkToFilter(filters.GetValueOrDefault(filterId), filterId, courseId, homeworkId); - } + await UpdateOrCreateFilter(GlobalFilterId, courseId, homeworkId, new List(), existingFilters); + await UpdateOrCreateFilter(group.Id.ToString(), courseId, homeworkId, groupMates, existingFilters); } - private async Task AddOrUpdateHomeworkToFilter(CourseFilter filter, string userId, long courseId, - long homeworkId) + private async Task UpdateOrCreateFilter(string id, long courseId, long homeworkId, List studentIds, + Dictionary existingFilters) { - if (filter != null) + if (existingFilters.TryGetValue(id, out var courseFilter) && courseFilter.Filter is { } filter) { - await UpdateFilterWithHomework(filter, homeworkId); + filter.StudentIds = studentIds; + filter.HomeworkIds.Add(homeworkId); + await UpdateAsync(courseFilter.Id, courseFilter.Filter); } else { - await CreateFilterWithHomework(userId, courseId, homeworkId); - } - } - - private async Task UpdateFilterWithHomework(CourseFilter courseFilter, long homeworkId) - { - if (!courseFilter.Filter.HomeworkIds.Contains(homeworkId)) - { - courseFilter.Filter.HomeworkIds.Add(homeworkId); - await UpdateAsync(courseFilter.Id, courseFilter.Filter); + var newFilter = new Filter + { + StudentIds = studentIds, + HomeworkIds = new List { homeworkId }, + MentorIds = new List() + }; + await AddCourseFilter(newFilter, courseId, id); } } - - private async Task CreateFilterWithHomework(string userId, long courseId, long homeworkId) - { - var newFilter = new Filter - { - StudentIds = new List(), - HomeworkIds = new List { homeworkId }, - MentorIds = new List() - }; - - await AddCourseFilter(newFilter, courseId, userId); - } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index f5688a8ad..2794c76a9 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AutoMapper; using HwProj.CoursesService.API.Models; +using HwProj.CoursesService.API.Repositories; using HwProj.CoursesService.API.Repositories.Groups; using HwProj.Models.CoursesService.DTO; using Microsoft.EntityFrameworkCore; @@ -13,14 +14,17 @@ public class GroupsService : IGroupsService { private readonly IGroupsRepository _groupsRepository; private readonly IGroupMatesRepository _groupMatesRepository; + private readonly ICourseFilterRepository _courseFilterRepository; private readonly IMapper _mapper; public GroupsService(IGroupsRepository groupsRepository, IGroupMatesRepository groupMatesRepository, + ICourseFilterRepository courseFilterRepository, IMapper mapper) { _groupsRepository = groupsRepository; _groupMatesRepository = groupMatesRepository; + _courseFilterRepository = courseFilterRepository; _mapper = mapper; } @@ -89,6 +93,17 @@ await _groupMatesRepository { Name = updated.Name }); + + // Обновляем участников в фильтре группы + var groupFilter = await _courseFilterRepository.GetAsync(groupId.ToString(), group.CourseId); + if (groupFilter != null) + { + groupFilter.Filter.StudentIds = updatedStudentIds.ToList(); + await _courseFilterRepository.UpdateAsync(groupFilter.Id, f => new CourseFilter + { + FilterJson = groupFilter.FilterJson + }); + } } public async Task GetStudentGroupsAsync(long courseId, string studentId) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index 92bd924a0..aa36c18f6 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -47,7 +47,7 @@ public async Task AddHomeworkAsync(long courseId, CreateHomeworkViewMo var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); - await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, groupMates.Select(gm => gm.StudentId)); + await _courseFilterService.UpdateGroupFilters(courseId, homework.Id, group); notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } @@ -93,10 +93,10 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV if (update.GroupId is { } groupId) { - var group = (await _groupsService.GetGroupsAsync(groupId)).FirstOrDefault(); + var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); - await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, groupMates.Select(gm => gm.StudentId)); + await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, group); notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs index 9a599e4f5..0d49f1805 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/ICourseFilterService.cs @@ -15,6 +15,6 @@ public interface ICourseFilterService Task ApplyFiltersToCourses(string userId, CourseDTO[] courses); Task ApplyFilter(CourseDTO courseDto, string userId); Task GetAssignedStudentsIds(long courseId, string[] mentorsIds); - Task UpdateGroupFilters(long courseId, long homeworkId, IEnumerable studentIds); + Task UpdateGroupFilters(long courseId, long homeworkId, Group group); } } \ No newline at end of file From 46970a1a864e33ed0bb4d42991a57cb6dde5bb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 20:25:31 +0300 Subject: [PATCH 081/100] feat: unlock remove user from group --- .../src/components/Common/GroupSelector.tsx | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 9614648ef..5f681e52e 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -49,10 +49,10 @@ const GroupSelector: FC = (props) => { const [isSubmitting, setIsSubmitting] = useState(false); const [isError, setIsError] = useState(false); - const studentsWithoutGroup = useMemo(() => { - const studentsInGroups = groups.flatMap(g => g.studentsIds) - return props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)) - }, [groups, props.courseStudents]); + const studentsInGroups = useMemo(() => groups.flatMap(g => g.studentsIds), + [groups, props.groups]); + const studentsWithoutGroup = useMemo(() => props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)), + [groups, props.courseStudents]); const handleSubmitEdit = async () => { setIsSubmitting(true); @@ -92,6 +92,7 @@ const GroupSelector: FC = (props) => { disableClearable={props.selectedGroupId == undefined} fullWidth options={props.selectedGroupId == undefined ? groups : []} + disabled={props.choiceDisabled} renderOption={(props, option) => { if (option.id === -1) return
  • + Добавить новую @@ -126,28 +127,20 @@ const GroupSelector: FC = (props) => { formState.memberIds.includes(s.userId!)) || []} getOptionLabel={(option) => `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() } filterSelectedOptions - onChange={(_, values) => { - if (selectedGroup) { - // При редактировании выбранной группы можно только добавлять студентов - setFormState(prev => ({ - ...prev, - memberIds: [...formState.memberIds, - ...values.map(x => !formState.memberIds.includes(x.userId!) ? x.userId! : "").filter(Boolean)] - })) - } else { - setFormState(prev => ({ - ...prev, - memberIds: values - .map(x => x.userId!) - .filter(Boolean) - })) - } + onChange={(_, value) => { + if (value.map(x => x.userId!).filter(Boolean).length === 0) return; + setFormState(prev => ({ + ...prev, + memberIds: value + .map(x => x.userId!) + .filter(Boolean) + })) }} disabled={isSubmitting} renderTags={(tagValue, getTagProps) => @@ -155,7 +148,6 @@ const GroupSelector: FC = (props) => { )) From 19e56de44fbe4343d6fc0889f62a4eb927c56464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 21:42:16 +0300 Subject: [PATCH 082/100] feat: print students without group on hover --- hwproj.front/src/components/Courses/Course.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 6613ba9e1..4c014239d 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -21,6 +21,7 @@ import { Menu, MenuItem, Stack, + Tooltip, Typography } from "@mui/material"; import {CourseExperimental} from "./CourseExperimental"; @@ -316,9 +317,20 @@ const Course: React.FC = () => { {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && - - Студентов, не записанных в группу: {studentsWithoutGroup.length} - + + {studentsWithoutGroup.map(s => ( +
    {`${s.surname ?? ""} ${s.name ?? ""} / ${s.email ?? ""}`.trim()}
    + ))} + + } + arrow + > + + Студентов, не записанных в группу: {studentsWithoutGroup.length} + +
    } Date: Mon, 13 Apr 2026 22:14:02 +0300 Subject: [PATCH 083/100] fix: right test sum counting --- .../src/components/Courses/StudentStats.tsx | 56 ++++++++++++------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index c2e8e16d1..91c20a1f9 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -106,7 +106,8 @@ const StudentStats: React.FC = (props) => { const notTests = homeworks.filter(h => !h.tags!.includes(TestTag)) - const testGroups = Lodash(homeworks.filter(h => h.tags!.includes(TestTag))) + const testHomeworks = homeworks.filter(h => h.tags!.includes(TestTag)) + const testGroups = Lodash(testHomeworks) .groupBy((h: HomeworkViewModel) => { const key = h.tags!.find(t => !DefaultTags.includes(t)) return key || h.id!.toString(); @@ -114,17 +115,26 @@ const StudentStats: React.FC = (props) => { .values() .value(); - const testsMaxSum = testGroups - .map(h => h[0]) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => - sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)), 0) + const testsWithGroupsMaxSum = testHomeworks + .filter(h => h.groupId !== undefined) + .flatMap(h => h.tasks) + .reduce((sum, task) => sum + (task!.maxRating || 0), 0) + + const testsWithoutGroupsMaxSum = testHomeworks.filter(h => h.groupId === undefined) + .flatMap(h => h.tasks) + .reduce((sum, task) => sum + (task!.maxRating || 0), 0) - const hasHomeworks = !!notTests + const homeworksWithGroups = notTests.filter(h => h.groupId) + const homeworksWithoutGroupMaxSum = notTests.filter(h => !h.groupId) .filter(h => !h.tags!.includes(BonusTag)) .flatMap(homework => homework.tasks) - .filter(task => !task!.tags!.includes(BonusTag)) - const hasTests = testsMaxSum > 0 + .reduce((sum, task) => { + return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); + }, 0) + console.log(homeworksWithoutGroupMaxSum) + + const hasHomeworks = homeworksWithoutGroupMaxSum > 0 || homeworksWithGroups.length > 0 + const hasTests = testsWithGroupsMaxSum + testsWithoutGroupsMaxSum > 0 const showBestSolutions = isMentor && (hasHomeworks || hasTests) const bestTaskSolutions = new Map() @@ -215,7 +225,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - ДЗ + ДЗ {homeworksWithoutGroupMaxSum > 0 && `(${homeworksWithoutGroupMaxSum})`} } {hasTests && = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - КР ({testsMaxSum}) + КР {testsWithoutGroupsMaxSum > 0 && `(${testsWithoutGroupsMaxSum})`} } {showBestSolutions && @@ -257,13 +267,13 @@ const StudentStats: React.FC = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) - const homeworksMaxSum = notTests + const userHomeworksMaxSum = notTests .filter(h => !h.tags!.includes(BonusTag) && - (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!) || !h.groupId)) + (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) .flatMap(homework => homework.tasks) .reduce((sum, task) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) + }, 0) + homeworksWithoutGroupMaxSum const testsSum = testGroups .map(group => { @@ -282,6 +292,12 @@ const StudentStats: React.FC = (props) => { .flat() .reduce((sum, rating) => sum + rating, 0) + const userTestsMaxSum = testHomeworks + .filter(h => h.groupId !== undefined && + (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) + .flatMap(homework => homework.tasks) + .reduce((sum, task) => sum + (task!.maxRating || 0), 0) + testsWithoutGroupsMaxSum + const bestSolutionsCount = bestTaskSolutions.values() .filter(x => x === cm.id) .toArray().length @@ -338,12 +354,12 @@ const StudentStats: React.FC = (props) => { scope="row" variant={"body"} > - 0 && + label={`${homeworksSum} ${homeworksWithGroups.length > 0 ? `/ ${userHomeworksMaxSum}` : ""}`}/>} } {hasTests && = (props) => { scope="row" variant={"body"} > - 0 && + label={`${testsSum} ${testHomeworks.some(h => h.groupId !== undefined) ? `/ ${userTestsMaxSum}` : ""}`}/>} } {showBestSolutions && Date: Mon, 13 Apr 2026 22:26:06 +0300 Subject: [PATCH 084/100] refactor: use linq.aggregate --- .../Services/CourseFilterService.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index a8dc451fe..f2b085949 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -97,17 +97,16 @@ public async Task ApplyFilter(CourseDTO course, string userId) if (!isMentor) { - var studentCourse = course; - if (courseFilters.TryGetValue(GlobalFilterId, out var groupFilter)) - studentCourse = ApplyFilterInternal(course, studentCourse, groupFilter, - ApplyFilterOperation.Subtract); - - // Применяем фильтры всех групп студента - foreach (var group in studentGroups) - { - if (courseFilters.TryGetValue(group.Id.ToString(), out var groupCourseFilter)) - studentCourse = ApplyFilterInternal(course, studentCourse, groupCourseFilter, ApplyFilterOperation.Union); - } + var globalFilter = courseFilters.GetValueOrDefault(GlobalFilterId); + var globalCourse = globalFilter != null + ? ApplyFilterInternal(course, course, globalFilter, ApplyFilterOperation.Subtract) + : course; + + var studentCourse = studentGroups + .Select(g => courseFilters.GetValueOrDefault(g.Id.ToString())) + .Where(cf => cf != null) + .Aggregate(globalCourse, (current, groupCourseFilter) => + ApplyFilterInternal(course, current, groupCourseFilter, ApplyFilterOperation.Union)); var mentorIds = course.MentorIds .Where(u => From a4ea57f74dbae2201b2119d235b91a4b2c48a093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 22:30:52 +0300 Subject: [PATCH 085/100] refactor: get student groups from course --- .../Services/CourseFilterService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index f2b085949..4d6efedde 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -22,13 +22,11 @@ public class CourseFilterService : ICourseFilterService { private const string GlobalFilterId = ""; private readonly ICourseFilterRepository _courseFilterRepository; - private readonly IGroupsService _groupsService; public CourseFilterService( - ICourseFilterRepository courseFilterRepository, IGroupsService groupsService) + ICourseFilterRepository courseFilterRepository) { _courseFilterRepository = courseFilterRepository; - _groupsService = groupsService; } public async Task> CreateOrUpdateCourseFilter(CreateCourseFilterModel courseFilterModel) @@ -83,8 +81,10 @@ public async Task ApplyFilter(CourseDTO course, string userId) var isMentor = course.MentorIds.Contains(userId); var isCourseStudent = course.AcceptedStudents.Any(t => t.StudentId == userId); - // Получаем группы пользователя, чтобы найти фильтры для них - var studentGroups = await _groupsService.GetStudentGroupsAsync(course.Id, userId); + // Получаем группы пользователя из course + var studentGroups = course.Groups + .Where(g => g.StudentsIds.Contains(userId)) + .ToArray(); var groupIds = studentGroups.Select(g => g.Id.ToString()).ToArray(); var findFiltersFor = isMentor || !isCourseStudent From 5c90055069841f5238a021e8f76c45fff093a25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 23:10:51 +0300 Subject: [PATCH 086/100] refactor: simplify group filter updating --- .../Services/CourseFilterService.cs | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs index 4d6efedde..e5e51dfc8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/CourseFilterService.cs @@ -215,33 +215,23 @@ private CourseDTO ApplyFilterInternal(CourseDTO initialCourseDto, CourseDTO edit public async Task UpdateGroupFilters(long courseId, long homeworkId, Group group) { - var groupMates = (group?.GroupMates.ToArray() ?? Array.Empty()).Select(gm => gm.StudentId).ToList(); + var filterIds = group != null + ? new[] { GlobalFilterId, group.Id.ToString() } + : new[] { GlobalFilterId }; - var existingFilters = (await _courseFilterRepository.GetAsync(new[] { GlobalFilterId, group.Id.ToString() }, courseId)) - .ToDictionary(x => x.Id, x => x.CourseFilter); - - await UpdateOrCreateFilter(GlobalFilterId, courseId, homeworkId, new List(), existingFilters); - await UpdateOrCreateFilter(group.Id.ToString(), courseId, homeworkId, groupMates, existingFilters); - } + var filters = await _courseFilterRepository.GetAsync(filterIds, courseId); - private async Task UpdateOrCreateFilter(string id, long courseId, long homeworkId, List studentIds, - Dictionary existingFilters) - { - if (existingFilters.TryGetValue(id, out var courseFilter) && courseFilter.Filter is { } filter) + foreach (var filterId in filterIds) { - filter.StudentIds = studentIds; - filter.HomeworkIds.Add(homeworkId); - await UpdateAsync(courseFilter.Id, courseFilter.Filter); - } - else - { - var newFilter = new Filter - { - StudentIds = studentIds, - HomeworkIds = new List { homeworkId }, - MentorIds = new List() - }; - await AddCourseFilter(newFilter, courseId, id); + var existingCourseFilter = filters.SingleOrDefault(f => f.Id == filterId)?.CourseFilter; + var newFilter = existingCourseFilter?.Filter + ?? new Filter { StudentIds = new List(), HomeworkIds = new List(), MentorIds = new List() }; + newFilter.HomeworkIds.Add(homeworkId); + + if (existingCourseFilter != null) + await UpdateAsync(existingCourseFilter.Id, newFilter); + else + await AddCourseFilter(newFilter, courseId, filterId); } } } From 1d0b69643175c33501269539fad206f2efa7e31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Mon, 13 Apr 2026 23:11:25 +0300 Subject: [PATCH 087/100] refactor: remove unnecessary updation call --- .../HwProj.CoursesService.API/Services/HomeworksService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs index aa36c18f6..a6816f1bd 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/HomeworksService.cs @@ -96,7 +96,6 @@ public async Task UpdateHomeworkAsync(long homeworkId, CreateHomeworkV var group = (await _groupsService.GetGroupsAsync(groupId)).SingleOrDefault(); var groupMates = group?.GroupMates.ToArray() ?? Array.Empty(); - await _courseFilterService.UpdateGroupFilters(course.Id, homework.Id, group); notifyStudentIds = groupMates.Select(gm => gm.StudentId).ToArray(); } From 38f25c10ea5ee9cc04b0cd7073fb67b6c1456257 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Tue, 14 Apr 2026 01:00:48 +0300 Subject: [PATCH 088/100] Wip --- hwproj.front/src/services/StudentStatsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/services/StudentStatsUtils.ts b/hwproj.front/src/services/StudentStatsUtils.ts index eefdebebd..133ea2c50 100644 --- a/hwproj.front/src/services/StudentStatsUtils.ts +++ b/hwproj.front/src/services/StudentStatsUtils.ts @@ -42,7 +42,7 @@ export default class StudentStatsUtils { let color: string if(disabled) - color = "#d1d1d1" + color = "#eaeaea" else if (lastRatedSolution == undefined) color = "#ffffff" else From b354891171ad8f5df013f4ad9f2ab783360e67fc Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Tue, 14 Apr 2026 01:23:14 +0300 Subject: [PATCH 089/100] wip --- .../Services/GroupsService.cs | 16 ----------- .../src/components/Courses/Course.tsx | 27 +++++++++---------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 2794c76a9..58641d9f0 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using AutoMapper; using HwProj.CoursesService.API.Models; -using HwProj.CoursesService.API.Repositories; using HwProj.CoursesService.API.Repositories.Groups; using HwProj.Models.CoursesService.DTO; using Microsoft.EntityFrameworkCore; @@ -14,17 +13,14 @@ public class GroupsService : IGroupsService { private readonly IGroupsRepository _groupsRepository; private readonly IGroupMatesRepository _groupMatesRepository; - private readonly ICourseFilterRepository _courseFilterRepository; private readonly IMapper _mapper; public GroupsService(IGroupsRepository groupsRepository, IGroupMatesRepository groupMatesRepository, - ICourseFilterRepository courseFilterRepository, IMapper mapper) { _groupsRepository = groupsRepository; _groupMatesRepository = groupMatesRepository; - _courseFilterRepository = courseFilterRepository; _mapper = mapper; } @@ -79,7 +75,6 @@ public async Task UpdateAsync(long groupId, Group updated) var studentsToAdd = updatedStudentIds.Except(currentStudentIds).ToList(); var studentsToRemove = currentStudentIds.Except(updatedStudentIds).ToList(); - var groupMatesToAdd = updatedGroupMates.Where(x => studentsToAdd.Contains(x.StudentId)).ToArray(); foreach (var groupMate in groupMatesToAdd) groupMate.GroupId = groupId; @@ -93,17 +88,6 @@ await _groupMatesRepository { Name = updated.Name }); - - // Обновляем участников в фильтре группы - var groupFilter = await _courseFilterRepository.GetAsync(groupId.ToString(), group.CourseId); - if (groupFilter != null) - { - groupFilter.Filter.StudentIds = updatedStudentIds.ToList(); - await _courseFilterRepository.UpdateAsync(groupFilter.Id, f => new CourseFilter - { - FilterJson = groupFilter.FilterJson - }); - } } public async Task GetStudentGroupsAsync(long courseId, string studentId) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 4c014239d..136b8b17f 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -10,6 +10,7 @@ import EditIcon from "@material-ui/icons/Edit"; import { Alert, AlertTitle, + Badge, Box, Chip, Dialog, @@ -31,6 +32,7 @@ import AssessmentIcon from '@mui/icons-material/Assessment'; import NameBuilder from "../Utils/NameBuilder"; import {QRCodeSVG} from 'qrcode.react'; import QrCode2Icon from '@mui/icons-material/QrCode2'; +import GroupIcon from '@mui/icons-material/Group'; import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; @@ -317,20 +319,17 @@ const Course: React.FC = () => { {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && - - {studentsWithoutGroup.map(s => ( -
    {`${s.surname ?? ""} ${s.name ?? ""} / ${s.email ?? ""}`.trim()}
    - ))} - - } - arrow - > - - Студентов, не записанных в группу: {studentsWithoutGroup.length} - -
    + + + + + + + } Date: Tue, 14 Apr 2026 01:28:00 +0300 Subject: [PATCH 090/100] wip --- hwproj.front/src/components/Courses/Course.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Courses/Course.tsx b/hwproj.front/src/components/Courses/Course.tsx index 136b8b17f..c33122808 100644 --- a/hwproj.front/src/components/Courses/Course.tsx +++ b/hwproj.front/src/components/Courses/Course.tsx @@ -37,6 +37,7 @@ import {MoreVert} from "@mui/icons-material"; import {DotLottieReact} from "@lottiefiles/dotlottie-react"; import {FilesUploadWaiter} from "@/components/Files/FilesUploadWaiter"; import {CourseUnitType} from "@/components/Files/CourseUnitType"; +import Utils from "@/services/Utils"; type TabValue = "homeworks" | "stats" | "applications" @@ -321,7 +322,7 @@ const Course: React.FC = () => { {isCourseMentor && groups.length > 0 && studentsWithoutGroup.length > 0 && From 2b1fcd501d18f2712d9e88d954bc621e6445edbc Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Tue, 14 Apr 2026 01:36:42 +0300 Subject: [PATCH 091/100] wip --- hwproj.front/src/services/StudentStatsUtils.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/hwproj.front/src/services/StudentStatsUtils.ts b/hwproj.front/src/services/StudentStatsUtils.ts index 133ea2c50..f2149f807 100644 --- a/hwproj.front/src/services/StudentStatsUtils.ts +++ b/hwproj.front/src/services/StudentStatsUtils.ts @@ -1,6 +1,7 @@ import {SolutionDto, SolutionState} from "@/api"; import {colorBetween} from "./JsUtils"; import Utils from "./Utils"; +import {grey} from "@material-ui/core/colors"; export default class StudentStatsUtils { @@ -17,7 +18,7 @@ export default class StudentStatsUtils { : "#ffffff" } - static calculateLastRatedSolution(solutions: SolutionDto[]){ + static calculateLastRatedSolution(solutions: SolutionDto[]) { const ratedSolutions = solutions!.filter(x => x.state !== SolutionState.NUMBER_0) return ratedSolutions.slice(-1)[0] } @@ -30,7 +31,7 @@ export default class StudentStatsUtils { const lastRatedSolution = ratedSolutions.slice(-1)[0] let solutionsDescription: string - if(disabled) + if (disabled) solutionsDescription = "Задача недоступна для этого студента" else if (lastSolution === undefined) solutionsDescription = "Решение отсутствует" @@ -41,8 +42,8 @@ export default class StudentStatsUtils { else solutionsDescription = "Последняя оценка — " + `${lastRatedSolution.rating}/${taskMaxRating} ${Utils.pluralizeHelper(["балл", "балла", "баллов"], taskMaxRating)}\nНовое решение ожидает проверки` let color: string - if(disabled) - color = "#eaeaea" + if (disabled) + color = grey[300] else if (lastRatedSolution == undefined) color = "#ffffff" else From 4b1b3c251498110e343fc538e973870e17ac2bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 14 Apr 2026 19:36:06 +0300 Subject: [PATCH 092/100] fix: max sum counting --- .../src/components/Courses/StudentStats.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 91c20a1f9..814356cd3 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -120,7 +120,7 @@ const StudentStats: React.FC = (props) => { .flatMap(h => h.tasks) .reduce((sum, task) => sum + (task!.maxRating || 0), 0) - const testsWithoutGroupsMaxSum = testHomeworks.filter(h => h.groupId === undefined) + const testsWithoutGroupsMaxSum = testHomeworks.filter(h => h.groupId == undefined) .flatMap(h => h.tasks) .reduce((sum, task) => sum + (task!.maxRating || 0), 0) @@ -131,7 +131,6 @@ const StudentStats: React.FC = (props) => { .reduce((sum, task) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); }, 0) - console.log(homeworksWithoutGroupMaxSum) const hasHomeworks = homeworksWithoutGroupMaxSum > 0 || homeworksWithGroups.length > 0 const hasTests = testsWithGroupsMaxSum + testsWithoutGroupsMaxSum > 0 @@ -225,7 +224,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - ДЗ {homeworksWithoutGroupMaxSum > 0 && `(${homeworksWithoutGroupMaxSum})`} + ДЗ {homeworksWithGroups.length === 0 && `(${homeworksWithoutGroupMaxSum})`}
    } {hasTests && = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - КР {testsWithoutGroupsMaxSum > 0 && `(${testsWithoutGroupsMaxSum})`} + КР {homeworksWithGroups.length === 0 && `(${testsWithoutGroupsMaxSum})`} } {showBestSolutions && @@ -267,7 +266,7 @@ const StudentStats: React.FC = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) - const userHomeworksMaxSum = notTests + const studentHomeworksMaxSum = notTests .filter(h => !h.tags!.includes(BonusTag) && (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) .flatMap(homework => homework.tasks) @@ -292,7 +291,7 @@ const StudentStats: React.FC = (props) => { .flat() .reduce((sum, rating) => sum + rating, 0) - const userTestsMaxSum = testHomeworks + const studentTestsMaxSum = testHomeworks .filter(h => h.groupId !== undefined && (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) .flatMap(homework => homework.tasks) @@ -354,12 +353,12 @@ const StudentStats: React.FC = (props) => { scope="row" variant={"body"} > - {userHomeworksMaxSum > 0 && 0 && 0 ? `/ ${userHomeworksMaxSum}` : ""}`}/>} + label={`${homeworksSum} ${homeworksWithGroups.length > 0 ? `/ ${studentHomeworksMaxSum}` : ""}`}/>} } {hasTests && = (props) => { scope="row" variant={"body"} > - {userTestsMaxSum > 0 && 0 && h.groupId !== undefined) ? `/ ${userTestsMaxSum}` : ""}`}/>} + label={`${testsSum} ${testHomeworks.some(h => h.groupId !== undefined) ? `/ ${studentTestsMaxSum}` : ""}`}/>} } {showBestSolutions && Date: Tue, 14 Apr 2026 20:44:03 +0300 Subject: [PATCH 093/100] feat: add grouped students label --- .../src/components/Common/GroupSelector.tsx | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 5f681e52e..755346ec3 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -8,7 +8,8 @@ import { CircularProgress, Chip, Alert, - AlertTitle + AlertTitle, + Typography } from "@mui/material"; import ApiSingleton from "../../api/ApiSingleton"; import {GroupViewModel, AccountDataDto} from "@/api"; @@ -47,12 +48,25 @@ const GroupSelector: FC = (props) => { }, [props.selectedGroupId, props.groups]) const [isSubmitting, setIsSubmitting] = useState(false); - const [isError, setIsError] = useState(false); - const studentsInGroups = useMemo(() => groups.flatMap(g => g.studentsIds), - [groups, props.groups]); - const studentsWithoutGroup = useMemo(() => props.courseStudents.filter((cm) => !studentsInGroups.includes(cm.userId)), - [groups, props.courseStudents]); + const studentToGroups = useMemo(() => { + const map = new Map(); + (props.groups || []).forEach(g => { + g.studentsIds?.forEach(stId => { + if (!map.has(stId)) map.set(stId, []); + map.get(stId)!.push(g.name!); + }); + }); + return map; + }, [props.groups, props.selectedGroupId]); + + const studentsInMultipleGroups = useMemo(() => { + const set = new Set(); + studentToGroups.forEach((groups, studentId) => { + if (groups.length > 1) set.add(studentId); + }); + return set; + }, [studentToGroups]); const handleSubmitEdit = async () => { setIsSubmitting(true); @@ -78,7 +92,6 @@ const GroupSelector: FC = (props) => { } } catch (error) { console.error('Failed to update group:', error); - setIsError(true); } finally { setIsSubmitting(false); } @@ -129,12 +142,15 @@ const GroupSelector: FC = (props) => { fullWidth options={props.courseStudents} value={props.courseStudents?.filter(s => formState.memberIds.includes(s.userId!)) || []} - getOptionLabel={(option) => - `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim() - } + getOptionLabel={(option) => { + const groups = studentToGroups.get(option.userId!); + const groupSuffix = groups && groups.length > 0 + ? ' — в группе: ' + groups[0] + : ''; + return `${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}${groupSuffix}`.trim(); + }} filterSelectedOptions onChange={(_, value) => { - if (value.map(x => x.userId!).filter(Boolean).length === 0) return; setFormState(prev => ({ ...prev, memberIds: value @@ -149,6 +165,7 @@ const GroupSelector: FC = (props) => { {...getTagProps({index})} label={`${option.surname ?? ""} ${option.name ?? ""} / ${option.email ?? ""}`.trim()} key={option.userId} + style={studentsInMultipleGroups.has(option.userId!) ? {color: "#3f51b5"} : undefined} /> )) } @@ -159,6 +176,7 @@ const GroupSelector: FC = (props) => { placeholder="Выберите студентов" /> )} + noOptionsText={'Больше нет студентов для выбора'} /> + {studentsInMultipleGroups.size > 0 && formState.memberIds.some(id => studentsInMultipleGroups.has(id)) && + + Синим выделены студенты, состоящие в нескольких группах + } } {props.selectedGroupId == undefined && From 56fc63911021d52eb7a0c029abfaf415b8125378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 14 Apr 2026 21:30:17 +0300 Subject: [PATCH 094/100] refactor: separate student max sum calculation --- .../src/components/Courses/StudentStats.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 814356cd3..540e06027 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -125,13 +125,24 @@ const StudentStats: React.FC = (props) => { .reduce((sum, task) => sum + (task!.maxRating || 0), 0) const homeworksWithGroups = notTests.filter(h => h.groupId) - const homeworksWithoutGroupMaxSum = notTests.filter(h => !h.groupId) - .filter(h => !h.tags!.includes(BonusTag)) + const homeworksWithoutGroupMaxSum = notTests + .filter(h => !h.groupId) .flatMap(homework => homework.tasks) .reduce((sum, task) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); }, 0) + const getStudentHomeworkMaxSum = (studentId: string, isTests: boolean) => { + const works = isTests ? testHomeworks : notTests; + return works + .filter(h => (isTests || !h.tags!.includes(BonusTag)) && + (h.groupId === undefined || props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(studentId!))) + .flatMap(homework => homework.tasks) + .reduce((sum, task) => { + return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); + }, 0) + } + const hasHomeworks = homeworksWithoutGroupMaxSum > 0 || homeworksWithGroups.length > 0 const hasTests = testsWithGroupsMaxSum + testsWithoutGroupsMaxSum > 0 const showBestSolutions = isMentor && (hasHomeworks || hasTests) @@ -266,13 +277,7 @@ const StudentStats: React.FC = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) - const studentHomeworksMaxSum = notTests - .filter(h => !h.tags!.includes(BonusTag) && - (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => { - return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) + homeworksWithoutGroupMaxSum + const studentHomeworksMaxSum = getStudentHomeworkMaxSum(cm.id!, false) const testsSum = testGroups .map(group => { @@ -291,11 +296,7 @@ const StudentStats: React.FC = (props) => { .flat() .reduce((sum, rating) => sum + rating, 0) - const studentTestsMaxSum = testHomeworks - .filter(h => h.groupId !== undefined && - (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(cm.id!))) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => sum + (task!.maxRating || 0), 0) + testsWithoutGroupsMaxSum + const studentTestsMaxSum = getStudentHomeworkMaxSum(cm.id!, true) const bestSolutionsCount = bestTaskSolutions.values() .filter(x => x === cm.id) From 6a414a6d1e7dbe75dd8406b878ad945dd12ca18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Tue, 14 Apr 2026 21:58:22 +0300 Subject: [PATCH 095/100] refactor: simplify max sum calculation --- .../src/components/Courses/StudentStats.tsx | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 540e06027..5341e7be3 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -120,28 +120,20 @@ const StudentStats: React.FC = (props) => { .flatMap(h => h.tasks) .reduce((sum, task) => sum + (task!.maxRating || 0), 0) - const testsWithoutGroupsMaxSum = testHomeworks.filter(h => h.groupId == undefined) - .flatMap(h => h.tasks) - .reduce((sum, task) => sum + (task!.maxRating || 0), 0) - const homeworksWithGroups = notTests.filter(h => h.groupId) - const homeworksWithoutGroupMaxSum = notTests - .filter(h => !h.groupId) - .flatMap(homework => homework.tasks) - .reduce((sum, task) => { - return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); - }, 0) - const getStudentHomeworkMaxSum = (studentId: string, isTests: boolean) => { + const getMaxSum = (studentId: string, isTests: boolean = false) => { const works = isTests ? testHomeworks : notTests; return works .filter(h => (isTests || !h.tags!.includes(BonusTag)) && - (h.groupId === undefined || props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(studentId!))) + (h.groupId == undefined || (props.groups.find(g => g.id === h.groupId)?.studentsIds?.includes(studentId)))) .flatMap(homework => homework.tasks) .reduce((sum, task) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); }, 0) } + const homeworksWithoutGroupMaxSum = getMaxSum("", false); + const testsWithoutGroupsMaxSum = getMaxSum("", true); const hasHomeworks = homeworksWithoutGroupMaxSum > 0 || homeworksWithGroups.length > 0 const hasTests = testsWithGroupsMaxSum + testsWithoutGroupsMaxSum > 0 @@ -277,7 +269,7 @@ const StudentStats: React.FC = (props) => { .flatMap(t => StudentStatsUtils.calculateLastRatedSolution(t.solutions || [])?.rating || 0) || 0 ) .reduce((sum, rating) => sum + rating, 0) - const studentHomeworksMaxSum = getStudentHomeworkMaxSum(cm.id!, false) + const studentHomeworksMaxSum = getMaxSum(cm.id!, false) const testsSum = testGroups .map(group => { @@ -296,7 +288,7 @@ const StudentStats: React.FC = (props) => { .flat() .reduce((sum, rating) => sum + rating, 0) - const studentTestsMaxSum = getStudentHomeworkMaxSum(cm.id!, true) + const studentTestsMaxSum = getMaxSum(cm.id!, true) const bestSolutionsCount = bestTaskSolutions.values() .filter(x => x === cm.id) From 05c667b20eda7a34e2d894d0d75949156f1eb8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Wed, 15 Apr 2026 22:38:20 +0300 Subject: [PATCH 096/100] refactor: remove unused variables --- .../src/components/Courses/StudentStats.tsx | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 5341e7be3..38ffb3268 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -115,11 +115,6 @@ const StudentStats: React.FC = (props) => { .values() .value(); - const testsWithGroupsMaxSum = testHomeworks - .filter(h => h.groupId !== undefined) - .flatMap(h => h.tasks) - .reduce((sum, task) => sum + (task!.maxRating || 0), 0) - const homeworksWithGroups = notTests.filter(h => h.groupId) const getMaxSum = (studentId: string, isTests: boolean = false) => { @@ -132,11 +127,9 @@ const StudentStats: React.FC = (props) => { return sum + (task!.tags!.includes(BonusTag) ? 0 : (task!.maxRating || 0)); }, 0) } - const homeworksWithoutGroupMaxSum = getMaxSum("", false); - const testsWithoutGroupsMaxSum = getMaxSum("", true); - const hasHomeworks = homeworksWithoutGroupMaxSum > 0 || homeworksWithGroups.length > 0 - const hasTests = testsWithGroupsMaxSum + testsWithoutGroupsMaxSum > 0 + const hasHomeworks = notTests.length > 0 + const hasTests = testHomeworks.length > 0 const showBestSolutions = isMentor && (hasHomeworks || hasTests) const bestTaskSolutions = new Map() @@ -227,7 +220,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - ДЗ {homeworksWithGroups.length === 0 && `(${homeworksWithoutGroupMaxSum})`} + ДЗ {homeworksWithGroups.length === 0 && `(${getMaxSum("", false)})`} } {hasTests && = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - КР {homeworksWithGroups.length === 0 && `(${testsWithoutGroupsMaxSum})`} + КР {homeworksWithGroups.length === 0 && `(${getMaxSum("", true)})`} } {showBestSolutions && From f7f7d60d6315164ba41227141412a6c3079bd327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A0=D0=BE=D1=81=D0=B8=D0=BD=20=D0=A1=D0=B5=D0=BC=D1=91?= =?UTF-8?q?=D0=BD?= Date: Thu, 16 Apr 2026 18:26:11 +0300 Subject: [PATCH 097/100] fix: group tests counting --- hwproj.front/src/components/Common/GroupSelector.tsx | 2 +- hwproj.front/src/components/Courses/StudentStats.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index 755346ec3..c88968262 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -63,7 +63,7 @@ const GroupSelector: FC = (props) => { const studentsInMultipleGroups = useMemo(() => { const set = new Set(); studentToGroups.forEach((groups, studentId) => { - if (groups.length > 1) set.add(studentId); + if (groups.length > 0) set.add(studentId); }); return set; }, [studentToGroups]); diff --git a/hwproj.front/src/components/Courses/StudentStats.tsx b/hwproj.front/src/components/Courses/StudentStats.tsx index 38ffb3268..e7e0b4a23 100644 --- a/hwproj.front/src/components/Courses/StudentStats.tsx +++ b/hwproj.front/src/components/Courses/StudentStats.tsx @@ -116,6 +116,7 @@ const StudentStats: React.FC = (props) => { .value(); const homeworksWithGroups = notTests.filter(h => h.groupId) + const testsWithGroups = testHomeworks.filter(t => t.groupId != undefined) const getMaxSum = (studentId: string, isTests: boolean = false) => { const works = isTests ? testHomeworks : notTests; @@ -229,7 +230,7 @@ const StudentStats: React.FC = (props) => { paddingRight: 5, borderLeft: borderStyle, }}> - КР {homeworksWithGroups.length === 0 && `(${getMaxSum("", true)})`} + КР {testsWithGroups.length === 0 && `(${getMaxSum("", true)})`} } {showBestSolutions && @@ -362,7 +363,7 @@ const StudentStats: React.FC = (props) => { backgroundColor: StudentStatsUtils.getRatingColor(testsSum, studentTestsMaxSum), fontSize: 16 }} - label={`${testsSum} ${testHomeworks.some(h => h.groupId !== undefined) ? `/ ${studentTestsMaxSum}` : ""}`}/>} + label={`${testsSum} ${testsWithGroups.length > 0 ? `/ ${studentTestsMaxSum}` : ""}`}/>} } {showBestSolutions && Date: Thu, 16 Apr 2026 19:28:06 +0300 Subject: [PATCH 098/100] fix: students to group counting --- hwproj.front/src/components/Common/GroupSelector.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index c88968262..ddbcfa009 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -51,19 +51,19 @@ const GroupSelector: FC = (props) => { const studentToGroups = useMemo(() => { const map = new Map(); - (props.groups || []).forEach(g => { + (props.groups || []).concat(formState).forEach(g => { g.studentsIds?.forEach(stId => { if (!map.has(stId)) map.set(stId, []); map.get(stId)!.push(g.name!); }); }); return map; - }, [props.groups, props.selectedGroupId]); + }, [props.groups, props.selectedGroupId, formState.memberIds]); const studentsInMultipleGroups = useMemo(() => { const set = new Set(); studentToGroups.forEach((groups, studentId) => { - if (groups.length > 0) set.add(studentId); + if (groups.length > 1) set.add(studentId); }); return set; }, [studentToGroups]); From ec1b1a8fe29aff89e6d7dee444d433afe0382912 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 20 Apr 2026 02:20:24 +0300 Subject: [PATCH 099/100] fix --- .../Services/GroupsService.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs index 58641d9f0..3ee1dae8b 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Services/GroupsService.cs @@ -92,18 +92,11 @@ await _groupMatesRepository public async Task GetStudentGroupsAsync(long courseId, string studentId) { - var studentGroupsIds = await _groupMatesRepository - .FindAll(cm => cm.StudentId == studentId) - .Select(cm => cm.GroupId) - .ToArrayAsync() - .ConfigureAwait(false); - - var studentGroups = await _groupsRepository - .GetGroupsWithGroupMatesAsync(studentGroupsIds) - .ConfigureAwait(false); + var studentGroups = await _groupsRepository.GetGroupsWithGroupMatesByCourse(courseId) + .Where(x => x.GroupMates.Any(g => g.StudentId == studentId)) + .ToListAsync(); return studentGroups - .Where(g => g.CourseId == courseId) .Select(c => _mapper.Map(c)) .ToArray(); } From 31a0f599b6bbe9692999f4c82aff6a85f07fa961 Mon Sep 17 00:00:00 2001 From: "Alexey.Berezhnykh" Date: Mon, 20 Apr 2026 02:21:54 +0300 Subject: [PATCH 100/100] wip --- hwproj.front/src/components/Common/GroupSelector.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hwproj.front/src/components/Common/GroupSelector.tsx b/hwproj.front/src/components/Common/GroupSelector.tsx index ddbcfa009..9d139a7d5 100644 --- a/hwproj.front/src/components/Common/GroupSelector.tsx +++ b/hwproj.front/src/components/Common/GroupSelector.tsx @@ -178,6 +178,10 @@ const GroupSelector: FC = (props) => { )} noOptionsText={'Больше нет студентов для выбора'} /> + {studentsInMultipleGroups.size > 0 && formState.memberIds.some(id => studentsInMultipleGroups.has(id)) && + + Синим выделены студенты, состоящие в нескольких группах + } - {studentsInMultipleGroups.size > 0 && formState.memberIds.some(id => studentsInMultipleGroups.has(id)) && - - Синим выделены студенты, состоящие в нескольких группах - } } {props.selectedGroupId == undefined &&