From 7029cdc6076c906ff52b7400a9e724272f219b14 Mon Sep 17 00:00:00 2001 From: Alexey Zarubin Date: Mon, 18 May 2026 00:36:23 +0300 Subject: [PATCH] Keep criteria values when editing rating --- .../Solutions/TaskSolutionComponent.tsx | 106 ++++++++++++++---- 1 file changed, 82 insertions(+), 24 deletions(-) diff --git a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx index 725f0c379..6724a0d7f 100644 --- a/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx +++ b/hwproj.front/src/components/Solutions/TaskSolutionComponent.tsx @@ -116,16 +116,83 @@ const TaskSolutionComponent: FC = (props) => { } }; + const parseNumber = (value: string): number | undefined => { + const match = value.replace(",", ".").match(/-?\d+(\.\d+)?/); + if (!match) return undefined; + + const parsed = Number(match[0]); + return Number.isFinite(parsed) ? parsed : undefined; + }; + + const parseStoredCriteria = (comment?: string) => { + const criteria = new Map(); + let extraScore: number | undefined; + if (!comment) return {criteria, extraScore, commentWithoutCriteria: ""}; + + const lines = comment.split("\n"); + const tableStart = lines.findIndex(line => + line.trim() === "| Критерий оценивания | Баллы |" + ); + if (tableStart === -1) { + return {criteria, extraScore, commentWithoutCriteria: comment}; + } + + let tableEnd = tableStart + 1; + for (; tableEnd < lines.length; tableEnd++) { + const line = lines[tableEnd].trim(); + if (!line.startsWith("|") || !line.endsWith("|")) break; + } + + lines.slice(tableStart + 2, tableEnd).forEach(line => { + const cells = line + .split("|") + .slice(1, -1) + .map(cell => cell.trim()); + if (cells.length < 2) return; + + const value = parseNumber(cells[1]); + if (value === undefined) return; + + if (cells[0] === "Доп. оценка") { + extraScore = value; + } else { + criteria.set(cells[0], value); + } + }); + + const commentWithoutCriteria = [ + ...lines.slice(0, tableStart), + ...lines.slice(tableEnd), + ].join("\n").trim(); + + return {criteria, extraScore, commentWithoutCriteria}; + }; + + const getInitialCriterionValue = ( + criterionId: number, + criterionName: string, + draft: CriteriaDraft | null, + storedCriteria: Map + ) => { + const draftValue = draft?.criteria + ?.find(x => x.criterionId === criterionId)?.value; + if (typeof draftValue === "number") return draftValue; + + const storedValue = storedCriteria.get(criterionName); + return storedValue ?? Number.NaN; + }; + const getDefaultState = (): ISolutionState => { const storageValue = RatingStorage.tryGet(storageKey); + const storedCriteria = parseStoredCriteria(props.solution?.lecturerComment); const clickedForRate = props.forMentor - ? (storageValue !== null) + ? (storageValue != null) : false; return { points: storageValue?.points || props.solution?.rating || 0, - lecturerComment: storageValue?.comment || props.solution?.lecturerComment || "", + lecturerComment: storageValue?.comment || storedCriteria.commentWithoutCriteria, clickedForRate, addBonusPoints: hasCriteria, }; @@ -137,6 +204,7 @@ const TaskSolutionComponent: FC = (props) => { const [state, setState] = useState(getDefaultState); const initialDraft = loadCriteriaDraft(); + const initialStoredCriteria = parseStoredCriteria(props.solution?.lecturerComment); const getDeadlineCriterionValue = (criterion: { arguments?: string; maxPoints?: number }) => { if (!props.solution?.publicationDate || !criterion.arguments) return Number.NaN; @@ -155,21 +223,21 @@ const TaskSolutionComponent: FC = (props) => { const deadlineValue = c.type === CriterionTypeDeadline ? getDeadlineCriterionValue(c) : Number.NaN; - const draftValue = initialDraft?.criteria - ?.find(x => x.criterionId === id)?.value; return { criterionId: id, name: c.name ?? "", maxPoints: c.maxPoints ?? 0, - value: Number.isFinite(deadlineValue) ? deadlineValue : (draftValue ?? NaN), + value: Number.isFinite(deadlineValue) + ? deadlineValue + : getInitialCriterionValue(id, c.name ?? "", initialDraft, initialStoredCriteria.criteria), comment: "", }; }) ); const [extraScore, setExtraScore] = useState( - initialDraft?.extraScore ?? 0 + initialDraft?.extraScore ?? initialStoredCriteria.extraScore ?? 0 ); const [criteriaModified, setCriteriaModified] = useState(false); const [showOriginalCommentText, setShowOriginalCommentText] = useState(false) @@ -183,6 +251,7 @@ const TaskSolutionComponent: FC = (props) => { setState(getDefaultState()); const draft = loadCriteriaDraft(); + const storedCriteria = parseStoredCriteria(props.solution?.lecturerComment); setCriterionRatings( (taskWithCriteria.criteria ?? []).map(c => { @@ -190,26 +259,26 @@ const TaskSolutionComponent: FC = (props) => { const deadlineValue = c.type === CriterionTypeDeadline ? getDeadlineCriterionValue(c) : Number.NaN; - const draftValue = draft?.criteria - ?.find(x => x.criterionId === id)?.value; return { criterionId: id, name: c.name ?? "", maxPoints: c.maxPoints ?? 0, - value: Number.isFinite(deadlineValue) ? deadlineValue : (draftValue ?? NaN), + value: Number.isFinite(deadlineValue) + ? deadlineValue + : getInitialCriterionValue(id, c.name ?? "", draft, storedCriteria.criteria), comment: "", }; }) ); - setExtraScore(draft?.extraScore ?? 0); + setExtraScore(draft?.extraScore ?? storedCriteria.extraScore ?? 0); setCriteriaModified(false); getAchievementState(); setRateInProgressState(false); getActuality(); setShowOriginalCommentText(false); - }, [props.student.userId, props.task.id, props.solution?.id, props.solution?.rating]); + }, [props.student.userId, props.task.id, props.solution?.id, props.solution?.rating, props.solution?.lecturerComment]); useEffect(() => { if (!hasCriteria || !state.addBonusPoints || !state.clickedForRate || !criteriaModified) return; @@ -1072,10 +1141,11 @@ const TaskSolutionComponent: FC = (props) => {