diff --git a/client/src/components/SplitButton.tsx b/client/src/components/SplitButton.tsx index 0b7e01172..5afbada4a 100644 --- a/client/src/components/SplitButton.tsx +++ b/client/src/components/SplitButton.tsx @@ -41,9 +41,17 @@ interface Props extends ButtonGroupProps { initiallySelected?: number; variant?: ButtonProps['variant']; color?: ButtonProps['color']; + onMenuItemClick?: (index: number) => void; } -function SplitButton({ options, initiallySelected, variant, color, ...props }: Props): JSX.Element { +function SplitButton({ + options, + initiallySelected, + variant, + color, + onMenuItemClick, + ...props +}: Props): JSX.Element { const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); const logger = useLogger('SplitButton'); @@ -69,6 +77,10 @@ function SplitButton({ options, initiallySelected, variant, color, ...props }: P ) => { setSelectedIndex(index); setOpen(false); + + if (onMenuItemClick) { + onMenuItemClick(index); + } }; const handleToggle = () => { diff --git a/client/src/model/Grading.ts b/client/src/model/Grading.ts index cc4be7ef0..58125b5f7 100644 --- a/client/src/model/Grading.ts +++ b/client/src/model/Grading.ts @@ -1,5 +1,5 @@ import { Transform, Type } from 'class-transformer'; -import { IExerciseGrading, IGrading } from 'shared/model/Gradings'; +import { IExerciseGrading, IGrading, SheetState } from 'shared/model/Gradings'; import { Modify } from '../typings/Modify'; import { Exercise, Subexercise } from './Exercise'; @@ -60,6 +60,7 @@ export class Grading implements Modify { readonly belongsToTeam!: boolean; readonly comment?: string; readonly additionalPoints?: number; + readonly sheetState?: SheetState; @Type(() => ExerciseGrading) @Transform(({ value }) => new Map(value)) diff --git a/client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts b/client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts index 91a731de2..89da4c46a 100644 --- a/client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts +++ b/client/src/pages/points-sheet/enter-form/EnterPoints.helpers.ts @@ -89,5 +89,6 @@ export function convertFormStateToGradingDTO({ createNewGrading: !prevGrading, comment: values.comment, additionalPoints, + sheetState: values.sheetState, }; } diff --git a/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts index 2fa36be1b..06aeb1f4a 100644 --- a/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts +++ b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.helpers.ts @@ -1,3 +1,4 @@ +import { SheetState } from 'shared/model/Gradings'; import { Exercise, HasExercises } from '../../../../model/Exercise'; import { ExerciseGrading, Grading } from '../../../../model/Grading'; import { FormikSubmitCallback } from '../../../../types'; @@ -17,6 +18,7 @@ export interface PointsFormState { exercises: { [exerciseId: string]: PointsFormExerciseState; }; + sheetState?: SheetState; } export type PointsFormSubmitCallback = FormikSubmitCallback; @@ -76,5 +78,6 @@ export function generateInitialValues({ sheet, grading }: InitialValuesOptions): comment: grading?.comment ?? '', additionalPoints: grading?.additionalPoints?.toString() ?? '0', exercises, + sheetState: grading?.sheetState ? (grading.sheetState ?? SheetState.NO_STATE) : undefined, }; } diff --git a/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx index d1e7c4fb7..8030676c1 100644 --- a/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx +++ b/client/src/pages/points-sheet/enter-form/components/EnterPointsForm.tsx @@ -5,9 +5,14 @@ import makeStyles from '@mui/styles/makeStyles'; import clsx from 'clsx'; import { Formik, useFormikContext } from 'formik'; import React, { useEffect, useState } from 'react'; -import { convertExercisePointInfoToString, getPointsOfAllExercises } from 'shared/model/Gradings'; +import { + convertExercisePointInfoToString, + getPointsOfAllExercises, + SheetState, +} from 'shared/model/Gradings'; import FormikDebugDisplay from '../../../../components/forms/components/FormikDebugDisplay'; import SubmitButton from '../../../../components/loading/SubmitButton'; +import SplitButton from '../../../../components/SplitButton'; import { useDialog } from '../../../../hooks/dialog-service/DialogService'; import { useKeyboardShortcut } from '../../../../hooks/useKeyboardShortcut'; import { Exercise } from '../../../../model/Exercise'; @@ -30,6 +35,7 @@ const useStyles = makeStyles((theme: Theme) => }, textBox: { display: 'flex', + alignItems: 'center', marginBottom: theme.spacing(1), }, unsavedChangesText: { @@ -37,6 +43,7 @@ const useStyles = makeStyles((theme: Theme) => }, pointsText: { marginLeft: 'auto', + marginRight: theme.spacing(2), }, exerciseBox: { flex: 1, @@ -44,7 +51,6 @@ const useStyles = makeStyles((theme: Theme) => buttonRow: { display: 'flex', justifyContent: 'flex-end', - // This prevents a flashing scrollbar if the form spinner is shown. marginBottom: theme.spacing(0.5), }, cancelButton: { @@ -74,8 +80,7 @@ function EnterPointsForm({ grading, setIsAutoSubmitting, ...props }: Props): JSX ); useEffect(() => { - const values = generateInitialValues({ grading, sheet }); - setInitialValues(values); + setInitialValues(generateInitialValues({ grading, sheet })); }, [grading, sheet]); return ( @@ -94,9 +99,10 @@ function EnterPointsFormInner({ }: FormProps): JSX.Element { const classes = useStyles(); const dialog = useDialog(); - const formikContext = useFormikContext(); - const { values, handleSubmit, resetForm, isSubmitting, dirty, submitForm } = formikContext; + + const { values, handleSubmit, resetForm, isSubmitting, dirty, submitForm, setFieldValue } = + formikContext; const achieved = getAchievedPointsFromState(values); const total = getPointsOfAllExercises(sheet); @@ -142,18 +148,35 @@ function EnterPointsFormInner({ useKeyboardShortcut([{ key: 's', modifiers: { ctrlKey: true } }], (e) => { e.preventDefault(); + if (dirty) submitForm(); + }); - if (!dirty) { - return; + const handleOnMenuItemClick = (index: number) => { + if (index === 0) { + setFieldValue('sheetState', SheetState.PASSED); + } else { + setFieldValue('sheetState', SheetState.NOT_PASSED); } + }; - submitForm(); - }); + const toggleSheetState = (newState: SheetState) => { + const newSheetState = values.sheetState === newState ? SheetState.NO_STATE : newState; + setFieldValue('sheetState', newSheetState); + }; + + const buttonColor = + values.sheetState === SheetState.PASSED + ? 'success' + : values.sheetState === SheetState.NOT_PASSED + ? 'error' + : 'inherit'; - // unstable_usePrompt({ - // message: 'Es gibt ungespeicherte Änderungen. Soll die Seite wirklich verlassen werden?', - // when: dirty, - // }); + const initiallySelected = + values.sheetState === SheetState.PASSED + ? 0 + : values.sheetState === SheetState.NOT_PASSED + ? 1 + : 0; return ( <> @@ -161,12 +184,37 @@ function EnterPointsFormInner({
{dirty && ( - {<>Es gibt ungespeicherte Änderungen.} + Es gibt ungespeicherte Änderungen. )} - {`Gesamt: ${achieved} / ${totalPoints} Punkte`} + + {`Gesamt: ${achieved} / ${totalPoints} Punkte`} + + + { + toggleSheetState(SheetState.PASSED); + }, + }, + }, + { + label: 'Als nicht bestanden markieren', + ButtonProps: { + onClick: () => { + toggleSheetState(SheetState.NOT_PASSED); + }, + }, + }, + ]} + />
diff --git a/server/src/database/entities/grading.entity.ts b/server/src/database/entities/grading.entity.ts index c31880a09..c9efbbcdb 100644 --- a/server/src/database/entities/grading.entity.ts +++ b/server/src/database/entities/grading.entity.ts @@ -8,7 +8,8 @@ import { PrimaryKey, Property, } from '@mikro-orm/core'; -import { IExerciseGrading, IGrading } from 'shared/model/Gradings'; +import { EncryptedEnumType } from '../types/encryption/EncryptedEnumType'; +import { IExerciseGrading, IGrading, SheetState } from 'shared/model/Gradings'; import { v4 } from 'uuid'; import { ExerciseGradingDTO, GradingDTO } from '../../module/student/student.dto'; import { EncryptedMapType } from '../types/encryption/EncryptedMapType'; @@ -107,6 +108,9 @@ export class Grading { @ManyToOne(() => ShortTest, { deleteRule: 'cascade' }) private shortTest?: ShortTest; + @Property({ type: EncryptedEnumType }) + sheetState?: SheetState; + @Property({ nullable: false }) handInId?: string; @@ -172,6 +176,10 @@ export class Grading { this.additionalPoints = dto.additionalPoints ?? 0; this.exerciseGradings = []; + if (dto.sheetState) { + this.sheetState = dto.sheetState; + } + for (const [exerciseId, exerciseGradingDTO] of dto.exerciseGradings) { this.exerciseGradings.push(ExerciseGrading.fromDTO(exerciseId, exerciseGradingDTO)); } @@ -202,6 +210,7 @@ export class Grading { comment: this.comment, belongsToTeam: this.belongsToTeam, exerciseGradings: [...exerciseGradings], + sheetState: this.sheetState, }; } diff --git a/server/src/module/markdown/markdown.service.ts b/server/src/module/markdown/markdown.service.ts index 3f8e98f79..8de2b9bb2 100644 --- a/server/src/module/markdown/markdown.service.ts +++ b/server/src/module/markdown/markdown.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import hljs from 'highlight.js'; import MarkdownIt from 'markdown-it'; -import { convertExercisePointInfoToString } from 'shared/model/Gradings'; +import { convertExercisePointInfoToString, SheetState } from 'shared/model/Gradings'; import { IStudentMarkdownData, ITeamMarkdownData } from 'shared/model/Markdown'; import { getNameOfEntity } from 'shared/util/helpers'; import { HasExercises } from '../../database/entities/ratedEntity.entity'; @@ -9,6 +9,7 @@ import { Team } from '../../database/entities/team.entity'; import { ScheinexamService } from '../scheinexam/scheinexam.service'; import { SheetService } from '../sheet/sheet.service'; import { ShortTestService } from '../short-test/short-test.service'; +import { GradingService } from '../student/grading.service'; import { StudentService } from '../student/student.service'; import { TeamService } from '../team/team.service'; import { @@ -23,7 +24,6 @@ import { TeamGradings, TeamMarkdownData, } from './markdown.types'; -import { GradingService } from '../student/grading.service'; @Injectable() export class MarkdownService { @@ -278,7 +278,7 @@ export class MarkdownService { }); const totalPointInfo = convertExercisePointInfoToString(pointInfo.total); - const header = `# ${nameOfEntity}\n\n**Gesamt: ${pointInfo.achieved} / ${totalPointInfo}**`; + const header = `# ${nameOfEntity}${grading.sheetState && grading.sheetState !== SheetState.NO_STATE ? `: ${grading.sheetState}` : ''}\n\n**Gesamt: ${pointInfo.achieved} / ${totalPointInfo}**`; return `${header}\n\n${exerciseMarkdown}`; } diff --git a/server/src/module/student/student.dto.ts b/server/src/module/student/student.dto.ts index e57401b02..3a7e52d65 100644 --- a/server/src/module/student/student.dto.ts +++ b/server/src/module/student/student.dto.ts @@ -14,7 +14,12 @@ import { ValidateNested, } from 'class-validator'; import { AttendanceState, IAttendanceDTO } from 'shared/model/Attendance'; -import { IExerciseGradingDTO, IGradingDTO, IPresentationPointsDTO } from 'shared/model/Gradings'; +import { + IExerciseGradingDTO, + IGradingDTO, + IPresentationPointsDTO, + SheetState, +} from 'shared/model/Gradings'; import { ICakeCountDTO, ICreateStudentDTO, @@ -148,6 +153,10 @@ export class GradingDTO implements IGradingDTO { @IsOptional() @IsNumber() additionalPoints?: number; + + @IsOptional() + @IsEnum(SheetState) + sheetState?: SheetState; } export class PresentationPointsDTO implements IPresentationPointsDTO { diff --git a/server/src/shared/model/Gradings.ts b/server/src/shared/model/Gradings.ts index 0949af55f..d65c11bd0 100644 --- a/server/src/shared/model/Gradings.ts +++ b/server/src/shared/model/Gradings.ts @@ -1,5 +1,11 @@ import { IExercise, IHasExercises } from './HasExercises'; +export enum SheetState { + PASSED = 'Bestanden', + NOT_PASSED = 'Nicht bestanden', + NO_STATE = 'Nicht bewertet', +} + export interface GradingResponseData { studentId: string; gradingData: IGrading | undefined; @@ -19,6 +25,7 @@ export interface IGrading { points: number; comment?: string; additionalPoints?: number; + sheetState?: SheetState; } export interface IExerciseGradingDTO { @@ -36,6 +43,7 @@ export interface IGradingDTO { shortTestId?: string; comment?: string; additionalPoints?: number; + sheetState?: SheetState; } export interface IPresentationPointsDTO {