From 953ce51ca6d8c3551514368020be8600f8168183 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 27 Apr 2026 09:37:26 +0300 Subject: [PATCH 01/20] Add deadline criteria --- .../ViewModels/CriterionViewModel.cs | 4 +- .../Domains/MappingExtensions.cs | 6 +- ...60427093000_CriterionArguments.Designer.cs | 16 +++ .../20260427093000_CriterionArguments.cs | 25 ++++ .../Migrations/CourseContextModelSnapshot.cs | 3 + .../Models/Criterion.cs | 4 +- .../Repositories/TasksRepository.cs | 5 +- hwproj.front/src/api/api.ts | 9 +- .../Solutions/TaskSolutionComponent.tsx | 59 +++++++-- .../Tasks/CourseTaskExperimental.tsx | 114 +++++++++++++++++- .../src/components/Tasks/TaskCriteria.tsx | 14 ++- 11 files changed, 236 insertions(+), 23 deletions(-) create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.Designer.cs create mode 100644 HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.cs diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs index 7f07a540a..fe6c1c925 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs @@ -4,7 +4,8 @@ namespace HwProj.Models.CoursesService.ViewModels { public enum CriterionType { - Free = 0 + Free = 0, + Deadline = 1 } public class CriterionViewModel @@ -13,5 +14,6 @@ public class CriterionViewModel public CriterionType Type { get; set; } [Required] public string Name { get; set; } = null!; [Range(0, int.MaxValue)] public int MaxPoints { get; set; } + public string Arguments { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 152bf0410..48ce7a8ea 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -64,7 +64,8 @@ public static HomeworkTaskViewModel ToHomeworkTaskViewModel(this HomeworkTask ta Id = c.Id, Type = (CriterionType)c.Type, Name = c.Name, - MaxPoints = c.MaxPoints + MaxPoints = c.MaxPoints, + Arguments = c.Arguments }) .ToList(), }; @@ -120,7 +121,8 @@ public static CoursePreview ToCoursePreview(this Course course) Id = criterion.Id, Type = (Models.CriterionType)criterion.Type, Name = criterion.Name, - MaxPoints = criterion.MaxPoints + MaxPoints = criterion.MaxPoints, + Arguments = criterion.Arguments }; public static HomeworkTask ToHomeworkTask(this PostTaskViewModel postTaskViewModel) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.Designer.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.Designer.cs new file mode 100644 index 000000000..6865af99a --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.Designer.cs @@ -0,0 +1,16 @@ +// +using HwProj.CoursesService.API.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + [DbContext(typeof(CourseContext))] + [Migration("20260427093000_CriterionArguments")] + partial class CriterionArguments + { + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.cs new file mode 100644 index 000000000..fac8c4c21 --- /dev/null +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/20260427093000_CriterionArguments.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace HwProj.CoursesService.API.Migrations +{ + public partial class CriterionArguments : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Arguments", + table: "Criteria", + type: "nvarchar(max)", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Arguments", + table: "Criteria"); + } + } +} diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs index 4b3bb83b5..199917cf8 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Migrations/CourseContextModelSnapshot.cs @@ -128,6 +128,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxPoints") .HasColumnType("int"); + b.Property("Arguments") + .HasColumnType("nvarchar(max)"); + b.Property("Name") .HasColumnType("nvarchar(max)"); diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs index debc0b99b..b2db42563 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs @@ -5,7 +5,8 @@ namespace HwProj.CoursesService.API.Models { public enum CriterionType { - Free = 0 + Free = 0, + Deadline = 1 } public class Criterion : IEntity @@ -17,5 +18,6 @@ public class Criterion : IEntity public CriterionType Type { get; set; } public string Name { get; set; } public int MaxPoints { get; set; } + public string Arguments { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs index 769a23be6..b8405e9ee 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Repositories/TasksRepository.cs @@ -89,10 +89,11 @@ public bool Equals(Criterion? x, Criterion? y) x.TaskId == y.TaskId && x.Type == y.Type && x.Name == y.Name && - x.MaxPoints == y.MaxPoints; + x.MaxPoints == y.MaxPoints && + x.Arguments == y.Arguments; } - public int GetHashCode(Criterion obj) => HashCode.Combine(obj.Id, obj.TaskId, obj.Name); + public int GetHashCode(Criterion obj) => HashCode.Combine(obj.Id, obj.TaskId, obj.Name, obj.Arguments); } } } diff --git a/hwproj.front/src/api/api.ts b/hwproj.front/src/api/api.ts index 4e0c6dfec..e42934f1c 100644 --- a/hwproj.front/src/api/api.ts +++ b/hwproj.front/src/api/api.ts @@ -655,7 +655,8 @@ export interface CreateHomeworkViewModel { * @enum {string} */ export enum CriterionType { - NUMBER_0 = 0 + NUMBER_0 = 0, + NUMBER_1 = 1 } /** * @@ -687,6 +688,12 @@ export interface CriterionViewModel { * @memberof CriterionViewModel */ maxPoints?: number; + /** + * + * @type {string} + * @memberof CriterionViewModel + */ + arguments?: string; } /** * diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index b5276a530..66e3650c2 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -49,6 +49,8 @@ import {UserAvatar} from "@/components/Common/UserAvatar"; type TaskWithCriteria = HomeworkTaskViewModel & {}; +const CriterionTypeDeadline = 1; + type CriterionRating = { criterionId: number; name: string; @@ -136,9 +138,23 @@ const TaskSolutionComponent: FC = (props) => { const initialDraft = loadCriteriaDraft(); + const getDeadlineCriterionValue = (criterion: { arguments?: string; maxPoints?: number }) => { + if (!props.solution?.publicationDate || !criterion.arguments) return Number.NaN; + + const solutionDate = new Date(props.solution.publicationDate).getTime(); + const deadlineDate = new Date(criterion.arguments).getTime(); + + if (Number.isNaN(solutionDate) || Number.isNaN(deadlineDate)) return Number.NaN; + + return solutionDate <= deadlineDate ? (criterion.maxPoints ?? 0) : 0; + }; + const [criterionRatings, setCriterionRatings] = useState(() => (taskWithCriteria.criteria ?? []).map(c => { const id = c.id!; + const deadlineValue = c.type === CriterionTypeDeadline + ? getDeadlineCriterionValue(c) + : Number.NaN; const draftValue = initialDraft?.criteria ?.find(x => x.criterionId === id)?.value; @@ -146,7 +162,7 @@ const TaskSolutionComponent: FC = (props) => { criterionId: id, name: c.name ?? "", maxPoints: c.maxPoints ?? 0, - value: draftValue || NaN, + value: Number.isFinite(deadlineValue) ? deadlineValue : (draftValue ?? NaN), comment: "", }; }) @@ -171,6 +187,9 @@ const TaskSolutionComponent: FC = (props) => { setCriterionRatings( (taskWithCriteria.criteria ?? []).map(c => { const id = c.id ?? 0; + const deadlineValue = c.type === CriterionTypeDeadline + ? getDeadlineCriterionValue(c) + : Number.NaN; const draftValue = draft?.criteria ?.find(x => x.criterionId === id)?.value; @@ -178,7 +197,7 @@ const TaskSolutionComponent: FC = (props) => { criterionId: id, name: c.name ?? "", maxPoints: c.maxPoints ?? 0, - value: draftValue || NaN, + value: Number.isFinite(deadlineValue) ? deadlineValue : (draftValue ?? NaN), comment: "", }; }) @@ -309,8 +328,13 @@ const TaskSolutionComponent: FC = (props) => { if (!hasCriteria || criterionRatings.length === 0) return baseComment; const rows: string[] = criterionRatings.map(cr => { + const criterion = taskWithCriteria.criteria?.find(c => c.id === cr.criterionId); const safeValue = Number.isFinite(cr.value) ? cr.value : 0; - return `| ${cr.name} | ${safeValue} / ${cr.maxPoints} |`; + const deadlineText = criterion?.type === CriterionTypeDeadline && criterion.arguments + ? `, дедлайн ${Utils.renderDateWithoutSeconds(new Date(criterion.arguments))}` + : ""; + + return `| ${cr.name}${deadlineText} | ${safeValue} / ${cr.maxPoints} |`; }); if ((extraScore ?? 0) !== 0) { @@ -423,9 +447,10 @@ const TaskSolutionComponent: FC = (props) => { return ( + @@ -577,4 +681,4 @@ const CourseTaskExperimental: FC<{ ); } -export default CourseTaskExperimental; \ No newline at end of file +export default CourseTaskExperimental; diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index 190801521..7b3399f68 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -1,6 +1,9 @@ import {HomeworkTaskViewModel} from "@/api"; import {Chip, Divider, Stack, Typography} from "@mui/material"; import {FC} from "react"; +import Utils from "../../services/Utils"; + +const CriterionTypeDeadline = 1; const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { return task.criteria && task.criteria.length > 0 ? ( @@ -14,7 +17,14 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { {task.criteria.map(c => ( - {c.name} + + {c.name} + {c.type === CriterionTypeDeadline && c.arguments && ( + + Дедлайн: {Utils.renderDateWithoutSeconds(new Date(c.arguments))} + + )} + ))} @@ -22,4 +32,4 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { ) : null } -export default TaskCriteria; \ No newline at end of file +export default TaskCriteria; From 058bb7abf18fc951b26221b886d4f405e64d149b Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 27 Apr 2026 13:37:28 +0300 Subject: [PATCH 02/20] Refine deadline criterion scoring --- .../HwProj.CoursesService.API/Startup.cs | 11 + hwproj.front/package-lock.json | 53 +++-- .../Solutions/TaskSolutionComponent.tsx | 6 +- .../Tasks/CourseTaskExperimental.tsx | 225 +++++++++++------- .../src/components/Tasks/TaskCriteria.tsx | 32 ++- 5 files changed, 213 insertions(+), 114 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs index 62461edc6..94eeae315 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Startup.cs @@ -76,6 +76,17 @@ public void Configure(IApplicationBuilder app, IHostEnvironment env, CourseConte app.UseEndpoints(x => x.MapControllers()); app.UseDatabase(env, context); + EnsureCriterionArgumentsColumn(context); + } + + private static void EnsureCriterionArgumentsColumn(CourseContext context) + { + context.Database.ExecuteSqlRaw(@" +IF OBJECT_ID(N'[Criteria]', N'U') IS NOT NULL + AND COL_LENGTH(N'[Criteria]', N'Arguments') IS NULL +BEGIN + ALTER TABLE [Criteria] ADD [Arguments] nvarchar(max) NULL; +END"); } } } diff --git a/hwproj.front/package-lock.json b/hwproj.front/package-lock.json index 1c7b32e30..b6f4352cf 100644 --- a/hwproj.front/package-lock.json +++ b/hwproj.front/package-lock.json @@ -4977,6 +4977,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-6.5.16.tgz", "integrity": "sha512-p3DqQi+8QRL5k7jXhXmJZLsE/GqHqyY6PcoA1oNTJr0try48uhTGUOYkgzmqtDaa/qPFO5LP+xCPzZXckGtquQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/api": "6.5.16", @@ -5004,12 +5005,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/api": { "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/api/-/api-6.5.16.tgz", "integrity": "sha512-HOsuT8iomqeTMQJrRx5U8nsC7lJTwRr1DhdD0SzlqL4c80S/7uuCy4IZvOt4sYQjOzW5fOo/kamcoBXyLproTA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/channels": "6.5.16", @@ -5043,6 +5046,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/builder-webpack4": { @@ -5441,6 +5445,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/channels/-/channels-6.5.16.tgz", "integrity": "sha512-VylzaWQZaMozEwZPJdyJoz+0jpDa8GRyaqu9TGG6QGv+KU5POoZaGLDkRE7TzWkyyP0KQLo80K99MssZCpgSeg==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5500,6 +5505,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.5.16.tgz", "integrity": "sha512-pxcNaCj3ItDdicPTXTtmYJE3YC1SjxFrBmHcyrN+nffeNyiMuViJdOOZzzzucTUG0wcOOX8jaSyak+nnHg5H1Q==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2", @@ -5514,6 +5520,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/components/-/components-6.5.16.tgz", "integrity": "sha512-LzBOFJKITLtDcbW9jXl0/PaG+4xAz25PK8JxPZpIALbmOpYWOAPcO6V9C2heX6e6NgWFMUxjplkULEk9RCQMNA==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -5538,6 +5545,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/core": { @@ -5704,6 +5712,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.5.16.tgz", "integrity": "sha512-qMZQwmvzpH5F2uwNUllTPg6eZXr2OaYZQRRN8VZJiuorZzDNdAFmiVWMWdkThwmyLEJuQKXxqCL8lMj/7PPM+g==", + "dev": true, "license": "MIT", "dependencies": { "core-js": "^3.8.2" @@ -5804,6 +5813,7 @@ "version": "0.0.2--canary.4566f4d.1", "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.2--canary.4566f4d.1.tgz", "integrity": "sha512-9OVvMVh3t9znYZwb0Svf/YQoxX2gVOeQTGe2bses2yj+a3+OJnCrUF3/hGv6Em7KujtOdL2LL+JnG49oMVGFgQ==", + "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.15" @@ -6276,6 +6286,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/router/-/router-6.5.16.tgz", "integrity": "sha512-ZgeP8a5YV/iuKbv31V8DjPxlV4AzorRiR8OuSt/KqaiYXNXlOoQDz/qMmiNcrshrfLpmkzoq7fSo4T8lWo2UwQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6297,12 +6308,14 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/semver": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@storybook/semver/-/semver-7.3.2.tgz", "integrity": "sha512-SWeszlsiPsMI0Ps0jVNtH64cI5c0UF3f7KgjVKJoNP30crQ6wUSddY2hsdeczZXEKVJGEn50Q60flcGsQGIcrg==", + "dev": true, "license": "ISC", "dependencies": { "core-js": "^3.6.5", @@ -6319,6 +6332,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { "locate-path": "^5.0.0", @@ -6332,6 +6346,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "license": "MIT", "dependencies": { "p-locate": "^4.1.0" @@ -6344,6 +6359,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", "dependencies": { "p-try": "^2.0.0" @@ -6359,6 +6375,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { "p-limit": "^2.2.0" @@ -6442,6 +6459,7 @@ "version": "6.5.16", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-6.5.16.tgz", "integrity": "sha512-hNLctkjaYLRdk1+xYTkC1mg4dYz2wSv6SqbLpcKMbkPHTE0ElhddGPHQqB362md/w9emYXNkt1LSMD8Xk9JzVQ==", + "dev": true, "license": "MIT", "dependencies": { "@storybook/client-logger": "6.5.16", @@ -6462,6 +6480,7 @@ "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/ui": { @@ -7088,6 +7107,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.3.tgz", "integrity": "sha512-/CLhCW79JUeLKznI6mbVieGbl4QU5Hfn+6udw1YHZoofASjbQ5zaP5LzAUZYDpRYEjS4/P+DhEgyJ/PQmGGTWw==", + "dev": true, "license": "MIT" }, "node_modules/@types/isomorphic-fetch": { @@ -7456,6 +7476,7 @@ "version": "1.18.8", "resolved": "https://registry.npmjs.org/@types/webpack-env/-/webpack-env-1.18.8.tgz", "integrity": "sha512-G9eAoJRMLjcvN4I08wB5I7YofOb/kaJNd5uoCMX+LbKXTPCF+ZIHuqTnFaK9Jz1rgs035f9JUPUhNFtqgucy/A==", + "dev": true, "license": "MIT" }, "node_modules/@types/webpack-sources": { @@ -18027,6 +18048,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, "license": "MIT" }, "node_modules/is-generator-function": { @@ -18149,6 +18171,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18226,6 +18249,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -18697,13 +18721,6 @@ "node": ">= 10.13.0" } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", - "license": "MIT", - "peer": true - }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -19343,6 +19360,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true, "license": "MIT" }, "node_modules/map-visit": { @@ -20535,6 +20553,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, "license": "MIT", "dependencies": { "map-or-similar": "^1.5.0" @@ -22501,6 +22520,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -22703,6 +22723,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -22855,18 +22876,6 @@ "node": ">=6" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "license": "MIT", - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/portable-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/portable-fetch/-/portable-fetch-3.0.0.tgz", @@ -27164,6 +27173,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -28040,6 +28050,7 @@ "version": "2.14.4", "resolved": "https://registry.npmjs.org/store2/-/store2-2.14.4.tgz", "integrity": "sha512-srTItn1GOvyvOycgxjAnPA63FZNwy0PTyUBFMHRM+hVFltAeoh0LmNBz9SZqUS9mMqGk8rfyWyXn3GH5ReJ8Zw==", + "dev": true, "license": "MIT" }, "node_modules/stream-browserify": { @@ -28673,6 +28684,7 @@ "version": "6.0.8", "resolved": "https://registry.npmjs.org/telejson/-/telejson-6.0.8.tgz", "integrity": "sha512-nerNXi+j8NK1QEfBHtZUN/aLdDcyupA//9kAboYLrtzZlPLpUfqbVGWb9zz91f/mIjRbAYhbgtnJHY8I1b5MBg==", + "dev": true, "license": "MIT", "dependencies": { "@types/is-function": "^1.0.0", @@ -28689,6 +28701,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29210,6 +29223,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -30102,6 +30116,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/util.promisify": { diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index 66e3650c2..a4b5f4201 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -146,7 +146,7 @@ const TaskSolutionComponent: FC = (props) => { if (Number.isNaN(solutionDate) || Number.isNaN(deadlineDate)) return Number.NaN; - return solutionDate <= deadlineDate ? (criterion.maxPoints ?? 0) : 0; + return solutionDate <= deadlineDate ? 0 : -(criterion.maxPoints ?? 0); }; const [criterionRatings, setCriterionRatings] = useState(() => @@ -638,7 +638,7 @@ const TaskSolutionComponent: FC = (props) => { const isFilled = hasExplicitValue && (isThumbCriterion || numericValue !== 0); const isDeadlineCriterion = c.type === CriterionTypeDeadline; const deadlineDate = c.arguments ? new Date(c.arguments) : undefined; - const deadlinePassed = isDeadlineCriterion && numericValue === (c.maxPoints ?? 0); + const deadlinePassed = isDeadlineCriterion && numericValue === 0; return ( @@ -675,7 +675,7 @@ const TaskSolutionComponent: FC = (props) => { ) : isThumbCriterion ? ( diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index c7c804f69..900c40617 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -13,6 +13,7 @@ import { Link, Checkbox, FormControlLabel, + Menu, MenuItem } from "@mui/material"; import {MarkdownEditor, MarkdownPreview} from "components/Common/MarkdownEditor"; @@ -51,7 +52,6 @@ type TaskEditData = HomeworkTaskViewModel & { suggestedMaxRating?: number; }; -const CriterionTypeFree = CriterionType.NUMBER_0; const CriterionTypeDeadline = CriterionType.NUMBER_1; const CourseTaskEditor: FC<{ @@ -73,6 +73,9 @@ const CourseTaskEditor: FC<{ const [criteria, setCriteria] = useState(taskData.task.criteria || []) const [isCriteriaOpen, setIsCriteriaOpen] = useState(false) + const [addCriterionAnchor, setAddCriterionAnchor] = useState(null) + + const isDeadlineCriterion = (criterion: CriterionViewModel) => criterion.type === CriterionTypeDeadline; const addDefaultCriterion = () => { setCriteria(prev => [ @@ -80,6 +83,7 @@ const CourseTaskEditor: FC<{ {id: 0, type: 0, name: `Критерий №${prev.length + 1}`, maxPoints: 1} ]); setIsCriteriaOpen(true); + setAddCriterionAnchor(null); }; const addDeadlineCriterion = () => { @@ -92,12 +96,13 @@ const CourseTaskEditor: FC<{ { id: 0, type: CriterionTypeDeadline, - name: `Дедлайн №${prev.length + 1}`, + name: "Сдано вовремя", maxPoints: 1, arguments: deadline.toISOString(), } ]); setIsCriteriaOpen(true); + setAddCriterionAnchor(null); }; const updateCriterion = (index: number, patch: Partial) => @@ -111,7 +116,7 @@ const CourseTaskEditor: FC<{ const criteriaTotalPoints = useMemo( () => (criteria).reduce( - (sum, c) => sum + (c.maxPoints || 0), + (sum, c) => sum + (isDeadlineCriterion(c) ? 0 : (c.maxPoints || 0)), 0 ), [criteria] @@ -119,7 +124,20 @@ const CourseTaskEditor: FC<{ const autoMaxFromCriteria = criteria.length > 0; const criteriaHasErrors = criteria.some(c => - !c.name || (c.maxPoints ?? 0) <= 0 || (c.type === CriterionTypeDeadline && !c.arguments) + !c.name || (c.maxPoints ?? 0) <= 0 || (isDeadlineCriterion(c) && !c.arguments) + ); + + const renderAddCriterionMenu = () => ( + setAddCriterionAnchor(null)} + > + Обычный критерий + + Автокритерий: дедлайн + + ); useEffect(() => { @@ -365,21 +383,10 @@ const CourseTaskEditor: FC<{ + onClick={(e) => setAddCriterionAnchor(e.currentTarget)}> Добавить критерий оценивания - - -  или  - - - - - добавить дедлайн-критерий - - )} {criteria.length > 0 && ( @@ -407,7 +414,121 @@ const CourseTaskEditor: FC<{ - {criteria.map((c, index) => ( + {criteria.map((c, index) => isDeadlineCriterion(c) ? ( + + + + + + + Критерий дедлайна + + + + + updateCriterion(index, {name: e.target.value.slice(0, 50)})} + /> + + + + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + onBlur={(e) => + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + /> + + + removeCriterion(index)} + color="error" + size="small" + > + + + + + + Источник дедлайна + + + + + + + + + updateCriterion(index, { + arguments: e.target.value + ? new Date(e.target.value).toISOString() + : undefined, + }) + } + InputLabelProps={{shrink: true}} + /> + + + + + Как сработает правило + + + После {c.arguments ? Utils.renderDateWithoutSeconds(new Date(c.arguments)) : "дедлайна"} будет списан {c.maxPoints || 1} балл + + + + + + ) : ( - - { - const type = Number(e.target.value) as CriterionType; - updateCriterion(index, { - type, - arguments: type === CriterionTypeDeadline - ? c.arguments || new Date().toISOString() - : undefined, - }); - }} - > - Обычный - Дедлайн - - - - {c.type === CriterionTypeDeadline && ( - - - updateCriterion(index, { - arguments: e.target.value - ? new Date(e.target.value).toISOString() - : undefined, - }) - } - InputLabelProps={{ - shrink: true, - }} - /> - - )} - setAddCriterionAnchor(e.currentTarget)} sx={{ textTransform: "none", fontSize: "15px", @@ -532,31 +607,11 @@ const CourseTaskEditor: FC<{ > + Добавить критерий оценивания - )} + {renderAddCriterionMenu()} diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index 7b3399f68..ad839f823 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -17,15 +17,33 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { {task.criteria.map(c => ( - - {c.name} - {c.type === CriterionTypeDeadline && c.arguments && ( - - Дедлайн: {Utils.renderDateWithoutSeconds(new Date(c.arguments))} - + + {c.type === CriterionTypeDeadline && ( + )} + + {c.name} + {c.type === CriterionTypeDeadline && c.arguments && ( + + До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} + + )} + - + ))} From 276941c10f5bfa19baaadd3c777ad90e866f7534 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Tue, 28 Apr 2026 12:27:50 +0300 Subject: [PATCH 03/20] Fix homework task saving and deadline criteria UI --- .../Domains/MappingExtensions.cs | 3 +- .../Homeworks/CourseHomeworkExperimental.tsx | 87 +++++++++++-------- .../Tasks/CourseTaskExperimental.tsx | 70 ++++++++------- .../src/components/Tasks/TaskCriteria.tsx | 4 +- 4 files changed, 93 insertions(+), 71 deletions(-) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index 48ce7a8ea..c7d19d1b5 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -136,7 +136,8 @@ public static HomeworkTask ToHomeworkTask(this PostTaskViewModel postTaskViewMod IsDeadlineStrict = postTaskViewModel.IsDeadlineStrict, PublicationDate = postTaskViewModel.PublicationDate, IsBonusExplicit = postTaskViewModel.IsBonusExplicit, - Criteria = postTaskViewModel.Criteria.Select(x => x.ToCriterion()).ToList(), + Criteria = postTaskViewModel.Criteria?.Select(x => x.ToCriterion()).ToList() + ?? new System.Collections.Generic.List(), }; public static Homework ToHomework(this CreateHomeworkViewModel homework) diff --git a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx index 04f6b8911..ea6486fbe 100644 --- a/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx +++ b/hwproj.front/src/components/Homeworks/CourseHomeworkExperimental.tsx @@ -43,6 +43,8 @@ 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'; +import ErrorsHandler from "@/components/Utils/ErrorsHandler"; +import {enqueueSnackbar} from "notistack"; export interface HomeworkAndFilesInfo { homework: HomeworkViewModel & { isModified?: boolean }, @@ -243,43 +245,54 @@ const CourseHomeworkEditor: FC<{ e.preventDefault() setHandleSubmitLoading(true) - const update = { - homeworkId: homeworkId, - title: title!, - description: description, - tags: tags, - hasDeadline: metadata.hasDeadline, - deadlineDate: metadata.deadlineDate, - isDeadlineStrict: metadata.isDeadlineStrict, - publicationDate: metadata.publicationDate, - groupId: selectedGroupId, - actionOptions: editOptions, - tasks: isNewHomework ? homework.tasks!.map(t => { - const task: PostTaskViewModel = { - ...t, - title: t.title!, - maxRating: t.maxRating! - } - return task - }) : [] - } + try { + const update = { + homeworkId: homeworkId, + title: title!, + description: description, + tags: tags, + hasDeadline: metadata.hasDeadline, + deadlineDate: metadata.deadlineDate, + isDeadlineStrict: metadata.isDeadlineStrict, + publicationDate: metadata.publicationDate, + groupId: selectedGroupId, + actionOptions: editOptions, + tasks: isNewHomework ? homework.tasks!.map(t => { + const task: PostTaskViewModel = { + ...t, + title: t.title!, + maxRating: t.maxRating!, + criteria: t.criteria || [] + } + return task + }) : [] + } - const updatedHomework = isNewHomework - ? await ApiSingleton.homeworksApi.homeworksAddHomework(courseId!, update) - : await ApiSingleton.homeworksApi.homeworksUpdateHomework(+homeworkId!, update) - - const updatedHomeworkId = updatedHomework.value!.id! - await handleFilesChange( - courseId, CourseUnitType.Homework, updatedHomeworkId, - props.onStartProcessing, - () => { - if (isNewHomework) props.onUpdate({ - homework: update, - isDeleted: true - }) // remove fake homework - props.onUpdate({homework: updatedHomework.value!, isSaved: true}); - }, - ); + const updatedHomework = isNewHomework + ? await ApiSingleton.homeworksApi.homeworksAddHomework(courseId!, update) + : await ApiSingleton.homeworksApi.homeworksUpdateHomework(+homeworkId!, update) + + const updatedHomeworkId = updatedHomework.value!.id! + await handleFilesChange( + courseId, CourseUnitType.Homework, updatedHomeworkId, + props.onStartProcessing, + () => { + if (isNewHomework) props.onUpdate({ + homework: update, + isDeleted: true + }) // remove fake homework + props.onUpdate({homework: updatedHomework.value!, isSaved: true}); + }, + ); + } catch (error) { + const errors = await ErrorsHandler.getErrorMessages(error as Response, "errors"); + enqueueSnackbar(errors[0] || "Не удалось сохранить задание", { + variant: "error", + autoHideDuration: 4000, + }); + } finally { + setHandleSubmitLoading(false) + } } const isDisabled = hasErrors || !isLoaded || taskHasErrors @@ -569,4 +582,4 @@ const CourseHomeworkExperimental: FC<{ } } -export default CourseHomeworkExperimental; \ No newline at end of file +export default CourseHomeworkExperimental; diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 900c40617..fe98acb58 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -35,6 +35,8 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import TaskCriteria from "./TaskCriteria"; import {BonusTag} from "@/components/Common/HomeworkTags"; import Utils from "../../services/Utils"; +import ErrorsHandler from "@/components/Utils/ErrorsHandler"; +import {enqueueSnackbar} from "notistack"; interface IEditTaskMetadataState { @@ -226,26 +228,36 @@ const CourseTaskEditor: FC<{ e.preventDefault() setHandleSubmitLoading(true) - const update = { - ...metadata!, - title: title!, - description: description, - isBonusExplicit: isBonusExplicit, - maxRating: maxRating, - actionOptions: editOptions, - criteria: criteria, - }; - - const updatedTask = isNewTask - ? await ApiSingleton.tasksApi.tasksAddTask(homework.id!, update) - : await ApiSingleton.tasksApi.tasksUpdateTask(+id!, update) - - if (isNewTask) - props.onUpdate({ - task: props.speculativeTask, - isDeleted: true, - }) - props.onUpdate({task: updatedTask.value!, isSaved: true}) + try { + const update = { + ...metadata!, + title: title!, + description: description, + isBonusExplicit: isBonusExplicit, + maxRating: maxRating, + actionOptions: editOptions, + criteria: criteria, + }; + + const updatedTask = isNewTask + ? await ApiSingleton.tasksApi.tasksAddTask(homework.id!, update) + : await ApiSingleton.tasksApi.tasksUpdateTask(+id!, update) + + if (isNewTask) + props.onUpdate({ + task: props.speculativeTask, + isDeleted: true, + }) + props.onUpdate({task: updatedTask.value!, isSaved: true}) + } catch (error) { + const errors = await ErrorsHandler.getErrorMessages(error as Response, "errors"); + enqueueSnackbar(errors[0] || "Не удалось сохранить задачу", { + variant: "error", + autoHideDuration: 4000, + }); + } finally { + setHandleSubmitLoading(false) + } } const deleteTask = async () => { @@ -436,7 +448,7 @@ const CourseTaskEditor: FC<{ fontWeight: 600, }} /> - + Критерий дедлайна @@ -457,7 +469,7 @@ const CourseTaskEditor: FC<{ label="Штраф" type="number" size="small" - sx={{width: 115}} + sx={{width: 110}} value={-(c.maxPoints ?? 1)} inputProps={{max: -1}} onChange={(e) => @@ -481,15 +493,6 @@ const CourseTaskEditor: FC<{ - - - Источник дедлайна - - - - - - + + + На основе дедлайна + + = ({task}) => { {c.name} {c.type === CriterionTypeDeadline && c.arguments && ( - До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} + До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} · Штраф )} @@ -42,7 +42,7 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { style={{fontSize: 14}} size={"small"} color={"default"} - label={c.type === CriterionTypeDeadline ? `-${c.maxPoints}` : c.maxPoints} + label={c.maxPoints} /> ))} From 6bde54e45b2fd97052196a28cb9a4c93cdfbb3e1 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Tue, 28 Apr 2026 12:47:59 +0300 Subject: [PATCH 04/20] Fix deadline criterion arguments validation --- .../ViewModels/CriterionViewModel.cs | 2 +- .../Domains/MappingExtensions.cs | 3 +- .../Models/Criterion.cs | 2 +- .../Tasks/CourseTaskExperimental.tsx | 91 ++++++++----------- 4 files changed, 43 insertions(+), 55 deletions(-) diff --git a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs index fe6c1c925..c8005e5e3 100644 --- a/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs +++ b/HwProj.Common/HwProj.Models/CoursesService/ViewModels/CriterionViewModel.cs @@ -14,6 +14,6 @@ public class CriterionViewModel public CriterionType Type { get; set; } [Required] public string Name { get; set; } = null!; [Range(0, int.MaxValue)] public int MaxPoints { get; set; } - public string Arguments { get; set; } + public string? Arguments { get; set; } } } diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs index c7d19d1b5..48ce7a8ea 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Domains/MappingExtensions.cs @@ -136,8 +136,7 @@ public static HomeworkTask ToHomeworkTask(this PostTaskViewModel postTaskViewMod IsDeadlineStrict = postTaskViewModel.IsDeadlineStrict, PublicationDate = postTaskViewModel.PublicationDate, IsBonusExplicit = postTaskViewModel.IsBonusExplicit, - Criteria = postTaskViewModel.Criteria?.Select(x => x.ToCriterion()).ToList() - ?? new System.Collections.Generic.List(), + Criteria = postTaskViewModel.Criteria.Select(x => x.ToCriterion()).ToList(), }; public static Homework ToHomework(this CreateHomeworkViewModel homework) diff --git a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs index b2db42563..1c65c1a2c 100644 --- a/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs +++ b/HwProj.CoursesService/HwProj.CoursesService.API/Models/Criterion.cs @@ -18,6 +18,6 @@ public class Criterion : IEntity public CriterionType Type { get; set; } public string Name { get; set; } public int MaxPoints { get; set; } - public string Arguments { get; set; } + public string? Arguments { get; set; } } } diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index fe98acb58..786e5d997 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -14,7 +14,8 @@ import { Checkbox, FormControlLabel, Menu, - MenuItem + MenuItem, + Tooltip } from "@mui/material"; import {MarkdownEditor, MarkdownPreview} from "components/Common/MarkdownEditor"; import {FC, useEffect, useState, useMemo} from "react" @@ -32,6 +33,7 @@ import CloseIcon from "@mui/icons-material/Close"; import Collapse from "@mui/material/Collapse"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import TaskCriteria from "./TaskCriteria"; import {BonusTag} from "@/components/Common/HomeworkTags"; import Utils from "../../services/Utils"; @@ -451,18 +453,46 @@ const CourseTaskEditor: FC<{ Критерий дедлайна + + + - updateCriterion(index, {name: e.target.value.slice(0, 50)})} - /> + + updateCriterion(index, {name: e.target.value.slice(0, 50)})} + /> + + updateCriterion(index, { + arguments: e.target.value + ? new Date(e.target.value).toISOString() + : undefined, + }) + } + InputLabelProps={{shrink: true}} + /> + + На основе дедлайна + + - - - updateCriterion(index, { - arguments: e.target.value - ? new Date(e.target.value).toISOString() - : undefined, - }) - } - InputLabelProps={{shrink: true}} - /> - - - - На основе дедлайна - - - - - - Как сработает правило - - - После {c.arguments ? Utils.renderDateWithoutSeconds(new Date(c.arguments)) : "дедлайна"} будет списан {c.maxPoints || 1} балл - - - ) : ( From 5517db182951a3c5cd940787f8aba41ca2fc6463 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 12:32:33 +0300 Subject: [PATCH 05/20] Update deadline criterion UI --- .../Tasks/CourseTaskExperimental.tsx | 206 ++++++++++-------- 1 file changed, 113 insertions(+), 93 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 786e5d997..954784957 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -138,9 +138,7 @@ const CourseTaskEditor: FC<{ onClose={() => setAddCriterionAnchor(null)} > Обычный критерий - - Автокритерий: дедлайн - + Дедлайн ); @@ -429,102 +427,124 @@ const CourseTaskEditor: FC<{ {criteria.map((c, index) => isDeadlineCriterion(c) ? ( - - - - - - - Критерий дедлайна - - - - - - - - - updateCriterion(index, {name: e.target.value.slice(0, 50)})} - /> - - updateCriterion(index, { - arguments: e.target.value - ? new Date(e.target.value).toISOString() - : undefined, - }) - } - InputLabelProps={{shrink: true}} - /> - - На основе дедлайна - - - - - - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - onBlur={(e) => - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - /> - - - removeCriterion(index)} - color="error" - size="small" + + + - - - + авто + + + + + updateCriterion(index, {name: e.target.value.slice(0, 50)})} + /> - + + + + + + updateCriterion(index, { + arguments: e.target.value + ? new Date(e.target.value).toISOString() + : undefined, + }) + } + InputLabelProps={{shrink: true}} + /> + + + + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + onBlur={(e) => + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + /> + + + + + + + + removeCriterion(index)} + color="error" + size="small" + > + + + + ) : ( Date: Sun, 3 May 2026 12:38:43 +0300 Subject: [PATCH 06/20] Revert "Update deadline criterion UI" This reverts commit 5517db182951a3c5cd940787f8aba41ca2fc6463. --- .../Tasks/CourseTaskExperimental.tsx | 206 ++++++++---------- 1 file changed, 93 insertions(+), 113 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 954784957..786e5d997 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -138,7 +138,9 @@ const CourseTaskEditor: FC<{ onClose={() => setAddCriterionAnchor(null)} > Обычный критерий - Дедлайн + + Автокритерий: дедлайн + ); @@ -427,124 +429,102 @@ const CourseTaskEditor: FC<{ {criteria.map((c, index) => isDeadlineCriterion(c) ? ( - - - - + + + + + Критерий дедлайна + + + + + + + + + updateCriterion(index, {name: e.target.value.slice(0, 50)})} + /> + + updateCriterion(index, { + arguments: e.target.value + ? new Date(e.target.value).toISOString() + : undefined, + }) + } + InputLabelProps={{shrink: true}} + /> + + На основе дедлайна + + + + + + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + onBlur={(e) => + updateCriterion(index, { + maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), + }) + } + /> + + + removeCriterion(index)} + color="error" + size="small" > - авто - - - - - updateCriterion(index, {name: e.target.value.slice(0, 50)})} - /> + + + - - - - - - updateCriterion(index, { - arguments: e.target.value - ? new Date(e.target.value).toISOString() - : undefined, - }) - } - InputLabelProps={{shrink: true}} - /> - - - - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - onBlur={(e) => - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - /> - - - - - - - - removeCriterion(index)} - color="error" - size="small" - > - - - - + ) : ( Date: Sun, 3 May 2026 12:44:41 +0300 Subject: [PATCH 07/20] Update deadline criterion menu item --- .../Tasks/CourseTaskExperimental.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 786e5d997..8b6d0c703 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -137,9 +137,32 @@ const CourseTaskEditor: FC<{ open={Boolean(addCriterionAnchor)} onClose={() => setAddCriterionAnchor(null)} > - Обычный критерий - - Автокритерий: дедлайн + + Обычный критерий + + + + Дедлайн + + авто + + ); From 693d1d9d6781b37a892d58830ffea413e6e65ac3 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 12:56:57 +0300 Subject: [PATCH 08/20] Refine deadline criterion date field --- .../Tasks/CourseTaskExperimental.tsx | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 8b6d0c703..89d6e0381 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -298,6 +298,21 @@ const CourseTaskEditor: FC<{ const maxRatingLabel = criteria.length > 0 ? "Критерии" : props.speculativeTask.suggestedMaxRating === maxRating ? "Вычислено" : undefined + const getEffectiveDeadlineDate = () => { + const hasEffectiveDeadline = metadata?.hasDeadline ?? homework.hasDeadline; + const deadlineDate = metadata?.deadlineDate || homework.deadlineDate; + + return hasEffectiveDeadline && deadlineDate ? new Date(deadlineDate) : undefined; + } + + const isBasedOnEffectiveDeadline = (criterion: CriterionViewModel) => { + const effectiveDeadlineDate = getEffectiveDeadlineDate(); + + return !!criterion.arguments + && !!effectiveDeadlineDate + && new Date(criterion.arguments).getTime() === effectiveDeadlineDate.getTime(); + } + return ( updateCriterion(index, { @@ -512,9 +529,11 @@ const CourseTaskEditor: FC<{ } InputLabelProps={{shrink: true}} /> - - На основе дедлайна - + {isBasedOnEffectiveDeadline(c) && ( + + На основе дедлайна + + )} From 65be59b651a9525f90aa1ed12732c6c9f58be04b Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 13:08:05 +0300 Subject: [PATCH 09/20] Refine deadline criteria display --- .../src/components/Tasks/TaskCriteria.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index c64a34d8b..d11f67020 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -18,19 +18,13 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { {task.criteria.map(c => ( - {c.type === CriterionTypeDeadline && ( - - )} - {c.name} + + {c.name} + {c.type === CriterionTypeDeadline && ( + авто + )} + {c.type === CriterionTypeDeadline && c.arguments && ( До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} · Штраф @@ -41,8 +35,13 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { ))} From ea7b000a2d0c08e48e16c75c9f7e9424d8427eb5 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 13:13:47 +0300 Subject: [PATCH 10/20] Separate deadline penalty label --- .../src/components/Tasks/TaskCriteria.tsx | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index d11f67020..ec6d18c4a 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -27,22 +27,43 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { {c.type === CriterionTypeDeadline && c.arguments && ( - До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} · Штраф + До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} )} - + {c.type === CriterionTypeDeadline ? ( + + + Штраф + + + + ) : ( + + )} ))} From 9fefe348199cf6756a66b572897b03c31eaf53dc Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 15:13:41 +0300 Subject: [PATCH 11/20] Refine deadline criteria rating --- .../Solutions/TaskSolutionComponent.tsx | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index a4b5f4201..f1709d3c9 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -214,22 +214,24 @@ const TaskSolutionComponent: FC = (props) => { useEffect(() => { if (!hasCriteria || !state.addBonusPoints || !state.clickedForRate || !criteriaModified) return; - const criteriaTotal = criterionRatings.reduce( + const criteriaTotalRaw = criterionRatings.reduce( (sum, c) => sum + (Number.isFinite(c.value) ? Number(c.value) : 0), 0 ); + const criteriaTotal = Math.max(0, criteriaTotalRaw); const total = criteriaTotal + (Number.isFinite(extraScore) ? extraScore : 0); setState(prev => ({...prev, points: total})); }, [criterionRatings, extraScore, hasCriteria, state.addBonusPoints, state.clickedForRate, criteriaModified]); - const criteriaSum = + const criteriaTotalRaw = criterionRatings.reduce( (sum, c) => sum + (Number.isFinite(c.value) ? Number(c.value) : 0), 0 - ) + (Number.isFinite(extraScore) ? extraScore : 0); + ); + const criteriaSum = Math.max(0, criteriaTotalRaw) + (Number.isFinite(extraScore) ? extraScore : 0); - const isRateButtonDisabled = hasCriteria && criteriaSum < 0; + const isRateButtonDisabled = false; const [isCtrlPressed, setIsCtrlPressed] = useState(false) @@ -599,12 +601,12 @@ const TaskSolutionComponent: FC = (props) => { const renderCriteriaBlock = () => { if (!hasCriteria) return null; - const criteriaTotal = criterionRatings.reduce( + const criteriaTotalRaw = criterionRatings.reduce( (sum, c) => sum + (Number.isFinite(c.value) ? Number(c.value) : 0), 0 ); + const criteriaTotal = Math.max(0, criteriaTotalRaw); const totalWithExtra = criteriaTotal + (Number.isFinite(extraScore) ? extraScore : 0); - const isCriteriaSumNegative = totalWithExtra < 0; return ( @@ -638,7 +640,59 @@ const TaskSolutionComponent: FC = (props) => { const isFilled = hasExplicitValue && (isThumbCriterion || numericValue !== 0); const isDeadlineCriterion = c.type === CriterionTypeDeadline; const deadlineDate = c.arguments ? new Date(c.arguments) : undefined; - const deadlinePassed = isDeadlineCriterion && numericValue === 0; + const isSubmittedOnTime = isDeadlineCriterion && numericValue === 0; + const deadlineDelay = isDeadlineCriterion && deadlineDate && solution?.publicationDate + ? getDatesDiff(solution.publicationDate!, deadlineDate) + : ""; + + if (isDeadlineCriterion) { + return ( + + : } + sx={{ + gridColumn: "1 / span 2", + alignItems: "center", + py: 0.5, + "& .MuiAlert-message": { + width: "100%", + }, + "& .MuiAlert-icon": { + alignItems: "center", + }, + }} + > + + + {c.name} + + {isSubmittedOnTime + ? "Сдано вовремя" + : `Просрочено${deadlineDelay ? ` на ${deadlineDelay}` : ""}`} + + + + + + ); + } return ( @@ -671,13 +725,7 @@ const TaskSolutionComponent: FC = (props) => { - {isDeadlineCriterion ? ( - - ) : isThumbCriterion ? ( + {isThumbCriterion ? ( = (props) => { variant="standard" margin="dense" placeholder="0" + inputProps={{min: 0}} onChange={e => { let val = Number(e.target.value || 0); if (Number.isNaN(val)) val = 0; + val = Math.max(0, val); setCriteriaModified(true); setExtraScore(val); @@ -841,7 +891,7 @@ const TaskSolutionComponent: FC = (props) => { marginBottom: 8, fontSize: "0.95rem", fontWeight: 500, - color: isCriteriaSumNegative ? "#d32f2f" : undefined, + color: totalWithExtra === 0 ? "#d32f2f" : undefined, }} > {`Сумма по критериям: ${totalWithExtra} из ${maxRating}`} From b5347fb1003b657828bcb0655e3c900ca77bf98b Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 15:35:10 +0300 Subject: [PATCH 12/20] Refine deadline criteria comment table --- .../Solutions/TaskSolutionComponent.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index f1709d3c9..0e3aae289 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -332,11 +332,20 @@ const TaskSolutionComponent: FC = (props) => { const rows: string[] = criterionRatings.map(cr => { const criterion = taskWithCriteria.criteria?.find(c => c.id === cr.criterionId); const safeValue = Number.isFinite(cr.value) ? cr.value : 0; - const deadlineText = criterion?.type === CriterionTypeDeadline && criterion.arguments - ? `, дедлайн ${Utils.renderDateWithoutSeconds(new Date(criterion.arguments))}` - : ""; - return `| ${cr.name}${deadlineText} | ${safeValue} / ${cr.maxPoints} |`; + if (criterion?.type === CriterionTypeDeadline) { + const deadlineDelay = criterion.arguments && solution?.publicationDate + ? getDatesDiff(solution.publicationDate!, new Date(criterion.arguments)) + : ""; + const statusText = safeValue === 0 + ? "Сдано вовремя" + : `Просрочено${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; + const valueText = safeValue === 0 ? "✅" : `❌ ${safeValue}`; + + return `| ${cr.name}
${statusText} | ${valueText} |`; + } + + return `| ${cr.name} | ${safeValue} / ${cr.maxPoints} |`; }); if ((extraScore ?? 0) !== 0) { From d3f4ba85843594b52e1b8a81841975955d39033c Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Sun, 3 May 2026 15:40:22 +0300 Subject: [PATCH 13/20] Space deadline status in comment table --- hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index 0e3aae289..a9306c152 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -342,7 +342,7 @@ const TaskSolutionComponent: FC = (props) => { : `Просрочено${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; const valueText = safeValue === 0 ? "✅" : `❌ ${safeValue}`; - return `| ${cr.name}
${statusText} | ${valueText} |`; + return `| ${cr.name}
${statusText} | ${valueText} |`; } return `| ${cr.name} | ${safeValue} / ${cr.maxPoints} |`; From 291bdc61b1c42f0dadad88231d00e48d1366b9d1 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 4 May 2026 09:14:47 +0300 Subject: [PATCH 14/20] Update auto criterion badge styling --- .../Tasks/CourseTaskExperimental.tsx | 22 +++++------------ .../src/components/Tasks/TaskCriteria.tsx | 24 +++++++++++++++---- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 89d6e0381..68922c6cf 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -143,25 +143,15 @@ const CourseTaskEditor: FC<{ Дедлайн - - авто - + /> diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index ec6d18c4a..207a07f9a 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -5,6 +5,16 @@ import Utils from "../../services/Utils"; const CriterionTypeDeadline = 1; +const autoCriterionChipSx = { + height: 22, + backgroundColor: "#E8F8EE", + color: "#159947", + fontWeight: 600, + "& .MuiChip-label": { + px: 1, + }, +}; + const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { return task.criteria && task.criteria.length > 0 ? ( <> @@ -19,12 +29,18 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { - - {c.name} + + + {c.name} + {c.type === CriterionTypeDeadline && ( - авто + )} - + {c.type === CriterionTypeDeadline && c.arguments && ( До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} From 230d0a7be32430ab1ef644eb2639341129302fb7 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 4 May 2026 09:22:14 +0300 Subject: [PATCH 15/20] Refine deadline criterion penalty input --- .../Tasks/CourseTaskExperimental.tsx | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 68922c6cf..33c65cde4 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -478,7 +478,7 @@ const CourseTaskEditor: FC<{ fontWeight: 600, }} /> - + Критерий дедлайна
- - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - onBlur={(e) => - updateCriterion(index, { - maxPoints: Math.max(Math.abs(+e.target.value || 1), 1), - }) - } - /> + + + { + if (e.key === "-") e.preventDefault(); + }} + onChange={(e) => + updateCriterion(index, { + maxPoints: Math.max(+e.target.value, 1), + }) + } + onBlur={(e) => + updateCriterion(index, { + maxPoints: Math.max(+e.target.value, 1), + }) + } + /> + Date: Mon, 4 May 2026 09:28:46 +0300 Subject: [PATCH 16/20] Remove auto badge from criteria list --- .../src/components/Tasks/TaskCriteria.tsx | 25 +++---------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/hwproj.front/src/components/Tasks/TaskCriteria.tsx b/hwproj.front/src/components/Tasks/TaskCriteria.tsx index 207a07f9a..a7e311385 100644 --- a/hwproj.front/src/components/Tasks/TaskCriteria.tsx +++ b/hwproj.front/src/components/Tasks/TaskCriteria.tsx @@ -5,16 +5,6 @@ import Utils from "../../services/Utils"; const CriterionTypeDeadline = 1; -const autoCriterionChipSx = { - height: 22, - backgroundColor: "#E8F8EE", - color: "#159947", - fontWeight: 600, - "& .MuiChip-label": { - px: 1, - }, -}; - const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { return task.criteria && task.criteria.length > 0 ? ( <> @@ -29,18 +19,9 @@ const TaskCriteria: FC<{ task: HomeworkTaskViewModel }> = ({task}) => { - - - {c.name} - - {c.type === CriterionTypeDeadline && ( - - )} - + + {c.name} + {c.type === CriterionTypeDeadline && c.arguments && ( До {Utils.renderDateWithoutSeconds(new Date(c.arguments))} From aea83f6eccb975b8362258f641461540cbc12ee0 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 4 May 2026 09:33:23 +0300 Subject: [PATCH 17/20] Refine solution criteria deadline display --- .../Solutions/TaskSolutionComponent.tsx | 76 ++++++++++--------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index a9306c152..0144a5026 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -339,7 +339,7 @@ const TaskSolutionComponent: FC = (props) => { : ""; const statusText = safeValue === 0 ? "Сдано вовремя" - : `Просрочено${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; + : `Сдано позже${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; const valueText = safeValue === 0 ? "✅" : `❌ ${safeValue}`; return `| ${cr.name}
${statusText} | ${valueText} |`; @@ -622,11 +622,12 @@ const TaskSolutionComponent: FC = (props) => { - {taskWithCriteria.criteria!.map((c) => { + {taskWithCriteria.criteria!.map((c, criteriaIndex) => { const existingRating = criterionRatings.find(r => r.criterionId === c.id); const current = @@ -648,6 +649,10 @@ const TaskSolutionComponent: FC = (props) => { const isFilled = hasExplicitValue && (isThumbCriterion || numericValue !== 0); const isDeadlineCriterion = c.type === CriterionTypeDeadline; + const nextCriterion = taskWithCriteria.criteria![criteriaIndex + 1]; + const hasRegularCriterionAfter = isDeadlineCriterion && + nextCriterion !== undefined && + nextCriterion.type !== CriterionTypeDeadline; const deadlineDate = c.arguments ? new Date(c.arguments) : undefined; const isSubmittedOnTime = isDeadlineCriterion && numericValue === 0; const deadlineDelay = isDeadlineCriterion && deadlineDate && solution?.publicationDate @@ -665,6 +670,7 @@ const TaskSolutionComponent: FC = (props) => { sx={{ gridColumn: "1 / span 2", alignItems: "center", + mb: hasRegularCriterionAfter ? 1.5 : 0, py: 0.5, "& .MuiAlert-message": { width: "100%", @@ -686,18 +692,16 @@ const TaskSolutionComponent: FC = (props) => { {isSubmittedOnTime ? "Сдано вовремя" - : `Просрочено${deadlineDelay ? ` на ${deadlineDelay}` : ""}`} + : `Сдано позже${deadlineDelay ? ` на ${deadlineDelay}` : ""}`} - + {!isSubmittedOnTime && ( + + )}
); @@ -733,7 +737,7 @@ const TaskSolutionComponent: FC = (props) => { - + {isThumbCriterion ? ( = (props) => { }} /> ) : ( - + = (props) => { Доп. оценка (опционально) - { - let val = Number(e.target.value || 0); - if (Number.isNaN(val)) val = 0; - val = Math.max(0, val); - - setCriteriaModified(true); - setExtraScore(val); - }} - /> + + { + let val = Number(e.target.value || 0); + if (Number.isNaN(val)) val = 0; + val = Math.max(0, val); + + setCriteriaModified(true); + setExtraScore(val); + }} + /> +
@@ -1200,7 +1206,7 @@ const TaskSolutionComponent: FC = (props) => { { sentAfterDeadline && - Решение сдано на {sentAfterDeadline} позже дедлайна. + Решение сдано позже дедлайна на {sentAfterDeadline}. } From 2752768fb19702070c7e0111a572df8c01d29fcd Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 4 May 2026 09:38:46 +0300 Subject: [PATCH 18/20] Improve deadline criteria comment formatting --- .../src/components/Solutions/TaskSolutionComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index 0144a5026..4dddf5c44 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -340,9 +340,9 @@ const TaskSolutionComponent: FC = (props) => { const statusText = safeValue === 0 ? "Сдано вовремя" : `Сдано позже${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; - const valueText = safeValue === 0 ? "✅" : `❌ ${safeValue}`; + const valueText = safeValue === 0 ? "✅" : `${safeValue}`; - return `| ${cr.name}
${statusText} | ${valueText} |`; + return `|
${cr.name}
${statusText}
| ${valueText} |`; } return `| ${cr.name} | ${safeValue} / ${cr.maxPoints} |`; From a088b5e00314658676eae62f7d809ca810b02448 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Tue, 5 May 2026 10:09:21 +0300 Subject: [PATCH 19/20] Fix deadline criterion rating comment --- hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index 4dddf5c44..6fb326635 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -342,7 +342,7 @@ const TaskSolutionComponent: FC = (props) => { : `Сдано позже${deadlineDelay ? ` на ${deadlineDelay}` : ""}`; const valueText = safeValue === 0 ? "✅" : `${safeValue}`; - return `|
${cr.name}
${statusText}
| ${valueText} |`; + return `| ${cr.name} (${statusText.toLowerCase()}) | ${valueText} |`; } return `| ${cr.name} | ${safeValue} / ${cr.maxPoints} |`; From c50d7677db9ee5e5ed23a04c142fa27a4a69097b Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Tue, 5 May 2026 10:23:29 +0300 Subject: [PATCH 20/20] Allow task points editing for auto criteria --- .../components/Tasks/CourseTaskExperimental.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx index 33c65cde4..060190c14 100644 --- a/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx +++ b/hwproj.front/src/components/Tasks/CourseTaskExperimental.tsx @@ -126,7 +126,7 @@ const CourseTaskEditor: FC<{ [criteria] ) - const autoMaxFromCriteria = criteria.length > 0; + const hasRegularCriteria = criteria.some(c => !isDeadlineCriterion(c)); const criteriaHasErrors = criteria.some(c => !c.name || (c.maxPoints ?? 0) <= 0 || (isDeadlineCriterion(c) && !c.arguments) ); @@ -158,8 +158,8 @@ const CourseTaskEditor: FC<{ ); useEffect(() => { - if (autoMaxFromCriteria) setMaxRating(criteriaTotalPoints); - }, [criteriaTotalPoints, autoMaxFromCriteria]); + if (hasRegularCriteria) setMaxRating(criteriaTotalPoints); + }, [criteriaTotalPoints, hasRegularCriteria]); const isNewTask = taskData.task.id! < 0 @@ -206,7 +206,7 @@ const CourseTaskEditor: FC<{ //TODO: suggested max rating const [title, setTitle] = useState(task.title!) const [maxRating, setMaxRating] = useState( - criteria.length > 0 ? criteriaTotalPoints : task.maxRating! + hasRegularCriteria ? criteriaTotalPoints : task.maxRating! ) const [description, setDescription] = useState(task.description || "") const [isBonusExplicit, setIsBonusExplicit] = useState(props.speculativeTask.tags!.includes(BonusTag) && !props.speculativeHomework.tags!.includes(BonusTag)) @@ -286,7 +286,7 @@ const CourseTaskEditor: FC<{ const homeworkPublicationDateIsSet = !homework.publicationDateNotSet const maxRatingLabel = - criteria.length > 0 ? "Критерии" : props.speculativeTask.suggestedMaxRating === maxRating ? "Вычислено" : undefined + hasRegularCriteria ? "Критерии" : props.speculativeTask.suggestedMaxRating === maxRating ? "Вычислено" : undefined const getEffectiveDeadlineDate = () => { const hasEffectiveDeadline = metadata?.hasDeadline ?? homework.hasDeadline; @@ -352,9 +352,9 @@ const CourseTaskEditor: FC<{ margin="normal" type="number" value={maxRating} - InputProps={{readOnly: autoMaxFromCriteria}} + InputProps={{readOnly: hasRegularCriteria}} onChange={(e) => { - if (!autoMaxFromCriteria) { + if (!hasRegularCriteria) { e.persist(); setMaxRating(+e.target.value); }