From ef60fe12b3fd59c9c52269565a0e1de9dd8bf12f Mon Sep 17 00:00:00 2001 From: BenjaVR Date: Thu, 21 Feb 2019 00:06:38 +0100 Subject: [PATCH] Added modal with information about internship in a specified period - Changed date formats so they are all the same. --- .../planning/PlanningsFormModal.tsx | 2 + .../schools/SchoolInternshipSummaryModal.tsx | 299 ++++++++++++++++++ src/components/schools/SchoolsPage.tsx | 18 ++ src/components/schools/SchoolsTable.tsx | 77 ++++- src/components/students/StudentsTable.tsx | 64 ++-- src/models/Student.ts | 28 ++ .../repositories/StudentsRepository.ts | 15 +- 7 files changed, 459 insertions(+), 44 deletions(-) create mode 100644 src/components/schools/SchoolInternshipSummaryModal.tsx diff --git a/src/components/planning/PlanningsFormModal.tsx b/src/components/planning/PlanningsFormModal.tsx index b9f81a8..d426799 100644 --- a/src/components/planning/PlanningsFormModal.tsx +++ b/src/components/planning/PlanningsFormModal.tsx @@ -100,6 +100,7 @@ class PlanningsFormModal extends React.Component void; +} + +type SchoolInternshipSummaryModalProps = ISchoolInternshipSummaryModalProps & FormComponentProps; + +interface ISchoolInternshipSummaryModalState { + studentsWithInternship: Student[]; + areStudentsLoading: boolean; + selectedStartDate: moment.Moment | undefined; + selectedEndDate: moment.Moment | undefined; +} + +class SchoolInternshipSummaryModal extends React.Component { + + constructor(props: SchoolInternshipSummaryModalProps) { + super(props); + + this.state = { + studentsWithInternship: [], + areStudentsLoading: false, + selectedStartDate: undefined, + selectedEndDate: undefined, + }; + + this.renderStudentsFullyInPeriod = this.renderStudentsFullyInPeriod.bind(this); + this.renderStudentsNotFullyInPeriod = this.renderStudentsNotFullyInPeriod.bind(this); + this.renderStudentFullyInPeriod = this.renderStudentFullyInPeriod.bind(this); + this.renderStudentNotFullyInPeriod = this.renderStudentNotFullyInPeriod.bind(this); + this.renderHoursInformationalTooltip = this.renderHoursInformationalTooltip.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleStartDateChange = this.handleStartDateChange.bind(this); + this.handleEndDateChange = this.handleEndDateChange.bind(this); + this.handleDateChanged = this.handleDateChanged.bind(this); + } + + public componentWillReceiveProps(nextProps: SchoolInternshipSummaryModalProps): void { + if (nextProps.isVisible && !this.props.isVisible) { + this.setState({ + studentsWithInternship: [], + areStudentsLoading: false, + }); + + // Remember the last searched date, to be able to quickly check the same period for multiple schools. + if (this.state.selectedStartDate !== undefined && this.state.selectedEndDate) { + this.props.form.getFieldDecorator(nameof("selectedStartDate"), { initialValue: this.state.selectedStartDate }); + this.props.form.getFieldDecorator(nameof("selectedEndDate"), { initialValue: this.state.selectedEndDate }); + } + + if (nextProps.selectedSchool !== undefined) { + this.setState({ areStudentsLoading: true }); + StudentsRepository.getPlannedStudentsForSchool(nextProps.selectedSchool) + .then((students) => { + this.setState({ + studentsWithInternship: students, + areStudentsLoading: false, + }); + }); + } + } + } + + public render(): React.ReactNode { + const studentsFullyInPeriod = this.state.studentsWithInternship.filter((student) => { + return this.state.selectedStartDate !== undefined && this.state.selectedEndDate && student.internship !== undefined + && student.internship.startDate.isSameOrAfter(this.state.selectedStartDate) + && student.internship.endDate.isSameOrBefore(this.state.selectedEndDate); + }); + const studentsNotFullyInPeriod = this.state.studentsWithInternship.filter((student) => { + return this.state.selectedStartDate !== undefined && this.state.selectedEndDate && student.internship !== undefined + && (student.internship.startDate.isBefore(this.state.selectedStartDate) + || student.internship.endDate.isAfter(this.state.selectedEndDate)); + }); + + const totalStudentsCount = studentsFullyInPeriod.length + studentsNotFullyInPeriod.length; + const totalInternshipDaysCount = [...studentsFullyInPeriod, ...studentsNotFullyInPeriod] + .map((student) => student.getInternshipNumberOfDaysInRange(this.state.selectedStartDate, this.state.selectedEndDate)) + .reduce((sum, days) => sum + days, 0); + + const totalInternshipHoursCount = [...studentsFullyInPeriod, ...studentsNotFullyInPeriod] + .map((student) => student.getInternshipNumberOfHoursInRange(this.state.selectedStartDate, this.state.selectedEndDate)) + .reduce((sum, hours) => sum + hours, 0); + + const { getFieldDecorator } = this.props.form; + return ( + + + Selecteer stages tussen volgende datums: +
+ + + + {getFieldDecorator("selectedStartDate")( + , + )} + + + + + {getFieldDecorator("selectedEndDate", { + validateTrigger: "", + rules: [ + { + validator: (_, endDate: moment.Moment | null, callback) => { + const startDate = this.props.form.getFieldValue(nameof("selectedStartDate")); + if (isMomentDayAfterOrTheSameAsOtherDay(endDate || undefined, startDate)) { + return callback(); + } + return callback(false); + }, + message: "Deze datum mag niet eerder dan de \"van\" datum zijn", + }, + ], + })( + , + )} + + + +
+ {this.state.selectedStartDate !== undefined && this.state.selectedEndDate !== undefined && this.state.selectedEndDate.isSameOrAfter(this.state.selectedStartDate) && + + +

+ Totalen voor geselecteerde periode: +

+

+  {totalStudentsCount} {totalStudentsCount === 1 ? "student" : "studenten"}, +  {totalInternshipDaysCount} {totalInternshipDaysCount === 1 ? "stage dag" : "stage dagen"}, +  {totalInternshipHoursCount} {totalInternshipHoursCount === 1 ? "stage uur" : "stage uren "} + +   + +

+
+
+ + {this.renderStudentsFullyInPeriod(studentsFullyInPeriod)} +
+ {this.renderStudentsNotFullyInPeriod(studentsNotFullyInPeriod)} +
+
+ } +
+
+ ); + } + + private renderStudentsFullyInPeriod(students: Student[]): React.ReactNode { + return ( + Stages volledig binnen geselecteerde periode} + locale={{ emptyText: "Geen stages gevonden" }} + dataSource={students} + pagination={undefined} + renderItem={this.renderStudentFullyInPeriod} + /> + ); + } + + private renderStudentsNotFullyInPeriod(students: Student[]): React.ReactNode { + return ( + Stages gedeeltelijk binnen geselecteerde periode} + locale={{ emptyText: "Geen stages gevonden" }} + dataSource={students} + pagination={undefined} + renderItem={this.renderStudentNotFullyInPeriod} + /> + ); + } + + private renderStudentFullyInPeriod(student: Student): React.ReactNode { + const internshipDays = student.getInternshipNumberOfDaysInRange(this.state.selectedStartDate, this.state.selectedEndDate); + const internshipHours = student.getInternshipNumberOfHoursInRange(this.state.selectedStartDate, this.state.selectedEndDate); + return ( + + + + {student.fullName} + + + {student.internship!.startDate.format("DD/MM/YY")} - {student.internship!.endDate.format("DD/MM/YY")} + + + {internshipDays} {internshipDays === 1 ? "dag" : "dagen"} +   + ({internshipHours} {internshipHours === 1 ? "uur" : "uren"}) + + + + ); + } + + private renderStudentNotFullyInPeriod(student: Student): React.ReactNode { + const internshipDays = student.getInternshipNumberOfDaysInRange(this.state.selectedStartDate, this.state.selectedEndDate); + const internshipHours = student.getInternshipNumberOfHoursInRange(this.state.selectedStartDate, this.state.selectedEndDate); + return ( + + + + {student.fullName} + + + {student.internship!.startDate.format("DD/MM/YY")} - {student.internship!.endDate.format("DD/MM/YY")} + + + {internshipDays}/{student.internshipNumberOfDays} {internshipDays === 1 ? "dag" : "dagen"} +   + ({internshipHours}/{student.internship!.hours} {internshipHours === 1 ? "uur" : "uren"}) + + + + ); + } + + private renderHoursInformationalTooltip(): React.ReactNode { + return ( + + Voor studenten die gedeeltelijk in de geselecteerde periode zitten, werden niet alle ingegeven uren meegeteld! + Stel een stage van 10 dagen (50 uren in totaal) die voor 5 dagen in de gekozen periode zit, dan zal deze maar voor 25 uren worden meegeteld. + + ); + } + + private handleStartDateChange(newDate: moment.Moment | null): void { + const newStartDate = newDate || undefined; + this.setState({ selectedStartDate: newStartDate }, () => this.handleDateChanged()); + } + + private handleEndDateChange(newDate: moment.Moment | null): void { + const newEndDate = newDate || undefined; + this.setState({ selectedEndDate: newEndDate }, () => this.handleDateChanged()); + } + + private handleDateChanged(): void { + if (this.state.selectedStartDate === undefined || this.state.selectedEndDate === undefined || this.state.selectedStartDate.isSameOrBefore(this.state.selectedEndDate)) { + // Reset validation messages + this.props.form.setFields({ + [nameof("selectedStartDate")]: { + value: this.props.form.getFieldValue(nameof("selectedStartDate")), + errors: undefined, + }, + [nameof("selectedEndDate")]: { + value: this.props.form.getFieldValue(nameof("selectedEndDate")), + errors: undefined, + }, + }); + } else { + // Validate input + this.props.form.validateFieldsAndScroll([nameof("selectedStartDate"), nameof("selectedEndDate")]); + } + } + + private handleClose(): void { + this.props.handleClose(); + } +} + +const WrappedSchoolInternshipSummaryModal = Form.create()(SchoolInternshipSummaryModal); +export default WrappedSchoolInternshipSummaryModal; diff --git a/src/components/schools/SchoolsPage.tsx b/src/components/schools/SchoolsPage.tsx index 9470d41..bf8a72c 100644 --- a/src/components/schools/SchoolsPage.tsx +++ b/src/components/schools/SchoolsPage.tsx @@ -1,7 +1,11 @@ import { Col, notification, Row } from "antd"; import React from "react"; +import { Department } from "../../models/Department"; +import { Education } from "../../models/Education"; import { School } from "../../models/School"; import { AnyRouteComponentProps } from "../../routes"; +import { DepartmentsRepository } from "../../services/repositories/DepartmentsRepository"; +import { EducationsRepository } from "../../services/repositories/EducationsRepository"; import { SchoolsRepository } from "../../services/repositories/SchoolsRepository"; import SchoolFormModal from "./SchoolsFormModal"; import SchoolsTable from "./SchoolsTable"; @@ -10,6 +14,8 @@ type SchoolsPageProps = AnyRouteComponentProps; interface ISchoolsPageState { schools: School[]; + educations: Education[]; + departments: Department[]; isFetching: boolean; isAddSchoolsModalVisible: boolean; isEditSchoolsModalVisible: boolean; @@ -25,6 +31,8 @@ export default class SchoolsPage extends React.Component { + this.setState({ educations }); + }); + DepartmentsRepository.getDepartmentsByName() + .then((departments) => { + this.setState({ departments }); + }); } public componentWillUnmount(): void { @@ -63,6 +79,8 @@ export default class SchoolsPage extends React.Component Promise; onAddSchoolRequest: () => void; onEditSchoolRequest: (school: School) => void; } -class SchoolsTable extends React.Component { +interface ISchoolsTableState { + isInternshipSummaryModalOpen: boolean; + selectedSchool: School | undefined; +} + +class SchoolsTable extends React.Component { private columns: Array> = [ { @@ -22,6 +32,11 @@ class SchoolsTable extends React.Component { key: "name", sorter: (a, b) => stringSorter(a.name, b.name), }, + { + title: "Stages", + width: 120, + render: (record: School) => this.renderInternshipsAction(record), + }, { title: "Acties", key: "actions", @@ -34,23 +49,47 @@ class SchoolsTable extends React.Component { constructor(props: ISchoolsTableProps) { super(props); + this.state = { + isInternshipSummaryModalOpen: false, + selectedSchool: undefined, + }; + + this.renderInternshipsAction = this.renderInternshipsAction.bind(this); this.renderActions = this.renderActions.bind(this); this.renderTableTitle = this.renderTableTitle.bind(this); + this.handleOpenInternshipSummary = this.handleOpenInternshipSummary.bind(this); + this.handleCloseInternshipSummary = this.handleCloseInternshipSummary.bind(this); } public render(): React.ReactNode { return ( - + +
+ + + ); + } + + private renderInternshipsAction(school: School): React.ReactNode { + const handleOpenInternshipSummaryFn = () => this.handleOpenInternshipSummary(school); + return ( + Bekijk stages ); } @@ -99,6 +138,20 @@ class SchoolsTable extends React.Component { ); } + private handleOpenInternshipSummary(school: School): void { + this.setState({ + isInternshipSummaryModalOpen: true, + selectedSchool: school, + }); + } + + private handleCloseInternshipSummary(): void { + this.setState({ + isInternshipSummaryModalOpen: false, + selectedSchool: undefined, + }); + } + private generateTableRowKey(record: School, index: number): string { return record.id || index.toString(); } diff --git a/src/components/students/StudentsTable.tsx b/src/components/students/StudentsTable.tsx index ee5b4c0..52b7de5 100644 --- a/src/components/students/StudentsTable.tsx +++ b/src/components/students/StudentsTable.tsx @@ -59,6 +59,10 @@ class StudentsTable extends React.Component
- {selectedStudent !== undefined && selectedStudent.internship !== undefined && - - - - - - )} - > -

