From 5aefc3e3f818f8945a48efad4e99a8be632b3761 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Thu, 23 Jan 2020 20:29:22 +0100 Subject: [PATCH 01/19] Add route for admin to view the information about a criteria. --- client/src/routes/Routing.routes.tsx | 13 +++++- .../criteria-info-view/CriteriaInfoView.tsx | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 client/src/view/criteria-info-view/CriteriaInfoView.tsx diff --git a/client/src/routes/Routing.routes.tsx b/client/src/routes/Routing.routes.tsx index 75ecdc70a..505005517 100644 --- a/client/src/routes/Routing.routes.tsx +++ b/client/src/routes/Routing.routes.tsx @@ -35,6 +35,7 @@ import UserManagement from '../view/usermanagement/UserManagement'; import ScheinexamPointsOverview from '../view/points-scheinexam/overview/ScheinexamPointsOverview'; import EnterScheinexamPoints from '../view/points-scheinexam/enter-form/EnterScheinexamPoints'; import StudentInfo from '../view/studentmanagement/student-info/StudentInfo'; +import CriteriaInfoView from '../view/criteria-info-view/CriteriaInfoView'; export enum RoutingPath { ROOT = '/', @@ -52,7 +53,8 @@ export enum RoutingPath { MANAGE_USERS = '/admin/usermanagement', MANAGE_TUTORIALS = '/admin/tutorialmanagement', MANAGE_TUTORIALS_SUBSTITUTES = '/admin/tutorialmanagement/:tutorialid/substitute', - MANAGE_SCHEIN_CRITERIAS = '/admin/scheincriteriamanagement', + MANAGE_SCHEIN_CRITERIAS = '/admin/scheincriterias', + SCHEIN_CRITERIAS_INFO = '/admin/scheincriterias/info/:id', MANAGE_ATTENDANCES = '/admin/attendances', MANAGE_SHEETS = '/admin/sheets', MANAGE_ALL_STUDENTS = '/admin/students', @@ -224,6 +226,15 @@ export const ROUTES: readonly RouteType[] = [ isInDrawer: true, isPrivate: true, }, + { + path: RoutingPath.SCHEIN_CRITERIAS_INFO, + title: 'Scheinkriterien', + component: CriteriaInfoView, + icon: ScriptTextIcon, + roles: [Role.ADMIN, Role.EMPLOYEE], + isInDrawer: false, + isPrivate: true, + }, { path: RoutingPath.MANAGE_SCHEIN_CRITERIAS, title: 'Scheinkriterien', diff --git a/client/src/view/criteria-info-view/CriteriaInfoView.tsx b/client/src/view/criteria-info-view/CriteriaInfoView.tsx new file mode 100644 index 000000000..61414d1c6 --- /dev/null +++ b/client/src/view/criteria-info-view/CriteriaInfoView.tsx @@ -0,0 +1,43 @@ +import { Box, Typography } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import React from 'react'; +import { getNameOfEntity } from 'shared/dist/util/helpers'; +import BackButton from '../../components/BackButton'; +import Placeholder from '../../components/Placeholder'; +import { RoutingPath } from '../../routes/Routing.routes'; +import ScheinStatusBox from '../studentmanagement/student-info/components/ScheinStatusBox'; + +const useStyles = makeStyles(theme => + createStyles({ + backButton: { + marginRight: theme.spacing(2), + alignSelf: 'center', + }, + }) +); + +function CriteriaInfoView(): JSX.Element { + const classes = useStyles(); + + return ( + + + + + {/* {student && getNameOfEntity(student)} + + */} + + + + + + + ); +} + +export default CriteriaInfoView; From 95f8f91633e227a673815c3a8f829d72e310a8ab Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:12:43 +0100 Subject: [PATCH 02/19] Fix missing additional items on StudentRow on StudentOverview. --- .../studentmanagement/student-overview/components/StudentRow.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx b/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx index 1f19eb99d..363d9dc5e 100644 --- a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx +++ b/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx @@ -103,6 +103,7 @@ function StudentRow({ onEdit(student)} onDeleteClicked={() => onDelete(student)} + additionalItems={additionalMenuItems} /> } > From 658bc8f093cce9e9c34ee1f544d39b2039f4d6ad Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:13:55 +0100 Subject: [PATCH 03/19] Change some codes in the services to speed things up. --- .../scheinexam-service/ScheinexamService.class.ts | 6 +++++- .../src/services/sheet-service/SheetService.class.ts | 12 ++++++++---- .../services/student-service/StudentService.class.ts | 10 +++++++--- .../tutorial-service/TutorialService.class.ts | 8 +++++++- 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/server/src/services/scheinexam-service/ScheinexamService.class.ts b/server/src/services/scheinexam-service/ScheinexamService.class.ts index e208c80a2..db5cba67c 100644 --- a/server/src/services/scheinexam-service/ScheinexamService.class.ts +++ b/server/src/services/scheinexam-service/ScheinexamService.class.ts @@ -11,7 +11,7 @@ import { PointId, PointMap, getPointsOfExercise } from 'shared/dist/model/Points class ScheinExamService { public async getAllScheinExams(): Promise { - const examDocuments: ScheinexamDocument[] = await ScheinexamModel.find(); + const examDocuments: ScheinexamDocument[] = await this.getAllScheinExamAsDocuments(); const exams: ScheinExam[] = []; for (const doc of examDocuments) { @@ -21,6 +21,10 @@ class ScheinExamService { return exams; } + public async getAllScheinExamAsDocuments(): Promise { + return ScheinexamModel.find(); + } + public async createScheinExam({ scheinExamNo, date, diff --git a/server/src/services/sheet-service/SheetService.class.ts b/server/src/services/sheet-service/SheetService.class.ts index 818c8ec2f..1af604c58 100644 --- a/server/src/services/sheet-service/SheetService.class.ts +++ b/server/src/services/sheet-service/SheetService.class.ts @@ -1,14 +1,15 @@ +import { getPointsOfExercise, PointId, PointMap } from 'shared/dist/model/Points'; import { Sheet, SheetDTO } from 'shared/dist/model/Sheet'; -import { Student } from 'shared/dist/model/Student'; +import { getIdOfDocumentRef } from '../../helpers/documentHelpers'; import { convertDocumentToExercise, ExerciseDocument, generateExerciseDocumentsFromDTOs, } from '../../model/documents/ExerciseDocument'; import SheetModel, { SheetDocument } from '../../model/documents/SheetDocument'; +import { StudentDocument } from '../../model/documents/StudentDocument'; import { DocumentNotFoundError } from '../../model/Errors'; import teamService from '../team-service/TeamService.class'; -import { PointId, PointMap, getPointsOfExercise } from 'shared/dist/model/Points'; class SheetService { public async getAllSheets(): Promise { @@ -72,12 +73,15 @@ class SheetService { return !!sheet; } - public async getPointsOfStudent(student: Student, sheet: Sheet): Promise { + public async getPointsOfStudent(student: StudentDocument, sheet: Sheet): Promise { const pointsOfStudent = new PointMap(student.points); let pointsOfTeam = new PointMap(); if (student.team) { - const team = await teamService.getTeamWithId(student.tutorial, student.team.id); + const team = await teamService.getTeamWithId( + getIdOfDocumentRef(student.tutorial), + getIdOfDocumentRef(student.team) + ); pointsOfTeam = new PointMap(team.points); } diff --git a/server/src/services/student-service/StudentService.class.ts b/server/src/services/student-service/StudentService.class.ts index eea9d0a37..df60d38ea 100644 --- a/server/src/services/student-service/StudentService.class.ts +++ b/server/src/services/student-service/StudentService.class.ts @@ -29,13 +29,13 @@ import tutorialService from '../tutorial-service/TutorialService.class'; class StudentService { public async getAllStudents(): Promise { const studentDocs: StudentDocument[] = await this.getAllStudentsAsDocuments(); - const students: Student[] = []; + const students: Promise[] = []; for (const doc of studentDocs) { - students.push(await this.getStudentOrReject(doc)); + students.push(this.getStudentOrReject(doc)); } - return students; + return Promise.all(students); } public async getAllStudentsAsDocuments(): Promise { @@ -52,6 +52,8 @@ class StudentService { points: {}, scheinExamResults: {}, cakeCount: 0, + attendance: new Types.Map(), + presentationPoints: new Types.Map(), }; const createdStudent = await StudentModel.create(studentData); @@ -90,6 +92,8 @@ class StudentService { points: student.points, scheinExamResults: student.scheinExamResults, cakeCount: student.cakeCount, + attendance: student.attendance, + presentationPoints: student.presentationPoints, }; // Encrypt the student manually due to the encryption library not supporting 'updateOne()'. diff --git a/server/src/services/tutorial-service/TutorialService.class.ts b/server/src/services/tutorial-service/TutorialService.class.ts index 550d737ea..1385f16b8 100644 --- a/server/src/services/tutorial-service/TutorialService.class.ts +++ b/server/src/services/tutorial-service/TutorialService.class.ts @@ -179,12 +179,18 @@ class TutorialService { return Promise.all(correctors.map(cor => userService.getUserOrReject(cor))); } - public async getStudentsOfTutorial(id: string): Promise { + public async getStudentsOfTutorialAsDocuments(id: string): Promise { const tutorial = await this.getDocumentWithID(id); await tutorial.populate('students').execPopulate(); const students: StudentDocument[] = tutorial.students as StudentDocument[]; + return students; + } + + public async getStudentsOfTutorial(id: string): Promise { + const students: StudentDocument[] = await this.getStudentsOfTutorialAsDocuments(id); + return Promise.all(students.map(stud => studentService.getStudentOrReject(stud))); } From b7c719cf4aeea7632809e90c0024dc79e8cebc71 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:16:19 +0100 Subject: [PATCH 04/19] Change CriteriaCharts to consist of smaller components. --- .../src/components/info-paper/ChartPaper.tsx | 70 +++++++++++++++++++ .../src/components/info-paper/InfoPaper.tsx | 37 ++++++++++ .../components/CriteriaCharts.tsx | 70 +++++-------------- 3 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 client/src/components/info-paper/ChartPaper.tsx create mode 100644 client/src/components/info-paper/InfoPaper.tsx diff --git a/client/src/components/info-paper/ChartPaper.tsx b/client/src/components/info-paper/ChartPaper.tsx new file mode 100644 index 000000000..3e2788bfa --- /dev/null +++ b/client/src/components/info-paper/ChartPaper.tsx @@ -0,0 +1,70 @@ +import { CircularProgress, PaperProps } from '@material-ui/core'; +import { createStyles, makeStyles, useTheme } from '@material-ui/core/styles'; +import React from 'react'; +import Chart from 'react-google-charts'; +import { ReactGoogleChartProps } from 'react-google-charts/dist/types'; +import InfoPaper from './InfoPaper'; + +const useStyles = makeStyles(theme => + createStyles({ + summaryPaper: { + flex: 1, + background: theme.palette.background.default, + padding: theme.spacing(1.5), + }, + title: { + textAlign: 'center', + }, + chart: { + width: '100%', + height: '100%', + }, + loader: { + position: 'absolute', + top: '50%', + left: '50%', + }, + }) +); + +interface Props extends ReactGoogleChartProps { + title: string; + PaperProps?: PaperProps; +} + +function ChartPaper({ PaperProps, title, ...chartProps }: Props): JSX.Element { + const classes = useStyles(); + const theme = useTheme(); + const { fontStyle } = theme.mixins.chart(theme); + + return ( + + } + /> + + ); +} + +export default ChartPaper; diff --git a/client/src/components/info-paper/InfoPaper.tsx b/client/src/components/info-paper/InfoPaper.tsx new file mode 100644 index 000000000..46419022a --- /dev/null +++ b/client/src/components/info-paper/InfoPaper.tsx @@ -0,0 +1,37 @@ +import { Paper, PaperProps, Typography } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; +import React from 'react'; + +const useStyles = makeStyles(theme => + createStyles({ + summaryPaper: { + flex: 1, + background: theme.palette.background.default, + padding: theme.spacing(1.5), + position: 'relative', + }, + title: { + textAlign: 'center', + }, + }) +); + +interface Props extends PaperProps { + title: string; + children?: React.ReactNode; +} + +function InfoPaper({ children, title, className, ...props }: Props): JSX.Element { + const classes = useStyles(); + + return ( + + {title} + + {children} + + ); +} + +export default InfoPaper; diff --git a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx b/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx index d8a7440da..c54ef8944 100644 --- a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx +++ b/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx @@ -1,69 +1,35 @@ +import { Grid, GridProps } from '@material-ui/core'; +import { useTheme } from '@material-ui/core/styles'; import React from 'react'; -import { makeStyles, createStyles, useTheme } from '@material-ui/core/styles'; -import { Grid, Paper, Typography, CircularProgress, GridProps } from '@material-ui/core'; -import Chart from 'react-google-charts'; import { ScheinCriteriaSummary } from 'shared/dist/model/ScheinCriteria'; - -const useStyles = makeStyles(theme => - createStyles({ - summaryPaper: { - flex: 1, - background: theme.palette.background.default, - padding: theme.spacing(1.5), - }, - title: { - textAlign: 'center', - }, - chart: { - width: '100%', - height: '100%', - }, - loader: { - position: 'absolute', - top: '50%', - left: '50%', - }, - }) -); +import ChartPaper from '../../../../components/info-paper/ChartPaper'; interface Props extends GridProps { scheinStatus: ScheinCriteriaSummary; } function CriteriaCharts({ scheinStatus, ...props }: Props): JSX.Element { - const classes = useStyles(); const theme = useTheme(); - const { fontStyle } = theme.mixins.chart(theme); return ( {Object.values(scheinStatus.scheinCriteriaSummary).map(summary => ( - - {summary.name} - - } - data={[ - ['Status', 'Anzahl'], - ['Erfüllt', summary.achieved], - ['Nicht erfüllt', summary.total - summary.achieved], - ]} - options={{ - backgroundColor: 'transparent', - ...fontStyle, - slices: { - 0: { color: theme.palette.green.dark }, - 1: { color: theme.palette.red.dark }, - }, - legend: { - ...fontStyle, - }, - }} - /> - + ))} From dd176fd1a1b91fcdd3789b80bad24a685800226e Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:17:52 +0100 Subject: [PATCH 05/19] Add getInformation() to Scheincriteria. --- .../model/scheincriteria/Scheincriteria.ts | 8 ++++++-- .../criterias/AttendanceCriteria.ts | 19 ++++++++++++------- .../criterias/PresentationCriteria.ts | 10 +++++++--- .../criterias/SheetIndividualCriteria.ts | 18 +++++++++++------- .../criterias/SheetTotalCriteria.ts | 14 +++++++++----- 5 files changed, 45 insertions(+), 24 deletions(-) diff --git a/server/src/model/scheincriteria/Scheincriteria.ts b/server/src/model/scheincriteria/Scheincriteria.ts index aa439b257..32edfe437 100644 --- a/server/src/model/scheincriteria/Scheincriteria.ts +++ b/server/src/model/scheincriteria/Scheincriteria.ts @@ -1,10 +1,12 @@ import * as fs from 'fs'; -import { ScheinCriteriaStatus } from 'shared/dist/model/ScheinCriteria'; +import { ScheinCriteriaStatus, CriteriaInformation } from 'shared/dist/model/ScheinCriteria'; import { Student } from 'shared/dist/model/Student'; import * as Yup from 'yup'; import Logger from '../../helpers/Logger'; +import { StudentDocument } from '../documents/StudentDocument'; export type StatusCheckResponse = Omit; +export type CriteriaInformationWithoutName = Omit; export abstract class Scheincriteria { readonly identifier: string; @@ -13,7 +15,9 @@ export abstract class Scheincriteria { this.identifier = identifier; } - abstract async checkCriteriaStatus(student: Student): Promise; + abstract async checkCriteriaStatus(student: StudentDocument): Promise; + + abstract async getInformation(students: StudentDocument[]): Promise; } export type ScheincriteriaYupSchema = Yup.Schema; diff --git a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts index 125c5e25e..fd9d86a96 100644 --- a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts +++ b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts @@ -1,21 +1,22 @@ -import { Student } from 'shared/dist/model/Student'; +import { AttendanceState } from 'shared/dist/model/Attendance'; +import { ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; -import { StatusCheckResponse } from '../Scheincriteria'; +import tutorialService from '../../../services/tutorial-service/TutorialService.class'; +import { StudentDocument } from '../../documents/StudentDocument'; +import { CriteriaInformationWithoutName, StatusCheckResponse } from '../Scheincriteria'; import { PossiblePercentageCriteria, possiblePercentageCriteriaSchema, } from './PossiblePercentageCriteria'; -import tutorialService from '../../../services/tutorial-service/TutorialService.class'; -import { AttendanceState } from 'shared/dist/model/Attendance'; -import { ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; +import { getIdOfDocumentRef } from '../../../helpers/documentHelpers'; export class AttendanceCriteria extends PossiblePercentageCriteria { constructor(percentage: boolean, valueNeeded: number) { super('attendance', percentage, valueNeeded); } - async checkCriteriaStatus(student: Student): Promise { - const tutorial = await tutorialService.getDocumentWithID(student.tutorial); + async checkCriteriaStatus(student: StudentDocument): Promise { + const tutorial = await tutorialService.getDocumentWithID(getIdOfDocumentRef(student.tutorial)); const total = tutorial.dates.length; let visitedOrExcused = 0; @@ -39,6 +40,10 @@ export class AttendanceCriteria extends PossiblePercentageCriteria { infos: {}, }; } + + async getInformation(students: StudentDocument[]): Promise { + throw new Error('Method not implemented.'); + } } scheincriteriaService.registerBluePrint( diff --git a/server/src/model/scheincriteria/criterias/PresentationCriteria.ts b/server/src/model/scheincriteria/criterias/PresentationCriteria.ts index 5b8dd8805..5ba032c02 100644 --- a/server/src/model/scheincriteria/criterias/PresentationCriteria.ts +++ b/server/src/model/scheincriteria/criterias/PresentationCriteria.ts @@ -1,9 +1,9 @@ import { ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; -import { Student } from 'shared/dist/model/Student'; import * as Yup from 'yup'; import { CleanCriteriaShape } from '../../../helpers/typings'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; -import { Scheincriteria, StatusCheckResponse } from '../Scheincriteria'; +import { StudentDocument } from '../../documents/StudentDocument'; +import { CriteriaInformationWithoutName, Scheincriteria, StatusCheckResponse } from '../Scheincriteria'; import { ScheincriteriaNumber } from '../ScheincriteriaDecorators'; export class PresentationCriteria extends Scheincriteria { @@ -15,7 +15,7 @@ export class PresentationCriteria extends Scheincriteria { this.presentationsNeeded = presentationsNeeded; } - async checkCriteriaStatus(student: Student): Promise { + async checkCriteriaStatus(student: StudentDocument): Promise { const achieved = Object.values(student.presentationPoints).reduce( (prev, current) => prev + current, 0 @@ -30,6 +30,10 @@ export class PresentationCriteria extends Scheincriteria { infos: {}, }; } + + async getInformation(students: StudentDocument[]): Promise { + throw new Error('Method not implemented.'); + } } const presentationCriteriaSchema = Yup.object().shape>({ diff --git a/server/src/model/scheincriteria/criterias/SheetIndividualCriteria.ts b/server/src/model/scheincriteria/criterias/SheetIndividualCriteria.ts index ba0a8a9b7..bbafed103 100644 --- a/server/src/model/scheincriteria/criterias/SheetIndividualCriteria.ts +++ b/server/src/model/scheincriteria/criterias/SheetIndividualCriteria.ts @@ -1,16 +1,16 @@ -import { Student } from 'shared/dist/model/Student'; +import { PassedState, ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; +import { Sheet } from 'shared/dist/model/Sheet'; import * as Yup from 'yup'; import { CleanCriteriaShape } from '../../../helpers/typings'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; -import { StatusCheckResponse } from '../Scheincriteria'; +import sheetService from '../../../services/sheet-service/SheetService.class'; +import { StudentDocument } from '../../documents/StudentDocument'; +import { CriteriaInformationWithoutName, StatusCheckResponse } from '../Scheincriteria'; import { ScheincriteriaPossiblePercentage } from '../ScheincriteriaDecorators'; import { PossiblePercentageCriteria, possiblePercentageCriteriaSchema, } from './PossiblePercentageCriteria'; -import sheetService from '../../../services/sheet-service/SheetService.class'; -import { PassedState, ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; -import { Sheet } from 'shared/dist/model/Sheet'; export class SheetIndividualCriteria extends PossiblePercentageCriteria { @ScheincriteriaPossiblePercentage('percentagePerSheet') @@ -29,7 +29,7 @@ export class SheetIndividualCriteria extends PossiblePercentageCriteria { this.percentagePerSheet = percentagePerSheet; } - async checkCriteriaStatus(student: Student): Promise { + async checkCriteriaStatus(student: StudentDocument): Promise { const sheets = await sheetService.getAllSheets(); const infos: StatusCheckResponse['infos'] = {}; const totalSheetCount = sheets.reduce((count, sheet) => count + (sheet.bonusSheet ? 0 : 1), 0); @@ -53,9 +53,13 @@ export class SheetIndividualCriteria extends PossiblePercentageCriteria { }; } + async getInformation(students: StudentDocument[]): Promise { + throw new Error('Method not implemented.'); + } + private async checkAllSheets( sheets: Sheet[], - student: Student, + student: StudentDocument, infos: StatusCheckResponse['infos'] ) { let sheetsPassed = 0; diff --git a/server/src/model/scheincriteria/criterias/SheetTotalCriteria.ts b/server/src/model/scheincriteria/criterias/SheetTotalCriteria.ts index f8bab977c..3e128dd0f 100644 --- a/server/src/model/scheincriteria/criterias/SheetTotalCriteria.ts +++ b/server/src/model/scheincriteria/criterias/SheetTotalCriteria.ts @@ -1,20 +1,20 @@ import { PassedState, ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; -import { Student } from 'shared/dist/model/Student'; +import { Sheet } from 'shared/dist/model/Sheet'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; import sheetService from '../../../services/sheet-service/SheetService.class'; -import { StatusCheckResponse } from '../Scheincriteria'; +import { StudentDocument } from '../../documents/StudentDocument'; +import { CriteriaInformationWithoutName, StatusCheckResponse } from '../Scheincriteria'; import { PossiblePercentageCriteria, possiblePercentageCriteriaSchema, } from './PossiblePercentageCriteria'; -import { Sheet } from 'shared/dist/model/Sheet'; export class SheetTotalCriteria extends PossiblePercentageCriteria { constructor(percentage: boolean, valueNeeded: number) { super('sheetTotal', percentage, valueNeeded); } - async checkCriteriaStatus(student: Student): Promise { + async checkCriteriaStatus(student: StudentDocument): Promise { const sheets = await sheetService.getAllSheets(); const infos: StatusCheckResponse['infos'] = {}; @@ -38,9 +38,13 @@ export class SheetTotalCriteria extends PossiblePercentageCriteria { }; } + async getInformation(students: StudentDocument[]): Promise { + throw new Error('Method not implemented.'); + } + private async checkAllSheets( sheets: Sheet[], - student: Student, + student: StudentDocument, infos: StatusCheckResponse['infos'] ) { let pointsAchieved = 0; From e0b3c37814e56fcfb9f6573c7d2b19ae300ab7e6 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:18:23 +0100 Subject: [PATCH 06/19] Change small parts of ScheinexamDoc & StudentDoc. --- .../src/model/documents/ScheinexamDocument.ts | 18 ++++++++++++++---- server/src/model/documents/StudentDocument.ts | 8 ++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/server/src/model/documents/ScheinexamDocument.ts b/server/src/model/documents/ScheinexamDocument.ts index b726342c4..a6ce55cba 100644 --- a/server/src/model/documents/ScheinexamDocument.ts +++ b/server/src/model/documents/ScheinexamDocument.ts @@ -1,11 +1,17 @@ import { arrayProp, instanceMethod, InstanceType, prop, Typegoose } from '@typegoose/typegoose'; import { Document, Model } from 'mongoose'; import { ScheinExam } from 'shared/dist/model/Scheinexam'; -import { getPointsOfAllExercises, PointMap } from 'shared/dist/model/Points'; +import { getPointsOfAllExercises, PointMap, ExercisePointInfo } from 'shared/dist/model/Points'; import { CollectionName } from '../CollectionName'; import { ExerciseDocument, ExerciseSchema } from './ExerciseDocument'; import { StudentDocument } from './StudentDocument'; +interface PassedInformation { + passed: boolean; + achieved: number; + total: ExercisePointInfo; +} + export class ScheinexamSchema extends Typegoose implements Omit { @prop({ required: true }) scheinExamNo!: number; @@ -20,12 +26,16 @@ export class ScheinexamSchema extends Typegoose implements Omit, student: StudentDocument): boolean { + hasPassed(this: InstanceType, student: StudentDocument): PassedInformation { const points = new PointMap(student.scheinExamResults); const achieved: number = points.getSumOfPoints(this); - const { must: total } = getPointsOfAllExercises(this); + const { must, bonus } = getPointsOfAllExercises(this); - return achieved / total >= this.percentageNeeded; + return { + passed: achieved / must >= this.percentageNeeded, + achieved, + total: { must, bonus }, + }; } } diff --git a/server/src/model/documents/StudentDocument.ts b/server/src/model/documents/StudentDocument.ts index 904262e7b..f20b8e1a2 100644 --- a/server/src/model/documents/StudentDocument.ts +++ b/server/src/model/documents/StudentDocument.ts @@ -63,14 +63,14 @@ export class StudentSchema extends Typegoose @prop({ default: StudentStatus.ACTIVE }) status!: StudentStatus; - @mapProp({ of: AttendanceSchema }) - attendance?: Types.Map; + @mapProp({ of: AttendanceSchema, default: {} }) + attendance!: Types.Map; @prop({ default: {} }) points!: PointMapDTO; - @mapProp({ of: Number }) - presentationPoints?: Types.Map; + @mapProp({ of: Number, default: {} }) + presentationPoints!: Types.Map; @prop({ default: {} }) scheinExamResults!: PointMapDTO; From ec28b4756094649227beb5fe1e82fc0fadb5caed Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:18:55 +0100 Subject: [PATCH 07/19] Add endpoint to get information about one criteria. --- .../criterias/ScheinexamCriteria.ts | 117 +++++++++++++++--- .../modules/ScheinexamResultPDFModule.ts | 2 +- .../ScheincriteriaService.class.ts | 70 ++++++++++- .../ScheincriteriaService.routes.ts | 9 ++ shared/src/model/ScheinCriteria.ts | 43 +++++++ 5 files changed, 218 insertions(+), 23 deletions(-) diff --git a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts index e7bc57190..6fb4ba140 100644 --- a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts +++ b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts @@ -1,12 +1,25 @@ -import { PassedState, ScheinCriteriaUnit } from 'shared/dist/model/ScheinCriteria'; -import { ScheinExam } from 'shared/dist/model/Scheinexam'; -import { Student } from 'shared/dist/model/Student'; +import { + CriteriaSheetOrExamInformation, + PassedState, + ScheinCriteriaUnit, + CriteriaDistributionInformation, + CriteriaAveragesInformation, +} from 'shared/dist/model/ScheinCriteria'; import * as Yup from 'yup'; import { CleanCriteriaShape } from '../../../helpers/typings'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; import scheinexamService from '../../../services/scheinexam-service/ScheinexamService.class'; -import { Scheincriteria, StatusCheckResponse } from '../Scheincriteria'; +import { ScheinexamDocument } from '../../documents/ScheinexamDocument'; +import { StudentDocument } from '../../documents/StudentDocument'; +import { + CriteriaInformationWithoutName, + Scheincriteria, + StatusCheckResponse, +} from '../Scheincriteria'; import { ScheincriteriaPercentage } from '../ScheincriteriaDecorators'; +import { PointMap, getPointsOfExercise, PointId } from 'shared/src/model/Points'; +import { ExerciseDTOSchema } from 'shared/src/validators/Sheet'; +import { convertDocumentToExercise } from '../../documents/ExerciseDocument'; export class ScheinexamCriteria extends Scheincriteria { readonly passAllExamsIndividually: boolean; @@ -21,8 +34,8 @@ export class ScheinexamCriteria extends Scheincriteria { this.percentageOfAllPointsNeeded = percentageOfAllPointsNeeded; } - async checkCriteriaStatus(student: Student): Promise { - const exams = await scheinexamService.getAllScheinExams(); + async checkCriteriaStatus(student: StudentDocument): Promise { + const exams = await scheinexamService.getAllScheinExamAsDocuments(); const infos: StatusCheckResponse['infos'] = {}; const { examsPassed, pointsAchieved, pointsTotal } = this.checkAllExams(exams, student, infos); @@ -44,9 +57,81 @@ export class ScheinexamCriteria extends Scheincriteria { }; } + async getInformation(students: StudentDocument[]): Promise { + const exams = await scheinexamService.getAllScheinExamAsDocuments(); + const information: CriteriaInformationWithoutName['information'] = {}; + + exams.forEach(exam => { + const averages: { [exName: string]: number[] } = {}; + const distribution: CriteriaDistributionInformation = {}; + const achieved = { achieved: 0, notAchieved: 0, notPresent: 0 }; + + exam.exercises.forEach(exercise => { + averages[exercise.exName] = []; + }); + + students.forEach(student => { + const points = new PointMap(student.scheinExamResults); + const hasAttended = points.has(exam.id); + + if (!hasAttended) { + achieved.notPresent += 1; + return; + } + + const result = exam.hasPassed(student); + const distributionForThisResult = distribution[result.achieved] ?? { + value: 0, + aboveThreshhold: result.achieved / result.total.must >= exam.percentageNeeded, + }; + + exam.exercises.forEach(exercise => { + averages[exercise.exName].push(points.getPoints(new PointId(exam.id, exercise)) ?? 0); + }); + + distribution[result.achieved] = { + aboveThreshhold: distributionForThisResult.aboveThreshhold, + value: distributionForThisResult.value + 1, + }; + + if (result.passed) { + achieved.achieved += 1; + } else { + achieved.notAchieved += 1; + } + }); + + information[exam.id] = { + achieved, + total: achieved.achieved + achieved.notAchieved + achieved.notPresent, + averages: exam.exercises.reduce((avgInfo, exercise) => { + const total: number = getPointsOfExercise(exercise).must; + const achievedPoints = averages[exercise.exName]; + const value: number = + achievedPoints.length > 0 + ? achievedPoints.reduce((sum, current) => sum + current, 0) / achievedPoints.length + : 0; + + return { ...avgInfo, [exercise.exName]: { value, total } }; + }, {}), + distribution, + }; + }); + + return { + identifier: this.identifier, + sheetsOrExams: exams.map(exam => ({ + id: exam.id, + no: exam.scheinExamNo, + exercises: exam.exercises.map(convertDocumentToExercise), + })), + information, + }; + } + private checkAllExams( - exams: ScheinExam[], - student: Student, + exams: ScheinexamDocument[], + student: StudentDocument, infos: StatusCheckResponse['infos'] ): { examsPassed: number; pointsAchieved: number; pointsTotal: number } { // FIXME: DOES NOT WORK!!! @@ -55,21 +140,19 @@ export class ScheinexamCriteria extends Scheincriteria { let examsPassed = 0; for (const exam of exams) { - const result = scheinexamService.getScheinExamResult(student, exam); - const maxPoints = scheinexamService.getScheinExamTotalPoints(exam); - let state: PassedState = PassedState.NOTPASSED; + const { passed, achieved, total } = exam.hasPassed(student); + const state: PassedState = passed ? PassedState.PASSED : PassedState.NOTPASSED; - if (result / maxPoints > this.percentageOfAllPointsNeeded) { - state = PassedState.PASSED; + if (passed) { examsPassed += 1; } - pointsAchieved += result; - pointsTotal += maxPoints; + pointsAchieved += achieved; + pointsTotal += total.must; infos[exam.id] = { - achieved: result, - total: maxPoints, + achieved: achieved, + total: total.must, no: exam.scheinExamNo, unit: ScheinCriteriaUnit.POINT, state, diff --git a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts index d43c4b171..dd82563c2 100644 --- a/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts +++ b/server/src/services/pdf-service/modules/ScheinexamResultPDFModule.ts @@ -110,7 +110,7 @@ export class ScheinexamResultPDFModule extends PDFWithStudentsModule { + const [criteriaDoc, students] = await Promise.all([ + this.getDocumentWithId(id), + studentService.getAllStudentsAsDocuments(), + ]); + const criteria = this.generateCriteriaFromDocument(criteriaDoc); + const [criteriaInfo, studentSummaries] = await Promise.all([ + criteria.getInformation(students), + this.getSingleCriteriaResultOfAllStudents(criteriaDoc, students), + ]); + + return { + name: criteriaDoc.name, + studentSummaries, + ...criteriaInfo, + }; + } + + public async getSingleCriteriaResultOfAllStudents( + criteriaDoc: ScheincriteriaDocument, + students: StudentDocument[] + ): Promise { + // TODO: Clean me up. + const criteria = this.generateCriteriaFromDocument(criteriaDoc); + const results: Promise[] = []; + const studentSummaries: SingleScheincriteriaSummaryByStudents = {}; + + for (const student of students) { + results.push( + new Promise((resolve, reject) => { + criteria + .checkCriteriaStatus(student) + .then(status => { + resolve({ + id: criteriaDoc.id, + name: criteriaDoc.name, + ...status, + }); + }) + .catch(err => reject(err)); + }) + ); + } + + (await Promise.all(results)).forEach((status, idx) => { + const student = students[idx]; + studentSummaries[student.id] = status; + }); + + return studentSummaries; + } + public async getCriteriaResultsOfAllStudents(): Promise { - return this.calculateCriteriaResultOfMultipleStudents(await studentService.getAllStudents()); + return this.calculateCriteriaResultOfMultipleStudents( + await studentService.getAllStudentsAsDocuments() + ); } public async getCriteriaResultOfStudent(studentId: string): Promise { const [student, criterias] = await Promise.all([ - studentService.getStudentWithId(studentId), + studentService.getDocumentWithId(studentId), this.getAllCriteriaObjects(), ]); @@ -114,13 +172,15 @@ export class ScheincriteriaService { public async getCriteriaResultsOfStudentsOfTutorial( tutorialId: string ): Promise { - const students: Student[] = await tutorialService.getStudentsOfTutorial(tutorialId); + const students: StudentDocument[] = await tutorialService.getStudentsOfTutorialAsDocuments( + tutorialId + ); return this.calculateCriteriaResultOfMultipleStudents(students); } private async calculateCriteriaResultOfMultipleStudents( - students: Student[] + students: StudentDocument[] ): Promise { const summaries: ScheincriteriaSummaryByStudents = {}; const criterias = await this.getAllCriteriaObjects(); @@ -144,7 +204,7 @@ export class ScheincriteriaService { } private async calculateCriteriaResultOfStudent( - student: Student, + student: StudentDocument, criterias: ScheincriteriaWithId[] ): Promise { const criteriaSummaries: ScheinCriteriaSummary['scheinCriteriaSummary'] = {}; diff --git a/server/src/services/scheincriteria-service/ScheincriteriaService.routes.ts b/server/src/services/scheincriteria-service/ScheincriteriaService.routes.ts index 85ed04147..02f965ef7 100644 --- a/server/src/services/scheincriteria-service/ScheincriteriaService.routes.ts +++ b/server/src/services/scheincriteria-service/ScheincriteriaService.routes.ts @@ -4,6 +4,7 @@ import { Role } from 'shared/dist/model/Role'; import { ScheinCriteriaResponse, ScheincriteriaSummaryByStudents, + CriteriaInformation, } from 'shared/dist/model/ScheinCriteria'; import { validateAgainstScheincriteriaDTO } from 'shared/dist/validators/Scheincriteria'; import { @@ -48,6 +49,14 @@ scheincriteriaRouter.post( } ); +scheincriteriaRouter.get('/:id/info', ...checkRoleAccess(Role.ADMIN), async (req, res) => { + const id = req.params.id; + + const information: CriteriaInformation = await scheincriteriaService.getCriteriaInformation(id); + + res.json(information); +}); + scheincriteriaRouter.patch( '/:id', ...checkRoleAccess(Role.ADMIN), diff --git a/shared/src/model/ScheinCriteria.ts b/shared/src/model/ScheinCriteria.ts index 59a3083da..437c0408a 100644 --- a/shared/src/model/ScheinCriteria.ts +++ b/shared/src/model/ScheinCriteria.ts @@ -1,3 +1,5 @@ +import { HasExercises } from './Sheet'; + export interface ScheinCriteriaResponse { id: string; identifier: string; @@ -13,6 +15,8 @@ export interface ScheinCriteriaDTO { export type ScheincriteriaSummaryByStudents = { [studentId: string]: ScheinCriteriaSummary }; +export type SingleScheincriteriaSummaryByStudents = { [studentId: string]: ScheinCriteriaStatus }; + export interface ScheinCriteriaSummary { passed: boolean; scheinCriteriaSummary: { [criteriaId: string]: ScheinCriteriaStatus }; @@ -57,3 +61,42 @@ export enum ScheinCriteriaUnit { PRESENTATION = 'PRESENTATION', DATE = 'DATE', } + +export interface CriteriaAchievedInformation { + achieved: number; + notAchieved: number; + [other: string]: number; +} + +export interface CriteriaDistributionInformation { + [key: string]: { + value: number; + aboveThreshhold: boolean; + }; +} + +export interface CriteriaAveragesInformation { + [identifier: string]: { + value: number; + total: number; + }; +} + +export interface CriteriaSheetOrExamInformation extends HasExercises { + no: number; +} + +export interface CriteriaInformationItem { + achieved: CriteriaAchievedInformation; + total: number; + distribution?: CriteriaDistributionInformation; + averages?: CriteriaAveragesInformation; +} + +export interface CriteriaInformation { + identifier: string; + name: string; + studentSummaries: SingleScheincriteriaSummaryByStudents; + sheetsOrExams?: CriteriaSheetOrExamInformation[]; + information: { [id: string]: CriteriaInformationItem }; +} From 587a33591fcfd67fe37622791f69e2bc98337c91 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:19:22 +0100 Subject: [PATCH 08/19] Add view to display the information about a criteria. --- .../static/locales/de/scheincriteria.json | 5 +- client/src/hooks/fetching/Scheincriteria.ts | 18 +- client/src/hooks/fetching/Student.ts | 15 +- client/src/routes/Routing.helpers.ts | 4 + .../criteria-info-view/CriteriaInfoView.tsx | 258 +++++++++++++++++- .../components/ScheinCriteriaRow.tsx | 13 + 6 files changed, 293 insertions(+), 20 deletions(-) diff --git a/client/public/static/locales/de/scheincriteria.json b/client/public/static/locales/de/scheincriteria.json index 9114a73ba..337d6ceff 100644 --- a/client/public/static/locales/de/scheincriteria.json +++ b/client/public/static/locales/de/scheincriteria.json @@ -30,5 +30,8 @@ "UNIT_LABEL_PRESENTATION_plural" : "Präsentationen", "UNIT_LABEL_SHEET_plural" : "Blätter", "true" : "Ja", - "false" : "Nein" + "false" : "Nein", + "ACHIEVED_PIE_CHART_LABEL_achieved": "Bestanden", + "ACHIEVED_PIE_CHART_LABEL_notAchieved": "Nicht bestanden", + "ACHIEVED_PIE_CHART_LABEL_notPresent": "Abwesend" } \ No newline at end of file diff --git a/client/src/hooks/fetching/Scheincriteria.ts b/client/src/hooks/fetching/Scheincriteria.ts index 851d6969d..956bc4d7d 100644 --- a/client/src/hooks/fetching/Scheincriteria.ts +++ b/client/src/hooks/fetching/Scheincriteria.ts @@ -1,6 +1,10 @@ import axios from './Axios'; import { FormDataResponse } from '../../components/generatedForm/types/FieldData'; -import { ScheinCriteriaResponse, ScheinCriteriaDTO } from 'shared/dist/model/ScheinCriteria'; +import { + ScheinCriteriaResponse, + ScheinCriteriaDTO, + CriteriaInformation, +} from 'shared/dist/model/ScheinCriteria'; export async function getAllScheinCriterias(): Promise { const response = await axios.get('scheincriteria'); @@ -12,6 +16,18 @@ export async function getAllScheinCriterias(): Promise return Promise.reject(`Wrong response code (${response.status}).`); } +export async function getScheincriteriaInformation( + criteriaId: string +): Promise { + const response = await axios.get(`scheincriteria/${criteriaId}/info`); + + if (response.status === 200) { + return response.data; + } + + return Promise.reject(`Wrong response code (${response.status}).`); +} + export async function getScheinCriteriaFormData(): Promise { const response = await axios.get('scheincriteria/form'); diff --git a/client/src/hooks/fetching/Student.ts b/client/src/hooks/fetching/Student.ts index ca07ca498..60219212b 100644 --- a/client/src/hooks/fetching/Student.ts +++ b/client/src/hooks/fetching/Student.ts @@ -1,6 +1,9 @@ import { Attendance, AttendanceDTO } from 'shared/dist/model/Attendance'; import { UpdatePointsDTO } from 'shared/dist/model/Points'; -import { ScheinCriteriaSummary } from 'shared/dist/model/ScheinCriteria'; +import { + ScheinCriteriaSummary, + ScheincriteriaSummaryByStudents, +} from 'shared/dist/model/ScheinCriteria'; import { CakeCountDTO, PresentationPointsDTO, @@ -152,12 +155,10 @@ export async function getScheinCriteriaSummaryOfStudent( return Promise.reject(`Wrong status code (${response.status}).`); } -export async function getScheinCriteriaSummaryOfAllStudents(): Promise<{ - [studentId: string]: ScheinCriteriaSummary; -}> { - const response = await axios.get<{ [studentId: string]: ScheinCriteriaSummary }>( - `/scheincriteria/student` - ); +export async function getScheinCriteriaSummaryOfAllStudents(): Promise< + ScheincriteriaSummaryByStudents +> { + const response = await axios.get(`/scheincriteria/student`); if (response.status === 200) { return response.data; diff --git a/client/src/routes/Routing.helpers.ts b/client/src/routes/Routing.helpers.ts index 39ee403f5..59af9df7e 100644 --- a/client/src/routes/Routing.helpers.ts +++ b/client/src/routes/Routing.helpers.ts @@ -122,3 +122,7 @@ export function getStudentInfoPath({ studentId, tutorialId }: StudentInfoParams) .replace(':tutorialId?', tutorialId ?? '') .replace(/\/\/+/, '/'); } + +export function getScheincriteriaInfoPath(criteriaId: string): string { + return RoutingPath.SCHEIN_CRITERIAS_INFO.replace(':id', criteriaId).replace(/\/\/+/, '/'); +} diff --git a/client/src/view/criteria-info-view/CriteriaInfoView.tsx b/client/src/view/criteria-info-view/CriteriaInfoView.tsx index 61414d1c6..fbc99ee08 100644 --- a/client/src/view/criteria-info-view/CriteriaInfoView.tsx +++ b/client/src/view/criteria-info-view/CriteriaInfoView.tsx @@ -1,11 +1,36 @@ -import { Box, Typography } from '@material-ui/core'; -import { createStyles, makeStyles } from '@material-ui/core/styles'; -import React from 'react'; +import { + Box, + Chip, + Grid, + Table, + TableBody, + TableCell, + TableRow, + Typography, +} from '@material-ui/core'; +import { createStyles, makeStyles, useTheme } from '@material-ui/core/styles'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router'; +import { PointMap } from 'shared/dist/model/Points'; +import { CriteriaInformation, CriteriaInformationItem } from 'shared/dist/model/ScheinCriteria'; +import { HasExercises } from 'shared/dist/model/Sheet'; +import { Student } from 'shared/dist/model/Student'; import { getNameOfEntity } from 'shared/dist/util/helpers'; import BackButton from '../../components/BackButton'; +import CustomSelect, { OnChangeHandler } from '../../components/CustomSelect'; +import ChartPaper from '../../components/info-paper/ChartPaper'; +import InfoPaper from '../../components/info-paper/InfoPaper'; +import PaperTableRow from '../../components/PaperTableRow'; import Placeholder from '../../components/Placeholder'; +import PointsTable from '../../components/points-table/PointsTable'; +import StudentAvatar from '../../components/student-icon/StudentAvatar'; +import TableWithPadding from '../../components/TableWithPadding'; +import { getScheincriteriaInformation } from '../../hooks/fetching/Scheincriteria'; +import { getAllStudents } from '../../hooks/fetching/Student'; +import { useErrorSnackbar } from '../../hooks/useErrorSnackbar'; import { RoutingPath } from '../../routes/Routing.routes'; -import ScheinStatusBox from '../studentmanagement/student-info/components/ScheinStatusBox'; +import { i18nNamespace } from '../../util/lang/configI18N'; const useStyles = makeStyles(theme => createStyles({ @@ -13,28 +38,239 @@ const useStyles = makeStyles(theme => marginRight: theme.spacing(2), alignSelf: 'center', }, + graphGrid: { + marginTop: theme.spacing(1.5), + marginBottom: theme.spacing(2), + }, + studentRow: { + marginBottom: theme.spacing(2), + }, }) ); +interface PathParams { + id: string; +} + function CriteriaInfoView(): JSX.Element { const classes = useStyles(); + const theme = useTheme(); + + const { id: criteriaId } = useParams(); + const { setError } = useErrorSnackbar(); + + const { t } = useTranslation(i18nNamespace.SCHEINCRITERIA); + + const [students, setStudents] = useState(); + const [criteriaInfo, setCriteriaInfo] = useState(); + const [information, setInformation] = useState(); + const [selectedSheetOrExam, setSelectetSheetOrExam] = useState(); + + useEffect(() => { + getAllStudents() + .then(response => { + setStudents(response); + }) + .catch(() => { + setError('Studierende konnten nicht abgerufen werden.'); + setStudents(undefined); + }); + }, [setError]); + + useEffect(() => { + getScheincriteriaInformation(criteriaId) + .then(response => { + setCriteriaInfo(response); + }) + .catch(() => { + setError('Informationen über das Kriterium konnten nicht abgerufen werden.'); + }); + }, [criteriaId, setError]); + + useEffect(() => { + setInformation( + selectedSheetOrExam ? criteriaInfo?.information[selectedSheetOrExam.id] : undefined + ); + }, [criteriaInfo, selectedSheetOrExam]); + + const handleSheetOrExamChange: OnChangeHandler = event => { + if (!criteriaInfo?.sheetsOrExams) { + return; + } + + const exam = criteriaInfo.sheetsOrExams.find(exam => exam.id === event.target.value); + + setSelectetSheetOrExam(exam); + }; return ( - {/* {student && getNameOfEntity(student)} - - */} + {criteriaInfo && {criteriaInfo.name}} - + {criteriaInfo && criteriaInfo.sheetsOrExams && ( + + `Scheinklausur #${item.no}`} + itemToValue={item => item.id} + onChange={handleSheetOrExamChange} + value={selectedSheetOrExam?.id ?? ''} + /> + + + {!!selectedSheetOrExam && !!information && ( + <> + + + [ + t(`ACHIEVED_PIE_CHART_LABEL_${key}`), + value, + ]), + ]} + options={{ + slices: { + 0: { color: theme.palette.green.dark }, + 1: { color: theme.palette.red.dark }, + 2: { color: theme.palette.orange.dark }, + }, + legend: { + position: 'labeled', + }, + pieSliceText: 'value', + }} + /> + + + {information.distribution && ( + + [ + key, + info.value, + info.aboveThreshhold + ? theme.palette.green.dark + : theme.palette.red.dark, + info.value, + ]), + ]} + options={{ + slices: { + 0: { color: theme.palette.green.dark }, + 1: { color: theme.palette.red.dark }, + 2: { color: theme.palette.orange.dark }, + }, + legend: { + position: 'none', + }, + }} + /> + + )} + + {information.averages && ( + + + + + {Object.entries(information.averages).map(([identifier, info]) => ( + + {identifier} + {`${info.value} / ${info.value}`} + + ))} + +
+
+
+ )} +
+ + + + {!!students && !!selectedSheetOrExam && ( + { + const hasPassed = criteriaInfo.studentSummaries[student.id].passed; + const hasAttended = new PointMap(student.scheinExamResults).has( + selectedSheetOrExam.id + ); + + return ( + } + className={classes.studentRow} + > + + + + {hasAttended ? ( + + ) : ( + + )} + + + + + + + + + ); + }} + /> + )} + + + + )} +
+
+ )}
); diff --git a/client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx b/client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx index 3a0cc8d35..ac07fd38a 100644 --- a/client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx +++ b/client/src/view/scheincriteriamanagement/components/ScheinCriteriaRow.tsx @@ -1,11 +1,14 @@ import { TableCell, Typography } from '@material-ui/core'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { InformationOutline as InfoIcon } from 'mdi-material-ui'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ScheinCriteriaResponse as ScheinCriteria } from 'shared/dist/model/ScheinCriteria'; import EntityListItemMenu from '../../../components/list-item-menu/EntityListItemMenu'; import PaperTableRow, { PaperTableRowProps } from '../../../components/PaperTableRow'; import { i18nNamespace } from '../../../util/lang/configI18N'; +import { useHistory } from 'react-router'; +import { getScheincriteriaInfoPath } from '../../../routes/Routing.helpers'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -31,6 +34,7 @@ function ScheinCriteriaRow({ ...rest }: Props): JSX.Element { const classes = useStyles(); + const history = useHistory(); const { t } = useTranslation(i18nNamespace.SCHEINCRITERIA); return ( @@ -41,6 +45,15 @@ function ScheinCriteriaRow({ onEditCriteriaClicked(criteria)} onDeleteClicked={() => onDeleteCriteriaClicked(criteria)} + additionalItems={[ + { + primary: 'Informationen', + Icon: InfoIcon, + onClick: () => { + history.push(getScheincriteriaInfoPath(criteria.id)); + }, + }, + ]} /> } {...rest} From 94ca897da31c47f93bc9fc7b1a6d946735649291 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:21:54 +0100 Subject: [PATCH 09/19] Run prettier. --- server/src/model/scheincriteria/Scheincriteria.ts | 4 +++- .../model/scheincriteria/criterias/PresentationCriteria.ts | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/model/scheincriteria/Scheincriteria.ts b/server/src/model/scheincriteria/Scheincriteria.ts index 32edfe437..1c05f638d 100644 --- a/server/src/model/scheincriteria/Scheincriteria.ts +++ b/server/src/model/scheincriteria/Scheincriteria.ts @@ -17,7 +17,9 @@ export abstract class Scheincriteria { abstract async checkCriteriaStatus(student: StudentDocument): Promise; - abstract async getInformation(students: StudentDocument[]): Promise; + abstract async getInformation( + students: StudentDocument[] + ): Promise; } export type ScheincriteriaYupSchema = Yup.Schema; diff --git a/server/src/model/scheincriteria/criterias/PresentationCriteria.ts b/server/src/model/scheincriteria/criterias/PresentationCriteria.ts index 5ba032c02..a07849d61 100644 --- a/server/src/model/scheincriteria/criterias/PresentationCriteria.ts +++ b/server/src/model/scheincriteria/criterias/PresentationCriteria.ts @@ -3,7 +3,11 @@ import * as Yup from 'yup'; import { CleanCriteriaShape } from '../../../helpers/typings'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; import { StudentDocument } from '../../documents/StudentDocument'; -import { CriteriaInformationWithoutName, Scheincriteria, StatusCheckResponse } from '../Scheincriteria'; +import { + CriteriaInformationWithoutName, + Scheincriteria, + StatusCheckResponse, +} from '../Scheincriteria'; import { ScheincriteriaNumber } from '../ScheincriteriaDecorators'; export class PresentationCriteria extends Scheincriteria { From f844dc9d1391dde0cd4611074526956c38fe606d Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:25:24 +0100 Subject: [PATCH 10/19] Fix failing test. --- server/test/scheincriterias/scheinexam.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/test/scheincriterias/scheinexam.test.ts b/server/test/scheincriterias/scheinexam.test.ts index b33feb016..42fb06a6c 100644 --- a/server/test/scheincriterias/scheinexam.test.ts +++ b/server/test/scheincriterias/scheinexam.test.ts @@ -64,7 +64,7 @@ describe('Student has correctly passed / not passed', () => { const studentDoc = await studentService.getDocumentWithId(student.id); const examDoc = await scheinexamService.getDocumentWithId(exam.id); - expect(examDoc.hasPassed(studentDoc)).toBeTruthy(); + expect(examDoc.hasPassed(studentDoc).passed).toBeTruthy(); done(); }); @@ -103,7 +103,7 @@ describe('Student has correctly passed / not passed', () => { const studentDoc = await studentService.getDocumentWithId(student.id); const examDoc = await scheinexamService.getDocumentWithId(exam.id); - expect(examDoc.hasPassed(studentDoc)).toBeTruthy(); + expect(examDoc.hasPassed(studentDoc).passed).toBeTruthy(); done(); }); @@ -142,7 +142,7 @@ describe('Student has correctly passed / not passed', () => { const studentDoc = await studentService.getDocumentWithId(student.id); const examDoc = await scheinexamService.getDocumentWithId(exam.id); - expect(examDoc.hasPassed(studentDoc)).toBeFalsy(); + expect(examDoc.hasPassed(studentDoc).passed).toBeFalsy(); done(); }); From 8c257a8602425a4753703e2852e2751b26e7ec2a Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:34:33 +0100 Subject: [PATCH 11/19] Fix wrong import from shared. --- server/src/model/scheincriteria/Scheincriteria.ts | 3 +-- .../model/scheincriteria/criterias/ScheinexamCriteria.ts | 8 +++----- .../scheincriteria-service/ScheincriteriaService.class.ts | 7 +++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/server/src/model/scheincriteria/Scheincriteria.ts b/server/src/model/scheincriteria/Scheincriteria.ts index 1c05f638d..da3cfea63 100644 --- a/server/src/model/scheincriteria/Scheincriteria.ts +++ b/server/src/model/scheincriteria/Scheincriteria.ts @@ -1,6 +1,5 @@ import * as fs from 'fs'; -import { ScheinCriteriaStatus, CriteriaInformation } from 'shared/dist/model/ScheinCriteria'; -import { Student } from 'shared/dist/model/Student'; +import { CriteriaInformation, ScheinCriteriaStatus } from 'shared/dist/model/ScheinCriteria'; import * as Yup from 'yup'; import Logger from '../../helpers/Logger'; import { StudentDocument } from '../documents/StudentDocument'; diff --git a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts index 6fb4ba140..0fe719f1d 100644 --- a/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts +++ b/server/src/model/scheincriteria/criterias/ScheinexamCriteria.ts @@ -1,14 +1,15 @@ import { + CriteriaDistributionInformation, CriteriaSheetOrExamInformation, PassedState, ScheinCriteriaUnit, - CriteriaDistributionInformation, - CriteriaAveragesInformation, } from 'shared/dist/model/ScheinCriteria'; +import { getPointsOfExercise, PointId, PointMap } from 'shared/dist/model/Points'; import * as Yup from 'yup'; import { CleanCriteriaShape } from '../../../helpers/typings'; import scheincriteriaService from '../../../services/scheincriteria-service/ScheincriteriaService.class'; import scheinexamService from '../../../services/scheinexam-service/ScheinexamService.class'; +import { convertDocumentToExercise } from '../../documents/ExerciseDocument'; import { ScheinexamDocument } from '../../documents/ScheinexamDocument'; import { StudentDocument } from '../../documents/StudentDocument'; import { @@ -17,9 +18,6 @@ import { StatusCheckResponse, } from '../Scheincriteria'; import { ScheincriteriaPercentage } from '../ScheincriteriaDecorators'; -import { PointMap, getPointsOfExercise, PointId } from 'shared/src/model/Points'; -import { ExerciseDTOSchema } from 'shared/src/validators/Sheet'; -import { convertDocumentToExercise } from '../../documents/ExerciseDocument'; export class ScheinexamCriteria extends Scheincriteria { readonly passAllExamsIndividually: boolean; diff --git a/server/src/services/scheincriteria-service/ScheincriteriaService.class.ts b/server/src/services/scheincriteria-service/ScheincriteriaService.class.ts index 626ee0ea5..f7afbceb1 100644 --- a/server/src/services/scheincriteria-service/ScheincriteriaService.class.ts +++ b/server/src/services/scheincriteria-service/ScheincriteriaService.class.ts @@ -11,15 +11,14 @@ import { FormStringFieldData, } from 'shared/dist/model/FormTypes'; import { + CriteriaInformation, ScheinCriteriaDTO, ScheinCriteriaResponse, + ScheinCriteriaStatus, ScheinCriteriaSummary, ScheincriteriaSummaryByStudents, SingleScheincriteriaSummaryByStudents, - CriteriaInformation, - ScheinCriteriaStatus, } from 'shared/dist/model/ScheinCriteria'; -import { Student } from 'shared/dist/model/Student'; import { validateSchema } from 'shared/dist/validators/helper'; import * as Yup from 'yup'; import Logger from '../../helpers/Logger'; @@ -28,6 +27,7 @@ import ScheincriteriaModel, { ScheincriteriaDocument, ScheincriteriaSchema, } from '../../model/documents/ScheincriteriaDocument'; +import { StudentDocument } from '../../model/documents/StudentDocument'; import { BadRequestError, DocumentNotFoundError } from '../../model/Errors'; import { Scheincriteria, ScheincriteriaYupSchema } from '../../model/scheincriteria/Scheincriteria'; import { ScheincriteriaForm } from '../../model/scheincriteria/ScheincriteriaForm'; @@ -37,7 +37,6 @@ import { } from '../../model/scheincriteria/ScheincriteriaMetadata'; import studentService from '../student-service/StudentService.class'; import tutorialService from '../tutorial-service/TutorialService.class'; -import { StudentDocument } from '../../model/documents/StudentDocument'; interface ScheincriteriaWithId { criteriaId: string; From bba01c2c71f31b2675329cddff30898148123f96 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 01:54:31 +0100 Subject: [PATCH 12/19] Fix wrong display of average numbers on CriteriaInfoView. --- client/src/view/criteria-info-view/CriteriaInfoView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/view/criteria-info-view/CriteriaInfoView.tsx b/client/src/view/criteria-info-view/CriteriaInfoView.tsx index fbc99ee08..8998395e6 100644 --- a/client/src/view/criteria-info-view/CriteriaInfoView.tsx +++ b/client/src/view/criteria-info-view/CriteriaInfoView.tsx @@ -198,7 +198,8 @@ function CriteriaInfoView(): JSX.Element { {Object.entries(information.averages).map(([identifier, info]) => ( {identifier} - {`${info.value} / ${info.value}`} + {`${Math.round(info.value * 100) / + 100} / ${info.total}`} ))} From 18e4170e54947b4331d24bcf4d030381b5489e73 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 11:50:13 +0100 Subject: [PATCH 13/19] Change getStudentOrReject to make more use of parallelism. --- .../src/services/student-service/StudentService.class.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/services/student-service/StudentService.class.ts b/server/src/services/student-service/StudentService.class.ts index df60d38ea..8220d1065 100644 --- a/server/src/services/student-service/StudentService.class.ts +++ b/server/src/services/student-service/StudentService.class.ts @@ -25,6 +25,7 @@ import scheinexamService from '../scheinexam-service/ScheinexamService.class'; import sheetService from '../sheet-service/SheetService.class'; import teamService from '../team-service/TeamService.class'; import tutorialService from '../tutorial-service/TutorialService.class'; +import { TeamDocument } from '../../model/documents/TeamDocument'; class StudentService { public async getAllStudents(): Promise { @@ -253,8 +254,10 @@ class StudentService { } } - const team = await student.getTeam(); - const points: Student['points'] = (await student.getPoints()).toDTO(); + const [team, points] = await Promise.all([ + student.getTeam(), + student.getPoints(), + ]); return { id, @@ -267,7 +270,7 @@ class StudentService { team: team ? { id: team.id, teamNo: team.teamNo } : undefined, status, attendance: parsedAttendances, - points, + points: points.toDTO(), presentationPoints: presentationPoints ? presentationPoints.toObject({ flattenMaps: true }) : {}, From 33bbd79c2fa699e33cb5f56da5d4cb77ba58722e Mon Sep 17 00:00:00 2001 From: Dudrie Date: Fri, 24 Jan 2020 12:02:50 +0100 Subject: [PATCH 14/19] Add ascending sorting to distribution data. --- .../criteria-info-view/CriteriaInfoView.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/src/view/criteria-info-view/CriteriaInfoView.tsx b/client/src/view/criteria-info-view/CriteriaInfoView.tsx index 8998395e6..840deb0c4 100644 --- a/client/src/view/criteria-info-view/CriteriaInfoView.tsx +++ b/client/src/view/criteria-info-view/CriteriaInfoView.tsx @@ -167,14 +167,16 @@ function CriteriaInfoView(): JSX.Element { chartType='ColumnChart' data={[ ['Punkte', 'Anzahl', { role: 'style' }, { role: 'annotation' }], - ...Object.entries(information.distribution).map(([key, info]) => [ - key, - info.value, - info.aboveThreshhold - ? theme.palette.green.dark - : theme.palette.red.dark, - info.value, - ]), + ...Object.entries(information.distribution) + .sort((a, b) => Number.parseInt(a[0]) - Number.parseInt(b[0])) + .map(([key, info]) => [ + key, + info.value, + info.aboveThreshhold + ? theme.palette.green.dark + : theme.palette.red.dark, + info.value, + ]), ]} options={{ slices: { From 09ef7c8d1688695da6f20ae7c16c502dc2fef17d Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 25 Jan 2020 14:23:39 +0100 Subject: [PATCH 15/19] Fix wrong comparison in attendance criteria. --- .../src/model/scheincriteria/criterias/AttendanceCriteria.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts index fd9d86a96..ad0f4edff 100644 --- a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts +++ b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts @@ -28,8 +28,8 @@ export class AttendanceCriteria extends PossiblePercentageCriteria { }); const passed: boolean = this.percentage - ? visitedOrExcused / total > this.valueNeeded - : visitedOrExcused > this.valueNeeded; + ? visitedOrExcused / total >= this.valueNeeded + : visitedOrExcused >= this.valueNeeded; return { identifier: this.identifier, From d1fe30e439f7a4acfa488c83af0564cbad841a6a Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 25 Jan 2020 14:32:11 +0100 Subject: [PATCH 16/19] Fix wrong calculation of attendance criteria. --- server/src/model/scheincriteria/criterias/AttendanceCriteria.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts index ad0f4edff..b5fb0228d 100644 --- a/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts +++ b/server/src/model/scheincriteria/criterias/AttendanceCriteria.ts @@ -21,7 +21,7 @@ export class AttendanceCriteria extends PossiblePercentageCriteria { let visitedOrExcused = 0; - Object.values(student.attendance).forEach(({ state }) => { + student.attendance.forEach(({ state }) => { if (state === AttendanceState.PRESENT || state === AttendanceState.EXCUSED) { visitedOrExcused += 1; } From 1e77b47ae484796b6d62c102393cc8c7c4ca0a98 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Sat, 25 Jan 2020 14:32:36 +0100 Subject: [PATCH 17/19] Change CriteriaCharts to have labeled legends & values as text. --- .../student-info/components/CriteriaCharts.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx b/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx index c54ef8944..028e492a3 100644 --- a/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx +++ b/client/src/view/studentmanagement/student-info/components/CriteriaCharts.tsx @@ -28,6 +28,10 @@ function CriteriaCharts({ scheinStatus, ...props }: Props): JSX.Element { 0: { color: theme.palette.green.dark }, 1: { color: theme.palette.red.dark }, }, + legend: { + position: 'labeled', + }, + pieSliceText: 'value', }} /> From 0a2016efcacc4215775878dee2b57e6ed677b282 Mon Sep 17 00:00:00 2001 From: Dudrie Date: Mon, 27 Jan 2020 19:28:31 +0100 Subject: [PATCH 18/19] Remove schein criteria bars from student overview. Those bars might be the issue for the low performance of the app in production. --- .../AllStudentsAdminView.tsx | 13 +- .../TutorStudentmanagement.tsx | 30 +-- .../student-overview/Studentoverview.tsx | 3 - .../components/ExtendableStudentRow.tsx | 253 ------------------ .../components/StudentRow.tsx | 38 +-- 5 files changed, 17 insertions(+), 320 deletions(-) delete mode 100644 client/src/view/studentmanagement/student-overview/components/ExtendableStudentRow.tsx diff --git a/client/src/view/studentmanagement/AllStudentsAdminView.tsx b/client/src/view/studentmanagement/AllStudentsAdminView.tsx index 84c4ed2bf..a954c6e8b 100644 --- a/client/src/view/studentmanagement/AllStudentsAdminView.tsx +++ b/client/src/view/studentmanagement/AllStudentsAdminView.tsx @@ -8,7 +8,6 @@ import { useSnackbar } from 'notistack'; import React, { useEffect, useState } from 'react'; import { Attendance } from 'shared/dist/model/Attendance'; import { PointMap } from 'shared/dist/model/Points'; -import { ScheinCriteriaSummary } from 'shared/dist/model/ScheinCriteria'; import { Tutorial } from 'shared/dist/model/Tutorial'; import SubmitButton from '../../components/loading/SubmitButton'; import { getScheinStatusPDF } from '../../hooks/fetching/Files'; @@ -49,20 +48,11 @@ function AdminStudentManagement(): JSX.Element { const [isCreatingCSVFile, setCreatingCSVFile] = useState(false); const [isCreatingScheinStatus, setCreatingScheinStatus] = useState(false); const [tutorials, setTutorials] = useState([]); - const [summaries, setSummaries] = useState<{ [studentId: string]: ScheinCriteriaSummary }>({}); const [{ students }] = useStudentStore(); const { enqueueSnackbar } = useSnackbar(); useEffect(() => { - getScheinCriteriaSummaryOfAllStudents() - .then(response => setSummaries(response)) - .catch(() => - enqueueSnackbar('Konnte Ergebnisse der Scheinkriterien nicht abrufen.', { - variant: 'error', - }) - ); - getAllTutorials().then(response => setTutorials(response)); }, [enqueueSnackbar]); @@ -82,6 +72,8 @@ function AdminStudentManagement(): JSX.Element { async function generateCSVFile() { setCreatingCSVFile(true); + + const summaries = await getScheinCriteriaSummaryOfAllStudents(); const dataArray: Row[] = []; for (const { @@ -137,7 +129,6 @@ function AdminStudentManagement(): JSX.Element { return ( ; +type PropType = RouteComponentProps; function unifyFilterableText(text: string): string { return _.deburr(text).toLowerCase(); @@ -53,32 +50,15 @@ export function getFilteredStudents( }); } -function TutorStudentmanagement({ match: { params }, enqueueSnackbar }: PropType): JSX.Element { +function TutorStudentmanagement({ match: { params } }: PropType): JSX.Element { const classes = useStyles(); - - const [summaries, setSummaries] = useState<{ [studentId: string]: ScheinCriteriaSummary }>({}); - const tutorialId: string = params.tutorialId; - useEffect(() => { - (async function() { - getScheinCriteriaSummariesOfAllStudentsOfTutorial(tutorialId) - .then(response => setSummaries(response)) - .catch(() => { - enqueueSnackbar('Konnte Ergebnisse der Scheinkriterien nicht abrufen.', { - variant: 'error', - }); - - return []; - }); - })(); - }, [tutorialId, enqueueSnackbar]); - return ( -
{}
+
{}
); } -export default withRouter(withSnackbar(TutorStudentmanagement)); +export default withRouter(TutorStudentmanagement); diff --git a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx index fba59bb32..d14771e37 100644 --- a/client/src/view/studentmanagement/student-overview/Studentoverview.tsx +++ b/client/src/view/studentmanagement/student-overview/Studentoverview.tsx @@ -48,14 +48,12 @@ type SummariesByStudent = { [studentId: string]: ScheinCriteriaSummary }; interface Props { tutorials?: Tutorial[]; - summaries: SummariesByStudent; allowChangeTutorial?: boolean; additionalTopBarItem?: React.ReactNode; } function Studentoverview({ tutorials, - summaries, allowChangeTutorial, additionalTopBarItem, }: Props): JSX.Element { @@ -189,7 +187,6 @@ function Studentoverview({ - createStyles({ - root: { - flexGrow: 1, - }, - statusProgress: { - maxWidth: '200px', - // width: '50%', - margin: theme.spacing(1), - }, - content: { - '&:hover': { - background: 'rgba(0, 0, 0, 0.1)', - cursor: 'pointer', - }, - '& td': { - // This prevents table-cells from getting too small if other cells take up more space. - whiteSpace: 'nowrap', - '&:nth-last-of-type(2)': { - width: '100%', - }, - }, - }, - infoBlock: { - display: 'grid', - gridRowGap: theme.spacing(1), - gridColumnGap: theme.spacing(2), - gridTemplateColumns: 'max-content 1fr', - }, - noBottomBorder: { - '& > td': { - borderBottom: 'none', - }, - }, - showInfoIcon: { - transition: theme.transitions.create('transform', { - easing: theme.transitions.easing.easeInOut, - duration: theme.transitions.duration.shorter, - }), - transform: 'rotate(0deg)', - }, - showInfoIconOpened: { - transform: 'rotate(-180deg)', - }, - infoRowCell: { - paddingTop: theme.spacing(0), - }, - infoRowContent: { - maxWidth: '95%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - marginLeft: 'auto', - marginRight: 'auto', - }, - infoRowTable: { - width: 'unset', - }, - progressBarCell: { - width: '100%', - }, - emailButton: { - paddingRight: theme.spacing(1), - }, - emailIcon: { - marginLeft: theme.spacing(1), - }, - }) -); - -interface Props extends TableRowProps { - student: StudentWithFetchedTeam; - showTutorial?: boolean; - tutorials?: Tutorial[]; - summary?: ScheinCriteriaSummary; - onEditStudentClicked: (student: StudentWithFetchedTeam) => void; - onDeleteStudentClicked: (student: StudentWithFetchedTeam) => void; - onChangeTutorialClicked?: (student: StudentWithFetchedTeam) => void; -} - -interface CustomAvatarProps { - icon?: SvgIconComponent; - AvatarProps?: AvatarProps; - avatarTooltip?: string; -} - -function calculateProgress(summary: ScheinCriteriaSummary) { - let achieved = 0; - let total = 0; - - Object.values(summary.scheinCriteriaSummary).forEach(status => { - const infos = Object.values(status.infos); - - if (status.passed) { - achieved += 1; - total += 1; - } else { - if (infos.length > 0) { - infos.forEach(info => { - achieved += info.achieved / info.total; - total += 1; - }); - } else { - achieved += status.achieved / status.total; - total += 1; - } - } - }); - - return (achieved / total) * 100; -} - -function ExtendableStudentRow({ - student, - className, - onClick, - showTutorial, - tutorials, - summary, - onEditStudentClicked, - onDeleteStudentClicked, - onChangeTutorialClicked, - ...rest -}: Props): JSX.Element { - const { firstname, lastname, team } = student; - const classes = useStyles(); - - const [showInfoBox, setShowInfoBox] = useState(false); - - const tutorial = tutorials && tutorials.find(t => t.id === student.tutorial); - const additionalMenuItems: ListItem[] = [ - { - primary: 'E-Mail', - Icon: MailIcon, - disabled: !student.email, - onClick: () => { - window.location.href = `mailto:${student.email}`; - }, - }, - ]; - - if (onChangeTutorialClicked) { - additionalMenuItems.push({ - primary: 'Tutorium wechseln', - onClick: () => onChangeTutorialClicked(student), - Icon: AccountSwitch, - }); - } - - return ( - <> - } - className={clsx(classes.content, className, showInfoBox && classes.noBottomBorder)} - onClick={() => { - setShowInfoBox(!showInfoBox); - }} - buttonCellContent={ - <> - { - e.stopPropagation(); - setShowInfoBox(!showInfoBox); - }} - > - - - onEditStudentClicked(student)} - onDeleteClicked={() => onDeleteStudentClicked(student)} - stopClickPropagation - additionalItems={additionalMenuItems} - /> - - } - > - - - - - - - - - - {showInfoBox && summary && ( - - -
- -
-
-
- )} - - ); -} - -export default ExtendableStudentRow; diff --git a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx b/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx index 363d9dc5e..c4c7a0264 100644 --- a/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx +++ b/client/src/view/studentmanagement/student-overview/components/StudentRow.tsx @@ -1,23 +1,20 @@ -import React from 'react'; -import PaperTableRow, { PaperTableRowProps } from '../../../../components/PaperTableRow'; -import { Student, TeamInStudent } from 'shared/dist/model/Student'; -import { getNameOfEntity } from 'shared/dist/util/helpers'; -import StudentAvatar from '../../../../components/student-icon/StudentAvatar'; -import EntityListItemMenu from '../../../../components/list-item-menu/EntityListItemMenu'; -import { ScheinCriteriaSummary } from 'shared/dist/model/ScheinCriteria'; -import { TableCell, Button } from '@material-ui/core'; +import { Button, TableCell } from '@material-ui/core'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; import { + AccountSwitch as ChangeTutorialIcon, InformationOutline as InfoIcon, Mail as MailIcon, - AccountSwitch as ChangeTutorialIcon, } from 'mdi-material-ui'; -import StatusProgress from '../../../management/components/StatusProgress'; -import { calculateProgress } from './StudenRow.helpers'; +import React from 'react'; import { Link } from 'react-router-dom'; +import { Student, TeamInStudent } from 'shared/dist/model/Student'; +import { getNameOfEntity } from 'shared/dist/util/helpers'; +import EntityListItemMenu from '../../../../components/list-item-menu/EntityListItemMenu'; +import { ListItem } from '../../../../components/list-item-menu/ListItemMenu'; +import PaperTableRow, { PaperTableRowProps } from '../../../../components/PaperTableRow'; +import StudentAvatar from '../../../../components/student-icon/StudentAvatar'; import { getStudentInfoPath } from '../../../../routes/Routing.helpers'; -import { makeStyles, createStyles } from '@material-ui/core/styles'; import { useStudentStore } from '../../student-store/StudentStore'; -import { ListItem } from '../../../../components/list-item-menu/ListItemMenu'; const useStyles = makeStyles(theme => createStyles({ @@ -38,7 +35,6 @@ type StudentCallback = (student: Student) => void; interface Props extends PaperTableRowProps { student: Student; - criteriaSummary?: ScheinCriteriaSummary; onEdit: StudentCallback; onDelete: StudentCallback; onChangeTutorial?: StudentCallback; @@ -62,7 +58,6 @@ function getSubtext({ team, prefix }: GetSubtextParams): string { function StudentRow({ student, - criteriaSummary, subtextPrefix, className, onEdit, @@ -107,19 +102,6 @@ function StudentRow({ /> } > - - - -