Skip to content

Commit 9af235a

Browse files
authored
Merge pull request #1018 from nk-coding/feature/student_csv_import
Feature/student csv import
2 parents 606291e + 6900d5f commit 9af235a

24 files changed

Lines changed: 933 additions & 88 deletions

File tree

client/src/components/forms/StudentForm.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FormikHelpers } from 'formik';
2-
import React, { useMemo, useRef } from 'react';
3-
import { IStudentDTO, StudentStatus } from 'shared/model/Student';
2+
import { useMemo, useRef } from 'react';
3+
import { ICreateStudentDTO, StudentStatus } from 'shared/model/Student';
44
import * as Yup from 'yup';
55
import { useSettings } from '../../hooks/useSettings';
66
import { Student } from '../../model/Student';
@@ -72,7 +72,10 @@ interface InitialStateParams {
7272
export const CREATE_NEW_TEAM_VALUE = 'CREATE_NEW_TEAM_ACTION';
7373
type ItemType = Team | { type: typeof CREATE_NEW_TEAM_VALUE };
7474

75-
export function convertFormStateToDTO(values: StudentFormState, tutorialId: string): IStudentDTO {
75+
export function convertFormStateToDTO(
76+
values: StudentFormState,
77+
tutorialId: string
78+
): ICreateStudentDTO {
7679
const { firstname, lastname, iliasName, matriculationNo, email, courseOfStudies, team, status } =
7780
values;
7881

client/src/hooks/fetching/Student.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { plainToClass } from 'class-transformer';
22
import { IAttendance, IAttendanceDTO } from 'shared/model/Attendance';
33
import { IGradingDTO, IPresentationPointsDTO } from 'shared/model/Gradings';
4-
import { ICakeCountDTO, IStudent, IStudentDTO } from 'shared/model/Student';
4+
import {
5+
ICakeCountDTO,
6+
ICreateStudentDTO,
7+
ICreateStudentsDTO,
8+
IStudent,
9+
} from 'shared/model/Student';
510
import { sortByName } from 'shared/util/helpers';
611
import { Student } from '../../model/Student';
712
import axios from './Axios';
@@ -27,7 +32,7 @@ export async function getStudent(studentId: string): Promise<Student> {
2732
return Promise.reject(`Wrong response code (${response.status}).`);
2833
}
2934

30-
export async function createStudent(studentInfo: IStudentDTO): Promise<Student> {
35+
export async function createStudent(studentInfo: ICreateStudentDTO): Promise<Student> {
3136
const response = await axios.post<IStudent>('student', studentInfo);
3237

3338
if (response.status === 201) {
@@ -37,7 +42,17 @@ export async function createStudent(studentInfo: IStudentDTO): Promise<Student>
3742
return Promise.reject(`Wrong response code (${response.status}).`);
3843
}
3944

40-
export async function editStudent(id: string, studentInfo: IStudentDTO): Promise<Student> {
45+
export async function createManyStudents(dto: ICreateStudentsDTO): Promise<IStudent[]> {
46+
const response = await axios.post<IStudent[]>('student/generate', dto);
47+
48+
if (response.status === 201) {
49+
return response.data;
50+
}
51+
52+
return Promise.reject(`Wrong response code (${response.status}).`);
53+
}
54+
55+
export async function editStudent(id: string, studentInfo: ICreateStudentDTO): Promise<Student> {
4156
const response = await axios.patch<IStudent>(`student/${id}`, studentInfo);
4257

4358
if (response.status === 200) {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useMemo } from 'react';
2+
import { useParams } from 'react-router';
3+
import ImportCSV from '../../components/import-csv/components/ImportCSV';
4+
import MapCSVColumns from '../../components/import-csv/components/map-form/MapCSVColumns';
5+
import { CSVImportProvider } from '../../components/import-csv/ImportCSV.context';
6+
import { CSVMapColumsMetadata } from '../../components/import-csv/ImportCSV.types';
7+
import StepperWithButtons from '../../components/stepper-with-buttons/StepperWithButtons';
8+
import { ROUTES } from '../../routes/Routing.routes';
9+
import AdjustImportedStudentDataForm from './adjust-data-form/AdjustImportedStudentDataForm';
10+
11+
export type StudentColumns =
12+
| 'firstname'
13+
| 'lastname'
14+
| 'status'
15+
| 'team'
16+
| 'email'
17+
| 'iliasName'
18+
| 'matriculationNo'
19+
| 'courseOfStudies';
20+
type ColumnGroups = 'studentInformation';
21+
22+
interface Params {
23+
tutorialId: string;
24+
}
25+
26+
function ImportStudents(): JSX.Element {
27+
const { tutorialId } = useParams<Params>();
28+
const groupMetadata: CSVMapColumsMetadata<StudentColumns, ColumnGroups> = useMemo(
29+
() => ({
30+
information: {
31+
firstname: {
32+
label: 'Vorname',
33+
headersToAutoMap: ['Vorname'],
34+
group: 'studentInformation',
35+
required: true,
36+
},
37+
lastname: {
38+
label: 'Nachname',
39+
headersToAutoMap: ['Nachname', 'Name'],
40+
group: 'studentInformation',
41+
required: true,
42+
},
43+
status: {
44+
label: 'Status',
45+
headersToAutoMap: ['Status'],
46+
group: 'studentInformation',
47+
required: false,
48+
},
49+
team: {
50+
label: 'Team',
51+
headersToAutoMap: ['Team'],
52+
group: 'studentInformation',
53+
required: false,
54+
},
55+
email: {
56+
label: 'E-Mailadresse',
57+
headersToAutoMap: ['E-Mail'],
58+
group: 'studentInformation',
59+
required: false,
60+
},
61+
iliasName: {
62+
label: 'Ilias-Name',
63+
headersToAutoMap: ['Ilias', 'Ilias-Name', 'IliasName'],
64+
group: 'studentInformation',
65+
required: false,
66+
},
67+
matriculationNo: {
68+
label: 'Matrikelnummer',
69+
headersToAutoMap: ['Matrikelnummer'],
70+
group: 'studentInformation',
71+
required: false,
72+
},
73+
courseOfStudies: {
74+
label: 'Studiengang',
75+
headersToAutoMap: ['Studiengang'],
76+
group: 'studentInformation',
77+
required: false,
78+
},
79+
},
80+
groups: {
81+
studentInformation: { name: 'Studierendenformationen', index: 0 },
82+
},
83+
}),
84+
[]
85+
);
86+
87+
return (
88+
<CSVImportProvider groupMetadata={groupMetadata}>
89+
<StepperWithButtons
90+
steps={[
91+
{ label: 'CSV importieren', component: <ImportCSV /> },
92+
{ label: 'Spalten zuordnen', component: <MapCSVColumns /> },
93+
{
94+
label: 'Studierende importieren',
95+
component: <AdjustImportedStudentDataForm tutorialId={tutorialId} />,
96+
},
97+
]}
98+
alternativeLabel={false}
99+
backButtonLabel='Zurück'
100+
nextButtonLabel='Weiter'
101+
nextButtonDoneLabel='Fertigstellen'
102+
backButtonRoute={ROUTES.STUDENTOVERVIEW.create({ tutorialId })}
103+
routeAfterLastStep={{ route: ROUTES.STUDENTOVERVIEW, params: { tutorialId } }}
104+
/>
105+
</CSVImportProvider>
106+
);
107+
}
108+
109+
export default ImportStudents;
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { Box } from '@material-ui/core';
2+
import { Formik, useFormikContext } from 'formik';
3+
import { useSnackbar } from 'notistack';
4+
import { useEffect, useMemo } from 'react';
5+
import { useHistory } from 'react-router';
6+
import { ICreateStudentsDTO, IStudent, StudentStatus } from 'shared/model/Student';
7+
import FormikDebugDisplay from '../../../components/forms/components/FormikDebugDisplay';
8+
import { useImportCSVContext } from '../../../components/import-csv/ImportCSV.context';
9+
import {
10+
NextStepInformation,
11+
useStepper,
12+
} from '../../../components/stepper-with-buttons/context/StepperContext';
13+
import { createManyStudents } from '../../../hooks/fetching/Student';
14+
import { useCustomSnackbar } from '../../../hooks/snackbar/useCustomSnackbar';
15+
import { FormikSubmitCallback } from '../../../types';
16+
import { StudentColumns } from '../ImportStudents';
17+
import StudentDataBox from './components/StudentDataBox';
18+
import { convertCSVDataToFormData } from './components/StudentDataBox.helpers';
19+
20+
export interface StudentFormStateValue {
21+
rowNr: number;
22+
firstname: string;
23+
lastname: string;
24+
email?: string;
25+
status: StudentStatus;
26+
team?: string;
27+
iliasName?: string;
28+
matriculationNo?: string;
29+
courseOfStudies?: string;
30+
}
31+
32+
export interface StudentFormState {
33+
[id: string]: StudentFormStateValue;
34+
}
35+
36+
interface Props {
37+
tutorialId: string;
38+
}
39+
40+
function convertValuesToDTO(values: StudentFormState, tutorialId: string): ICreateStudentsDTO {
41+
return {
42+
tutorial: tutorialId,
43+
students: Object.values(values),
44+
};
45+
}
46+
47+
function AdjustImportedUserDataFormContent(): JSX.Element {
48+
const { setNextCallback, removeNextCallback } = useStepper();
49+
const { values, isValid, validateForm, submitForm } = useFormikContext<StudentFormState>();
50+
const { enqueueSnackbar } = useSnackbar();
51+
const history = useHistory();
52+
53+
useEffect(() => {
54+
setNextCallback(async (): Promise<NextStepInformation> => {
55+
const errors = await validateForm();
56+
57+
if (Object.entries(errors).length > 0) {
58+
enqueueSnackbar('Nutzerdaten sind ungültig.', { variant: 'error' });
59+
return { goToNext: false, error: true };
60+
}
61+
62+
const isSuccess: any = await submitForm();
63+
64+
if (!!isSuccess) {
65+
return { goToNext: true };
66+
} else {
67+
return { goToNext: false, error: true };
68+
}
69+
});
70+
71+
return () => removeNextCallback();
72+
}, [
73+
setNextCallback,
74+
removeNextCallback,
75+
isValid,
76+
values,
77+
enqueueSnackbar,
78+
history,
79+
submitForm,
80+
validateForm,
81+
]);
82+
83+
return (
84+
<Box display='flex' flex={1}>
85+
<StudentDataBox />
86+
87+
<FormikDebugDisplay showErrors />
88+
</Box>
89+
);
90+
}
91+
92+
function AdjustImportedStudentDataForm({ tutorialId }: Props): JSX.Element {
93+
const {
94+
csvData,
95+
mapColumnsHelpers: { mappedColumns },
96+
} = useImportCSVContext<StudentColumns, string>();
97+
const { enqueueSnackbar, enqueueSnackbarWithList } = useCustomSnackbar();
98+
99+
const initialValues: StudentFormState = useMemo(() => {
100+
return convertCSVDataToFormData({
101+
data: csvData,
102+
values: mappedColumns,
103+
});
104+
}, [csvData, mappedColumns]);
105+
106+
const handleSubmit: FormikSubmitCallback<StudentFormState> = async (values) => {
107+
const dto: ICreateStudentsDTO = convertValuesToDTO(values, tutorialId);
108+
109+
try {
110+
const response: IStudent[] = await createManyStudents(dto);
111+
112+
enqueueSnackbar(`${response.length} Studierende wurden erstellt.`, {
113+
variant: 'success',
114+
});
115+
return true;
116+
} catch (errors) {
117+
if (Array.isArray(errors)) {
118+
enqueueSnackbarWithList({
119+
title: 'Studierende konnten nicht erstellt werden.',
120+
textBeforeList:
121+
'Da bei einigen Studierenden ein Fehler aufgetreten ist, wurde kein/e Studierende erstellt. Folgende Studierende konnte nicht erstellt werden:',
122+
items: errors,
123+
variant: 'error',
124+
});
125+
} else {
126+
enqueueSnackbar(`Es konnten keine Studierenden erstellt werden.`, {
127+
variant: 'error',
128+
});
129+
}
130+
return false;
131+
}
132+
};
133+
134+
return (
135+
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
136+
<AdjustImportedUserDataFormContent />
137+
</Formik>
138+
);
139+
}
140+
141+
export default AdjustImportedStudentDataForm;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { StudentStatus } from 'shared/model/Student';
2+
import { CSVData, CSVMappedColumns } from '../../../../components/import-csv/ImportCSV.types';
3+
import { StudentColumns } from '../../ImportStudents';
4+
import { StudentFormState } from '../AdjustImportedStudentDataForm';
5+
6+
interface ConversionParams {
7+
data: CSVData;
8+
values: CSVMappedColumns<StudentColumns>;
9+
}
10+
11+
function convertColumnToStatus(statusData?: string): StudentStatus {
12+
const sanitized = statusData?.trim()?.toUpperCase();
13+
const status = Object.values(StudentStatus).find((status) => sanitized === status.toString());
14+
return status ?? StudentStatus.ACTIVE;
15+
}
16+
17+
export function convertCSVDataToFormData(params: ConversionParams): StudentFormState {
18+
const { data, values } = params;
19+
const emptyString = 'N/A';
20+
21+
const userFormState: StudentFormState = {};
22+
data.rows.forEach(({ rowNr, data }) => {
23+
const key = rowNr.toString();
24+
userFormState[key] = {
25+
rowNr,
26+
firstname: data[values.firstname as string]?.trim() || emptyString,
27+
lastname: data[values.lastname as string]?.trim() || emptyString,
28+
email: data[values.email as string]?.trim() || undefined,
29+
status: convertColumnToStatus(data[values.status as string]),
30+
team: data[values.team as string]?.trim(),
31+
iliasName: data[values.iliasName as string]?.trim() || undefined,
32+
matriculationNo: data[values.matriculationNo as string] || undefined,
33+
courseOfStudies: data[values.courseOfStudies as string] || undefined,
34+
};
35+
});
36+
37+
return userFormState;
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Typography } from '@material-ui/core';
2+
import { useFormikContext } from 'formik';
3+
import { useMemo } from 'react';
4+
import { getNameOfEntity } from 'shared/util/helpers';
5+
import OutlinedBox from '../../../../components/OutlinedBox';
6+
import TableWithPadding from '../../../../components/TableWithPadding';
7+
import { StudentFormState } from '../AdjustImportedStudentDataForm';
8+
import StudentDataRow from './StudentDataRow';
9+
10+
function StudentDataBox(): JSX.Element {
11+
const { values } = useFormikContext<StudentFormState>();
12+
const users = useMemo(
13+
() =>
14+
Object.values(values).sort((a, b) => getNameOfEntity(a).localeCompare(getNameOfEntity(b))),
15+
[values]
16+
);
17+
18+
return (
19+
<OutlinedBox flex={1}>
20+
<Typography>Studierendendaten festlegen</Typography>
21+
22+
<TableWithPadding
23+
items={users}
24+
createRowFromItem={(item) => <StudentDataRow name={`${item.rowNr}`} />}
25+
BoxProps={{ marginTop: 2 }}
26+
/>
27+
</OutlinedBox>
28+
);
29+
}
30+
31+
export default StudentDataBox;

0 commit comments

Comments
 (0)