- Van {selectedStudent.internship.startDate.format("DD/MM/YYYY")} tot en met {selectedStudent.internship.endDate.format("DD/MM/YYYY")} -   - ({selectedStudent.internshipNumberOfDays} {selectedStudent.internshipNumberOfDays === 1 ? "dag" : "dagen"}) - {departmentsNameForStudent !== undefined && - -  in {departmentsNameForStudent} - - } - . -

-
- } + + + + + + )} + destroyOnClose={true} + > +

+ Van {startDate} tot en met {endDate} + {departmentsNameForStudent !== undefined && + +  in {departmentsNameForStudent} + + } + . +

+

+ {internshipNumberOfDays} {internshipNumberOfDays === 1 ? "dag" : "dagen"} + ({internshipNumberOfHours} {internshipNumberOfHours === 1 ? "uur" : "uren"}). +

+
); } @@ -304,7 +309,6 @@ class StudentsTable extends React.Component { return student; } + public getInternshipNumberOfDaysInRange(from: moment.Moment | undefined, until: moment.Moment | undefined): number { + if (this.internship === undefined) { + return 0; + } + + let days = this.internshipNumberOfDays; + if (from !== undefined) { + const diff = Math.max(from.startOf("day").diff(this.internship.startDate.startOf("day"), "day"), 0); + days -= diff; + } + if (until !== undefined) { + const diff = Math.max(this.internship.endDate.startOf("day").diff(until.startOf("day"), "day"), 0); + days -= diff; + } + return days; + } + + public getInternshipNumberOfHoursInRange(from: moment.Moment | undefined, until: moment.Moment | undefined): number { + const totalDays = this.internshipNumberOfDays; + if (totalDays === 0 || this.internship === undefined) { + return 0; + } + const daysInRange = this.getInternshipNumberOfDaysInRange(from, until); + + const percentageOfDays = daysInRange / totalDays; + return Math.round(percentageOfDays * this.internship.hours); + } + protected getEntityInternal(): IStudent { return { firstName: this.firstName, diff --git a/src/services/repositories/StudentsRepository.ts b/src/services/repositories/StudentsRepository.ts index f784027..c6b3c0e 100644 --- a/src/services/repositories/StudentsRepository.ts +++ b/src/services/repositories/StudentsRepository.ts @@ -22,8 +22,7 @@ export class StudentsRepository { public static subscribeToPlannedStudents(onListen: (students: Student[]) => void): () => void { return FirestoreRefs.getStudentCollectionRef() .where(nameof("isPlanned"), "==", true) - .orderBy(nameof("firstName"), "asc") - .orderBy(nameof("lastName"), "asc") + .orderBy(nameof("internship", "startDate"), "asc") .onSnapshot((querySnapshot) => { const studentEntities = FirebaseModelMapper.mapDocsToObjects(querySnapshot.docs); const students = studentEntities.map((entity) => Student.fromEntity(entity)); @@ -73,6 +72,18 @@ export class StudentsRepository { return students; } + public static async getPlannedStudentsForSchool(school: School): Promise { + const querySnapshot = await FirestoreRefs.getStudentCollectionRef() + .where(nameof("schoolId"), "==", school.id) + .where(nameof("isPlanned"), "==", true) + .orderBy(nameof("internship", "startDate"), "asc") + .get(); + + const studentEntities = FirebaseModelMapper.mapDocsToObjects(querySnapshot.docs); + const students = studentEntities.map((entity) => Student.fromEntity(entity)); + return students; + } + public static async addStudent(student: Student): Promise { await FirestoreRefs.getStudentCollectionRef() .add(student.getEntity("new"));