From f71c917c61ccd34a8b2403d6821c071fd960a4e4 Mon Sep 17 00:00:00 2001 From: dominik24c Date: Thu, 17 Nov 2022 10:21:36 +0100 Subject: [PATCH] merge branch and add student_groups table and change logic of endpoint what use group model --- backend/app/coordinator/routes/enrollments.py | 7 +- backend/app/coordinator/routes/groups.py | 17 +- .../coordinator/routes/project_supervisor.py | 3 +- backend/app/coordinator/routes/students.py | 16 +- backend/app/coordinator/routes/year_group.py | 9 +- .../coordinator/schemas/project_supervisor.py | 2 +- backend/app/coordinator/schemas/students.py | 2 +- backend/app/examination_schedule/models.py | 2 +- backend/app/students/models.py | 17 +- backend/app/students/routes/enrollments.py | 21 +- backend/app/students/schemas.py | 7 + backend/migrations/versions/5c3c4c4e1a72_.py | 28 --- .../{a96f91e5b556_.py => 7deb011753b2_.py} | 66 +++--- frontend/.env.example | 1 + frontend/.gitignore | 2 + frontend/package-lock.json | 212 ++++++++++++++++++ frontend/package.json | 2 + frontend/src/api/axiosInstance.ts | 8 +- frontend/src/api/enrollment.ts | 3 +- frontend/src/api/groups.ts | 13 +- frontend/src/api/leaders.ts | 12 +- frontend/src/api/schedule.ts | 69 +++--- frontend/src/api/students.ts | 25 +-- frontend/src/utils/bigCalendarTranslations.ts | 22 ++ frontend/src/views/Login.tsx | 5 + frontend/src/views/coordinator/AddGroup.tsx | 26 ++- .../src/views/coordinator/EditSchedule.tsx | 96 +++++--- frontend/src/views/coordinator/Groups.tsx | 4 +- frontend/src/views/coordinator/Leaders.tsx | 4 +- frontend/src/views/coordinator/Schedule.tsx | 166 ++++++++++---- frontend/src/views/coordinator/Schedules.tsx | 30 ++- frontend/src/views/coordinator/Students.tsx | 9 +- frontend/src/views/student/Enrollment.tsx | 10 +- .../src/views/student/ScheduleAddGroup.tsx | 57 +++-- .../src/views/student/StudentSchedule.tsx | 32 ++- 35 files changed, 709 insertions(+), 296 deletions(-) delete mode 100644 backend/migrations/versions/5c3c4c4e1a72_.py rename backend/migrations/versions/{a96f91e5b556_.py => 7deb011753b2_.py} (93%) create mode 100644 frontend/.env.example create mode 100644 frontend/src/utils/bigCalendarTranslations.ts diff --git a/backend/app/coordinator/routes/enrollments.py b/backend/app/coordinator/routes/enrollments.py index 6a58888..f60c1cf 100644 --- a/backend/app/coordinator/routes/enrollments.py +++ b/backend/app/coordinator/routes/enrollments.py @@ -41,7 +41,7 @@ def create_term_of_defence(examination_schedule_id: int, data: dict) -> dict: start_date = data['start_date'] end_date = data['end_date'] - if not (ex.start_date.timestamp() < start_date.timestamp() and ex.end_date.timestamp() > end_date.timestamp()): + if not (ex.start_date.timestamp() <= start_date.timestamp() and ex.end_date.timestamp() >= end_date.timestamp()): abort(400, "Invalid date range!") if end_date <= start_date: @@ -97,8 +97,7 @@ def update_term_of_defence(examination_schedule_id: int, term_of_defence_id: int start_date = data['start_date'] end_date = data['end_date'] - - if not (ex.start_date.timestamp() < start_date.timestamp() and ex.end_date.timestamp() > end_date.timestamp()): + if not (ex.start_date.timestamp() <= start_date.timestamp() and ex.end_date.timestamp() >= end_date.timestamp()): abort(400, "Invalid date range!") if end_date <= start_date: @@ -110,7 +109,7 @@ def update_term_of_defence(examination_schedule_id: int, term_of_defence_id: int abort(400, "Invalid duration time!") term_of_defence = TermOfDefence.query.filter(TermOfDefence.id != term_of_defence_id, - TermOfDefence.examination_schedule_id == examination_schedule_id). \ + TermOfDefence.examination_schedule_id == examination_schedule_id). \ filter( or_(and_(TermOfDefence.start_date >= start_date, TermOfDefence.start_date < end_date, diff --git a/backend/app/coordinator/routes/groups.py b/backend/app/coordinator/routes/groups.py index a8de18a..16b824f 100644 --- a/backend/app/coordinator/routes/groups.py +++ b/backend/app/coordinator/routes/groups.py @@ -62,10 +62,11 @@ def create_group(year_group_id: int, data: dict) -> dict: group = Group(name=name, project_supervisor_id=project_supervisor_id, year_group_id=year_group_id) - students_without_groups = db.session.query(Student).join(Group, isouter=True) \ - .filter(Group.id.is_(None)).filter(Student.index.in_(students_indexes)).count() - - if students_without_groups != len(students_indexes): + students_without_groups = db.session.query(Student, Group). \ + join(Group, Student.groups). \ + filter(Group.year_group_id == year_group_id). \ + filter(db.or_(*[Student.index == idx for idx in students_indexes])).all() + if len(students_without_groups) > 0: abort(400, "One or more students have already belonged to group!") db.session.add(group) @@ -73,8 +74,7 @@ def create_group(year_group_id: int, data: dict) -> dict: students = db.session.query(Student).filter(Student.index.in_(students_indexes)).all() for student in students: - student.group_id = group.id - + group.students.append(student) db.session.commit() return {"message": "Group was created!"} @@ -96,10 +96,7 @@ def delete_group(id: int) -> dict: if group is None: abort(400, f"Group with id {id} doesn't exist!") - students = db.session.query(Student).filter_by(group_id=id).all() - for student in students: - student.group_id = None - + group.students = [] db.session.delete(group) db.session.commit() return {"message": "Group was deleted!"} diff --git a/backend/app/coordinator/routes/project_supervisor.py b/backend/app/coordinator/routes/project_supervisor.py index b349517..e1d11d8 100644 --- a/backend/app/coordinator/routes/project_supervisor.py +++ b/backend/app/coordinator/routes/project_supervisor.py @@ -92,7 +92,7 @@ def delete_project_supervisor(id: int) -> dict: filter(ProjectSupervisor.id == id).group_by(ProjectSupervisor.id) if count_groups is not None: - abort(400, f"Project Supervisor with id {id} has groups!") + abort(400, "Project Supervisor has at least one group!") db.session.delete(project_supervisor) db.session.commit() @@ -147,7 +147,6 @@ def add_project_supervisor_to_year_group(id: int, year_group_id: int, data: dict @bp.delete("//year-group/") -@bp.input(ProjectSupervisorYearGroupSchema) @bp.output(MessageSchema) def delete_project_supervisor_to_year_group(id: int, year_group_id: int) -> dict: project_supervisor = ProjectSupervisor.query.filter(ProjectSupervisor.id == id). \ diff --git a/backend/app/coordinator/routes/students.py b/backend/app/coordinator/routes/students.py index e7e54d7..203c423 100644 --- a/backend/app/coordinator/routes/students.py +++ b/backend/app/coordinator/routes/students.py @@ -40,7 +40,7 @@ def list_students(year_group_id: int, query: dict) -> dict: } -@bp.get("//") +@bp.get("//detail/") @bp.output(StudentSchema) def detail_student(index: int) -> Student: student = Student.query.filter_by(index=index).first() @@ -86,18 +86,20 @@ def create_student(data: dict) -> dict: index = data['index'] yg_id = data['year_group_id'] del data['year_group_id'] - student = Student.query.filter_by(index=index).first() - if student is not None: - abort(400, "Student has already exists!") - dummy_email = f'student{randint(1, 300_000)}@gmail.com' - student = Student(**data, email=dummy_email) - db.session.add(student) + student = Student.query.filter_by(index=index).join(Student.year_groups).first() + if student is None: + # abort(400, "Student has already exists!") + dummy_email = f'student{randint(1, 300_000)}@gmail.com' + student = Student(**data, email=dummy_email) + db.session.add(student) # add student to the chosen year group year_group = YearGroup.query.filter(YearGroup.id == yg_id).first() if year_group is None: abort(400, "Year group doesn't exist!") + if any((year_group.id == yg.id for yg in student.year_groups)): + abort(400, "You are assigned to this year group!") ygs = YearGroupStudents(student_index=student.index, year_group_id=year_group.id) db.session.add(ygs) diff --git a/backend/app/coordinator/routes/year_group.py b/backend/app/coordinator/routes/year_group.py index 35f2005..b6d8045 100644 --- a/backend/app/coordinator/routes/year_group.py +++ b/backend/app/coordinator/routes/year_group.py @@ -14,7 +14,8 @@ bp = APIBlueprint("year_group", __name__, url_prefix="/year-group") @bp.output(MessageSchema, status_code=200) def create_year_group(data: dict) -> dict: name = data['name'] - year_group = YearGroup.query.filter(YearGroup.name == name).first() + mode = data['mode'] + year_group = YearGroup.query.filter(YearGroup.name == name, YearGroup.mode == mode).first() if year_group is not None: abort(400, "Year group has already exists!") @@ -54,6 +55,12 @@ def update_year_of_group(id: int, data: dict) -> dict: if year_group is None: abort(404, 'Not found year group!') + name = data['name'] + mode = data['mode'] + year_group = YearGroup.query.filter(YearGroup.name == name, YearGroup.mode == mode, YearGroup.id != id).first() + if year_group is not None: + abort(400, "Year group has already exists!") + year_group_query.update(data) db.session.commit() diff --git a/backend/app/coordinator/schemas/project_supervisor.py b/backend/app/coordinator/schemas/project_supervisor.py index b8b8c2f..7ce5244 100644 --- a/backend/app/coordinator/schemas/project_supervisor.py +++ b/backend/app/coordinator/schemas/project_supervisor.py @@ -25,7 +25,7 @@ class ProjectSupervisorCreateSchema(Schema): class ProjectSupervisorEditSchema(Schema): first_name = fields.Str(validate=validate.Length(min=1, max=255), required=True) last_name = fields.Str(validate=validate.Length(min=1, max=255), required=True) - email = fields.Str(validate=validate.Length(min=0, max=11), required=True) + email = fields.Str(validate=validate.Length(min=0, max=255), required=True) class ProjectSupervisorYearGroupSchema(Schema): diff --git a/backend/app/coordinator/schemas/students.py b/backend/app/coordinator/schemas/students.py index d32154e..c29af19 100644 --- a/backend/app/coordinator/schemas/students.py +++ b/backend/app/coordinator/schemas/students.py @@ -20,7 +20,7 @@ class GroupSchema(ma.SQLAlchemyAutoSchema): class StudentSchema(ma.SQLAlchemyAutoSchema): - group = fields.Nested(GroupSchema) + groups = fields.List(fields.Nested(GroupSchema)) class Meta: model = Student diff --git a/backend/app/examination_schedule/models.py b/backend/app/examination_schedule/models.py index b18e6b8..2ea3175 100644 --- a/backend/app/examination_schedule/models.py +++ b/backend/app/examination_schedule/models.py @@ -6,7 +6,7 @@ class ExaminationSchedule(Base): __tablename__ = 'examination_schedules' title = db.Column(db.String(100), unique=True, nullable=False) - duration_time = db.Column(db.Integer) # in minutes + duration_time = db.Column(db.Integer, nullable=False) # in minutes start_date_for_enrollment_students = db.Column(db.DateTime) end_date_for_enrollment_students = db.Column(db.DateTime) start_date = db.Column(db.DateTime, nullable=False) diff --git a/backend/app/students/models.py b/backend/app/students/models.py index 217fd0c..bb28e51 100644 --- a/backend/app/students/models.py +++ b/backend/app/students/models.py @@ -18,11 +18,20 @@ class YearGroupStudents(Base): class YearGroup(Base): __tablename__ = 'year_groups' - name = db.Column(db.String(50), unique=True, nullable=False) - mode = db.Column(db.String(1), unique=True, nullable=False) + name = db.Column(db.String(50), nullable=False) + mode = db.Column(db.String(1), nullable=False) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) students = db.relationship("YearGroupStudents") + __table__args = ( + db.UniqueConstraint('name', 'mode', name='uc_name_mode_year_group') + ) + + +students_groups = db.Table('students_groups', + db.Column('group_id', db.ForeignKey('groups.id'), nullable=False), + db.Column('student_index', db.ForeignKey('students.index'), nullable=False)) + class Group(Base): __tablename__ = "groups" @@ -37,6 +46,7 @@ class Group(Base): year_group = db.relationship('YearGroup', backref='groups', lazy=True) points_for_first_term = db.Column(db.Integer, default=0, nullable=False) points_for_second_term = db.Column(db.Integer, default=0, nullable=False) + students = db.relationship('Student', secondary=students_groups, back_populates='groups') @classmethod def search_by_name(cls, year_group_id: int, search_name: str = None) -> BaseQuery: @@ -53,8 +63,7 @@ class Student(Person): pesel = db.Column(db.String(11), default='') index = db.Column(db.Integer, primary_key=True) - group_id = db.Column(db.Integer, db.ForeignKey('groups.id')) - groups = db.relationship('Group', backref='students', lazy=True) + groups = db.relationship('Group', secondary=students_groups, back_populates='students') year_groups = db.relationship("YearGroupStudents") @classmethod diff --git a/backend/app/students/routes/enrollments.py b/backend/app/students/routes/enrollments.py index b970857..d92c91a 100644 --- a/backend/app/students/routes/enrollments.py +++ b/backend/app/students/routes/enrollments.py @@ -24,27 +24,27 @@ def assign_your_group_to_term_of_defence(examination_schedule_id: int, term_of_d ################ term_of_defence = TermOfDefence.query.filter(TermOfDefence.id == term_of_defence_id, TermOfDefence.examination_schedule_id == examination_schedule_id).first() - - if term_of_defence is None: + ex = term_of_defence.examination_schedule + if term_of_defence is None or ex is None: abort(400, "Term of defence not found!") - st = Student.query.join(Group).join(ProjectSupervisor).filter(Student.index == student.index).first() - if st is None or st.groups.project_supervisor is None: + g = Group.query.join(ProjectSupervisor).filter(Group.year_group_id == examination_schedule_id). \ + join(Group.students).filter_by(index=student.index).first() + if g is None or g.project_supervisor is None: abort(400, "You don't have a group or your group doesn't have an assigned project supervisor!") - defence = TermOfDefence.query.filter(TermOfDefence.group_id == st.groups.id, + defence = TermOfDefence.query.filter(TermOfDefence.group_id == g.id, TermOfDefence.examination_schedule_id == examination_schedule_id).first() if defence is not None: abort(400, "Your group has already assigned to any exam date!") - g = Group.query.filter(Group.id == st.group_id).first() td = TermOfDefence.query.join(TermOfDefence.members_of_committee). \ filter_by(id=g.project_supervisor_id).first() if td is None: abort(400, "Your project supervisor is not in committee!") - term_of_defence.group_id = st.groups.id + term_of_defence.group_id = g.id db.session.add(term_of_defence) db.session.commit() return {"message": "You have just assigned the group for this exam date!"} @@ -60,13 +60,16 @@ def delete_your_group_from_term_of_defence(examination_schedule_id: int, term_of abort(404, "Student doesn't exist!") ################ - term_of_defence = TermOfDefence.query.filter(TermOfDefence.id == term_of_defence_id). \ + term_of_defence = TermOfDefence.query.join(ExaminationSchedule).filter(TermOfDefence.id == term_of_defence_id). \ filter(TermOfDefence.examination_schedule_id == examination_schedule_id).first() + ex = term_of_defence.examination_schedule if term_of_defence is None: abort(404, "Term of defence doesn't exist!") - if student.groups.id != term_of_defence.group_id: + group = Group.query.filter(Group.year_group_id == ex.year_group_id). \ + join(Group.students).filter_by(index=student.index).first() + if group.id != term_of_defence.group_id: abort(400, "You are not assigned to this group!") term_of_defence.group_id = None diff --git a/backend/app/students/schemas.py b/backend/app/students/schemas.py index 17bd64e..503f29d 100644 --- a/backend/app/students/schemas.py +++ b/backend/app/students/schemas.py @@ -37,10 +37,17 @@ class ExaminationScheduleListSchema(Schema): examination_schedules = fields.List(fields.Nested(ExaminationScheduleSchema)) +class ProjectSupervisorCommitteeSchema(Schema): + id = fields.Integer() + first_name = fields.Str() + last_name = fields.Str() + + class TermOfDefenceStudentItemSchema(Schema): id = fields.Integer() start_date = fields.DateTime() end_date = fields.DateTime() + members_of_committee = fields.List(fields.Nested(ProjectSupervisorCommitteeSchema)) class TermOfDefenceStudentListSchema(Schema): diff --git a/backend/migrations/versions/5c3c4c4e1a72_.py b/backend/migrations/versions/5c3c4c4e1a72_.py deleted file mode 100644 index ca71701..0000000 --- a/backend/migrations/versions/5c3c4c4e1a72_.py +++ /dev/null @@ -1,28 +0,0 @@ -"""empty message - -Revision ID: 5c3c4c4e1a72 -Revises: a96f91e5b556 -Create Date: 2022-11-12 11:48:38.377516 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '5c3c4c4e1a72' -down_revision = 'a96f91e5b556' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.add_column('examination_schedules', sa.Column('duration_time', sa.Integer(), nullable=True)) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_column('examination_schedules', 'duration_time') - # ### end Alembic commands ### diff --git a/backend/migrations/versions/a96f91e5b556_.py b/backend/migrations/versions/7deb011753b2_.py similarity index 93% rename from backend/migrations/versions/a96f91e5b556_.py rename to backend/migrations/versions/7deb011753b2_.py index 544eeb4..e9706be 100644 --- a/backend/migrations/versions/a96f91e5b556_.py +++ b/backend/migrations/versions/7deb011753b2_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: a96f91e5b556 +Revision ID: 7deb011753b2 Revises: -Create Date: 2022-11-12 11:34:01.223667 +Create Date: 2022-11-16 22:48:36.220156 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'a96f91e5b556' +revision = '7deb011753b2' down_revision = None branch_labels = None depends_on = None @@ -28,23 +28,33 @@ def upgrade(): ) op.create_index(op.f('ix_project_supervisors_first_name'), 'project_supervisors', ['first_name'], unique=False) op.create_index(op.f('ix_project_supervisors_last_name'), 'project_supervisors', ['last_name'], unique=False) + op.create_table('students', + sa.Column('first_name', sa.String(length=255), nullable=False), + sa.Column('last_name', sa.String(length=255), nullable=False), + sa.Column('email', sa.String(length=120), nullable=True), + sa.Column('pesel', sa.String(length=11), nullable=True), + sa.Column('index', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('index'), + sa.UniqueConstraint('email') + ) + op.create_index(op.f('ix_students_first_name'), 'students', ['first_name'], unique=False) + op.create_index(op.f('ix_students_last_name'), 'students', ['last_name'], unique=False) op.create_table('year_groups', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('name', sa.String(length=50), nullable=False), sa.Column('mode', sa.String(length=1), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), - sa.PrimaryKeyConstraint('id'), - sa.UniqueConstraint('mode'), - sa.UniqueConstraint('name') + sa.PrimaryKeyConstraint('id') ) op.create_table('examination_schedules', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('title', sa.String(length=100), nullable=False), - sa.Column('year_group_id', sa.Integer(), nullable=False), + sa.Column('duration_time', sa.Integer(), nullable=False), sa.Column('start_date_for_enrollment_students', sa.DateTime(), nullable=True), sa.Column('end_date_for_enrollment_students', sa.DateTime(), nullable=True), sa.Column('start_date', sa.DateTime(), nullable=False), sa.Column('end_date', sa.DateTime(), nullable=False), + sa.Column('year_group_id', sa.Integer(), nullable=False), sa.ForeignKeyConstraint(['year_group_id'], ['year_groups.id'], ), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('title') @@ -72,19 +82,20 @@ def upgrade(): sa.ForeignKeyConstraint(['year_group_id'], ['year_groups.id'], ), sa.PrimaryKeyConstraint('id') ) - op.create_table('students', - sa.Column('first_name', sa.String(length=255), nullable=False), - sa.Column('last_name', sa.String(length=255), nullable=False), - sa.Column('email', sa.String(length=120), nullable=True), - sa.Column('pesel', sa.String(length=11), nullable=True), - sa.Column('index', sa.Integer(), nullable=False), - sa.Column('group_id', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), - sa.PrimaryKeyConstraint('index'), - sa.UniqueConstraint('email') + op.create_table('year_group_students', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('year_group_id', sa.Integer(), nullable=True), + sa.Column('student_index', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['student_index'], ['students.index'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['year_group_id'], ['year_groups.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('students_groups', + sa.Column('group_id', sa.Integer(), nullable=False), + sa.Column('student_index', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), + sa.ForeignKeyConstraint(['student_index'], ['students.index'], ) ) - op.create_index(op.f('ix_students_first_name'), 'students', ['first_name'], unique=False) - op.create_index(op.f('ix_students_last_name'), 'students', ['last_name'], unique=False) op.create_table('temporary_availabilities', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('start_date', sa.DateTime(), nullable=False), @@ -111,30 +122,23 @@ def upgrade(): sa.ForeignKeyConstraint(['project_supervisor_id'], ['project_supervisors.id'], ), sa.ForeignKeyConstraint(['term_of_defence_id'], ['term_of_defences.id'], ) ) - op.create_table('year_group_students', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('year_group_id', sa.Integer(), nullable=True), - sa.Column('student_index', sa.Integer(), nullable=True), - sa.ForeignKeyConstraint(['student_index'], ['students.index'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['year_group_id'], ['year_groups.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('year_group_students') op.drop_table('committees') op.drop_table('term_of_defences') op.drop_table('temporary_availabilities') - op.drop_index(op.f('ix_students_last_name'), table_name='students') - op.drop_index(op.f('ix_students_first_name'), table_name='students') - op.drop_table('students') + op.drop_table('students_groups') + op.drop_table('year_group_students') op.drop_table('year_group_project_supervisors') op.drop_table('groups') op.drop_table('examination_schedules') op.drop_table('year_groups') + op.drop_index(op.f('ix_students_last_name'), table_name='students') + op.drop_index(op.f('ix_students_first_name'), table_name='students') + op.drop_table('students') op.drop_index(op.f('ix_project_supervisors_last_name'), table_name='project_supervisors') op.drop_index(op.f('ix_project_supervisors_first_name'), table_name='project_supervisors') op.drop_table('project_supervisors') diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..980d14d --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1 @@ +REACT_APP_BASE_URL=http://localhost:5000/api/ \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index 4d29575..532eddc 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +.env \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9aa8618..8ba7c30 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,6 +21,7 @@ "luxon": "^3.0.4", "react": "^18.1.0", "react-big-calendar": "^1.5.0", + "react-date-picker": "^9.1.0", "react-dom": "^18.1.0", "react-hook-form": "^7.31.3", "react-modal": "^3.16.1", @@ -29,6 +30,7 @@ "react-scripts": "5.0.1", "react-select": "^5.3.2", "typescript": "^4.6.4", + "use-local-storage-state": "^18.1.1", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -3887,6 +3889,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-calendar": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@types/react-calendar/-/react-calendar-3.9.0.tgz", + "integrity": "sha512-KpAu1MKAGFw5hNwlDnWsHWqI9i/igAB+8jH97YV7QpC2v7rlwNEU5i6VMFb73lGRacuejM/Zd2LklnEzkFV3XA==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.4.tgz", @@ -4344,6 +4354,14 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz", + "integrity": "sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -6411,6 +6429,11 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-element-overflow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/detect-element-overflow/-/detect-element-overflow-1.2.0.tgz", + "integrity": "sha512-Jtr9ivYPhpd9OJux+hjL0QjUKiS1Ghgy8tvIufUjFslQgIWvgGr4mn57H190APbKkiOmXnmtMI6ytaKzMusecg==" + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8261,6 +8284,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-user-locale": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.5.1.tgz", + "integrity": "sha512-WiNpoFRcHn1qxP9VabQljzGwkAQDrcpqUtaP0rNBEkFxJdh4f3tik6MfZsMYZc+UgQJdGCxWEjL9wnCUlRQXag==", + "dependencies": { + "lodash.memoize": "^4.1.1" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -11577,6 +11611,14 @@ "semver": "bin/semver.js" } }, + "node_modules/make-event-props": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.3.0.tgz", + "integrity": "sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==", + "funding": { + "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" + } + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -13854,6 +13896,47 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" }, + "node_modules/react-calendar": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.0.0.tgz", + "integrity": "sha512-y9Q5Oo3Mq869KExbOCP3aJ3hEnRZKZ0TqUa9QU1wJGgDZFrW1qTaWp5v52oZpmxTTrpAMTUcUGaC0QJcO1f8Nw==", + "dependencies": { + "@wojtekmaj/date-utils": "^1.0.2", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "prop-types": "^15.6.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-date-picker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-9.1.0.tgz", + "integrity": "sha512-wj9SoOhEgQTzJsDRLePpNzUDNrcSiCpj1TFSwiAnLlOuJvvkk10I9rdvIyHmeJg/G17hyP5wSwTppCZHqOTHhA==", + "dependencies": { + "@types/react-calendar": "^3.0.0", + "@wojtekmaj/date-utils": "^1.0.3", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-calendar": "^4.0.0", + "react-fit": "^1.4.0", + "update-input-width": "^1.2.2" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-date-picker?sponsor=1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -13988,6 +14071,23 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "node_modules/react-fit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-fit/-/react-fit-1.4.0.tgz", + "integrity": "sha512-cf9sFKbr1rlTB9fNIKE5Uy4NCMUOqrX2mdJ69V4RtmV4KubPdtnbIP1tEar16GXaToCRr7I7c9d2wkTNk9TV5g==", + "dependencies": { + "detect-element-overflow": "^1.2.0", + "prop-types": "^15.6.0", + "tiny-warning": "^1.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-fit?sponsor=1" + }, + "peerDependencies": { + "react": "^15.5.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.5.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-hook-form": { "version": "7.31.3", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.31.3.tgz", @@ -15700,6 +15800,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15999,6 +16104,14 @@ "yarn": "*" } }, + "node_modules/update-input-width": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-input-width/-/update-input-width-1.2.2.tgz", + "integrity": "sha512-6QwD9ZVSXb96PxOZ01DU0DJTPwQGY7qBYgdniZKJN02Xzom2m+9J6EPxMbefskqtj4x78qbe5psDSALq9iNEYg==", + "funding": { + "url": "https://github.com/wojtekmaj/update-input-width?sponsor=1" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -16007,6 +16120,21 @@ "punycode": "^2.1.0" } }, + "node_modules/use-local-storage-state": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-18.1.1.tgz", + "integrity": "sha512-09bl6q3mkSlkEt8KeBPCmdPEWEojWYF70Qbz+7wkfQX1feaFITM9m84+h0Jr6Cnf/IvpahkFh7UbX2VNN3ioTQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/astoilkov" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -19682,6 +19810,14 @@ "@types/react": "*" } }, + "@types/react-calendar": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@types/react-calendar/-/react-calendar-3.9.0.tgz", + "integrity": "sha512-KpAu1MKAGFw5hNwlDnWsHWqI9i/igAB+8jH97YV7QpC2v7rlwNEU5i6VMFb73lGRacuejM/Zd2LklnEzkFV3XA==", + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "18.0.4", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.4.tgz", @@ -20036,6 +20172,11 @@ "@xtuc/long": "4.2.2" } }, + "@wojtekmaj/date-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz", + "integrity": "sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==" + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -21551,6 +21692,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "detect-element-overflow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/detect-element-overflow/-/detect-element-overflow-1.2.0.tgz", + "integrity": "sha512-Jtr9ivYPhpd9OJux+hjL0QjUKiS1Ghgy8tvIufUjFslQgIWvgGr4mn57H190APbKkiOmXnmtMI6ytaKzMusecg==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -22902,6 +23048,14 @@ "get-intrinsic": "^1.1.1" } }, + "get-user-locale": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-1.5.1.tgz", + "integrity": "sha512-WiNpoFRcHn1qxP9VabQljzGwkAQDrcpqUtaP0rNBEkFxJdh4f3tik6MfZsMYZc+UgQJdGCxWEjL9wnCUlRQXag==", + "requires": { + "lodash.memoize": "^4.1.1" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -25317,6 +25471,11 @@ } } }, + "make-event-props": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.3.0.tgz", + "integrity": "sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==" + }, "makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -26825,6 +26984,33 @@ } } }, + "react-calendar": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.0.0.tgz", + "integrity": "sha512-y9Q5Oo3Mq869KExbOCP3aJ3hEnRZKZ0TqUa9QU1wJGgDZFrW1qTaWp5v52oZpmxTTrpAMTUcUGaC0QJcO1f8Nw==", + "requires": { + "@wojtekmaj/date-utils": "^1.0.2", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "prop-types": "^15.6.0" + } + }, + "react-date-picker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-9.1.0.tgz", + "integrity": "sha512-wj9SoOhEgQTzJsDRLePpNzUDNrcSiCpj1TFSwiAnLlOuJvvkk10I9rdvIyHmeJg/G17hyP5wSwTppCZHqOTHhA==", + "requires": { + "@types/react-calendar": "^3.0.0", + "@wojtekmaj/date-utils": "^1.0.3", + "clsx": "^1.2.1", + "get-user-locale": "^1.2.0", + "make-event-props": "^1.1.0", + "prop-types": "^15.6.0", + "react-calendar": "^4.0.0", + "react-fit": "^1.4.0", + "update-input-width": "^1.2.2" + } + }, "react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -26925,6 +27111,16 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" }, + "react-fit": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-fit/-/react-fit-1.4.0.tgz", + "integrity": "sha512-cf9sFKbr1rlTB9fNIKE5Uy4NCMUOqrX2mdJ69V4RtmV4KubPdtnbIP1tEar16GXaToCRr7I7c9d2wkTNk9TV5g==", + "requires": { + "detect-element-overflow": "^1.2.0", + "prop-types": "^15.6.0", + "tiny-warning": "^1.0.0" + } + }, "react-hook-form": { "version": "7.31.3", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.31.3.tgz", @@ -28194,6 +28390,11 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, + "tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -28416,6 +28617,11 @@ "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, + "update-input-width": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-input-width/-/update-input-width-1.2.2.tgz", + "integrity": "sha512-6QwD9ZVSXb96PxOZ01DU0DJTPwQGY7qBYgdniZKJN02Xzom2m+9J6EPxMbefskqtj4x78qbe5psDSALq9iNEYg==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -28424,6 +28630,12 @@ "punycode": "^2.1.0" } }, + "use-local-storage-state": { + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/use-local-storage-state/-/use-local-storage-state-18.1.1.tgz", + "integrity": "sha512-09bl6q3mkSlkEt8KeBPCmdPEWEojWYF70Qbz+7wkfQX1feaFITM9m84+h0Jr6Cnf/IvpahkFh7UbX2VNN3ioTQ==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0ff5e33..3bc328d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "luxon": "^3.0.4", "react": "^18.1.0", "react-big-calendar": "^1.5.0", + "react-date-picker": "^9.1.0", "react-dom": "^18.1.0", "react-hook-form": "^7.31.3", "react-modal": "^3.16.1", @@ -24,6 +25,7 @@ "react-scripts": "5.0.1", "react-select": "^5.3.2", "typescript": "^4.6.4", + "use-local-storage-state": "^18.1.1", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts index 055ef28..84b245d 100644 --- a/frontend/src/api/axiosInstance.ts +++ b/frontend/src/api/axiosInstance.ts @@ -1,5 +1,7 @@ -import axios from "axios"; +import axios from 'axios' -const axiosInstance = axios.create({}); +const axiosInstance = axios.create({ + baseURL: process.env.REACT_APP_BASE_URL, +}) -export default axiosInstance; +export default axiosInstance diff --git a/frontend/src/api/enrollment.ts b/frontend/src/api/enrollment.ts index 4b92521..4df8486 100644 --- a/frontend/src/api/enrollment.ts +++ b/frontend/src/api/enrollment.ts @@ -2,6 +2,7 @@ import axiosInstance from './axiosInstance' export const getEnrollmentList = ( params: Partial<{ + year_group_id: number page: number per_page: number name: string @@ -17,6 +18,6 @@ export const getEnrollmentList = ( email: string mode: number }[] - }>('http://127.0.0.1:5000/api/students/registrations/', { + }>(`students/registrations/${params.year_group_id}`, { params, }) diff --git a/frontend/src/api/groups.ts b/frontend/src/api/groups.ts index 8119472..d63ba43 100644 --- a/frontend/src/api/groups.ts +++ b/frontend/src/api/groups.ts @@ -20,22 +20,21 @@ export interface Group { export const getGroups = ( params: Partial<{ + year_group_id: number page: number per_page: number name: string }>, ) => axiosInstance.get<{ max_pages: number; groups: Group[] }>( - 'http://127.0.0.1:5000/api/coordinator/groups/', + `coordinator/groups/${params.year_group_id}`, { params, }, ) -export const createGroup = (payload: CreateGroup) => - axiosInstance.post('http://127.0.0.1:5000/api/coordinator/groups/', payload) +export const createGroup = (year_group_id: number, payload: CreateGroup) => + axiosInstance.post(`coordinator/groups/${year_group_id}`, payload) - export const deleteGroup = (id: number) => - axiosInstance.delete( - `http://127.0.0.1:5000/api/coordinator/groups/${id}/`, - ) \ No newline at end of file +export const deleteGroup = (id: number) => + axiosInstance.delete(`coordinator/groups/${id}/`) diff --git a/frontend/src/api/leaders.ts b/frontend/src/api/leaders.ts index 3090db2..9f33f7f 100644 --- a/frontend/src/api/leaders.ts +++ b/frontend/src/api/leaders.ts @@ -19,6 +19,7 @@ export interface Leader { export const getLeaders = ( params: Partial<{ + year_group_id: number fullname: string order_by_first_name: OrderType order_by_last_name: OrderType @@ -27,17 +28,12 @@ export const getLeaders = ( }> = {}, ) => axiosInstance.get( - 'http://127.0.0.1:5000/api/coordinator/project_supervisor', + `coordinator/project_supervisor/${params.year_group_id}`, { params }, ) export const createLeader = (payload: Leader) => - axiosInstance.post( - 'http://127.0.0.1:5000/api/coordinator/project_supervisor/', - payload, - ) + axiosInstance.post('coordinator/project_supervisor/', payload) export const deleteLeader = (id: number) => - axiosInstance.delete( - `http://127.0.0.1:5000/api/coordinator/project_supervisor/${id}/`, - ) + axiosInstance.delete(`coordinator/project_supervisor/${id}/`) diff --git a/frontend/src/api/schedule.ts b/frontend/src/api/schedule.ts index 5a4ac70..7a26be2 100644 --- a/frontend/src/api/schedule.ts +++ b/frontend/src/api/schedule.ts @@ -1,39 +1,35 @@ import axiosInstance from './axiosInstance' -export const getEvents = (scheduleId: number) => { +export const getTermsOfDefences = (scheduleId: number) => { return axiosInstance.get<{ - enrollments: { + term_of_defences: { id: number start_date: string end_date: string title: string - committee: { + members_of_committee: { members: { first_name: string; last_name: string }[] } group: { name: string } }[] - }>( - `http://127.0.0.1:5000/api/examination_schedule/enrollments/${scheduleId}/coordinator-view/?per_page=10000`, - ) + }>(`coordinator/enrollments/${scheduleId}/term-of-defences/`) } -export const getStudentsSchedule = (scheduleId: number) => { +export const getStudentsTermsOfDefences = (scheduleId: number) => { return axiosInstance.get<{ - enrollments: { + term_of_defences: { id: number start_date: string end_date: string title: string - committee: { + members_of_committee: { members: { first_name: string; last_name: string }[] } group: { name: string } }[] - }>( - `http://127.0.0.1:5000/api/examination_schedule/enrollments/${scheduleId}/student-view?per_page=10000`, - ) + }>(`students/examination-schedule/${scheduleId}/enrollments/`) } -export const getSchedules = () => { +export const getSchedules = (year_group_id: number = 1) => { return axiosInstance.get<{ examination_schedules: { id: number @@ -42,36 +38,34 @@ export const getSchedules = () => { title: string mode: boolean }[] - }>( - 'http://127.0.0.1:5000/api/coordinator/examination_schedule/?per_page=10000', - ) + }>(`coordinator/examination_schedule/${year_group_id}?per_page=10000`) } export const createEvent = ({ start_date, end_date, scheduleId, + project_supervisors, }: { start_date: string end_date: string scheduleId: number + project_supervisors: number[] }) => { - return axiosInstance.post( - `http://127.0.0.1:5000/api/coordinator/enrollments/${scheduleId}/`, - { - start_date, - end_date, - }, - ) + return axiosInstance.post(`coordinator/enrollments/${scheduleId}/add`, { + start_date, + end_date, + project_supervisors, + }) } -export const createSchedule = (title: string) => { +export const createSchedule = ( + year_group_id: number, + payload: { title: string; start_date: string; end_date: string }, +) => { return axiosInstance.post( - 'http://127.0.0.1:5000/api/coordinator/examination_schedule/', - { - title, - mode: true, - }, + `coordinator/examination_schedule/${year_group_id}/`, + payload, ) } @@ -84,13 +78,10 @@ export const setEventDate = ({ start_date: string end_date: string }) => { - return axiosInstance.put( - `http://127.0.0.1:5000/api/coordinator/examination_schedule/${id}/date`, - { - start_date, - end_date, - }, - ) + return axiosInstance.put(`coordinator/examination_schedule/${id}/date`, { + start_date, + end_date, + }) } export const assignGroup = ({ @@ -103,7 +94,7 @@ export const assignGroup = ({ studentIndex: number }) => { return axiosInstance.post( - `http://127.0.0.1:5000/api/students/${scheduleId}/enrollments/${enrollmentId}/`, + `students/${scheduleId}/enrollments/${enrollmentId}/`, { student_index: studentIndex, }, @@ -120,7 +111,7 @@ export const assignSupervisor = ({ supervisorId: number }) => { return axiosInstance.post( - `http://127.0.0.1:5000/api/project_supervisor/${scheduleId}/enrollments/${enrollmentId}/`, + `project_supervisor/${scheduleId}/enrollments/${enrollmentId}/`, { project_supervisor_id: supervisorId, }, @@ -129,7 +120,7 @@ export const assignSupervisor = ({ export const downloadSchedule = (scheduleId: number) => axiosInstance.post( - `http://127.0.0.1:5000/api/coordinator/examination_schedule/${scheduleId}/download/`, + `coordinator/examination_schedule/${scheduleId}/download/`, { responseType: 'blob', }, diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index fcec34c..d4f8ef3 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -18,6 +18,7 @@ export interface Student { export const getStudents = ( params: Partial<{ + year_group_id: number fullname: string order_by_first_name: OrderType order_by_last_name: OrderType @@ -27,30 +28,20 @@ export const getStudents = ( }> = {}, ) => axiosInstance.get( - 'http://127.0.0.1:5000/api/coordinator/students', + `coordinator/students/${params.year_group_id}`, { params }, ) export const createStudent = (payload: Student) => - axiosInstance.post('http://127.0.0.1:5000/api/coordinator/students/', payload) + axiosInstance.post('coordinator/students/', payload) export const uploadStudents = (payload: FormData) => - axiosInstance.post( - 'http://127.0.0.1:5000/api/coordinator/students/upload/', - payload, - ) + axiosInstance.post('coordinator/students/upload/', payload) export const deleteStudent = (index: number) => - axiosInstance.delete( - `http://127.0.0.1:5000/api/coordinator/students/${index}/`, - ) + axiosInstance.delete(`coordinator/students/${index}/`) export const downloadStudents = (mode: boolean) => - axiosInstance.post( - `http://127.0.0.1:5000/api/coordinator/students/download/?mode=${Number( - mode, - )}`, - { - responseType: 'blob', - }, - ) + axiosInstance.post(`coordinator/students/download/?mode=${Number(mode)}`, { + responseType: 'blob', + }) diff --git a/frontend/src/utils/bigCalendarTranslations.ts b/frontend/src/utils/bigCalendarTranslations.ts new file mode 100644 index 0000000..cd47f75 --- /dev/null +++ b/frontend/src/utils/bigCalendarTranslations.ts @@ -0,0 +1,22 @@ +const bigCalendarTranslations = { + date: 'Data', + time: 'Czas', + event: 'Event', + allDay: 'Cały dzień', + week: 'Tydzień', + work_week: 'Dni robocze', + day: 'Dzień', + month: 'Miesiąc', + previous: 'Cofnij', + next: 'Dalej', + yesterday: 'Wczoraj', + tomorrow: 'Jutro', + today: 'Dzisiaj', + agenda: 'Agenda', + + noEventsInRange: 'Brak wydarzeń w tym czasie.', + + showMore: (total: number) => `+${total} more`, +} + +export default bigCalendarTranslations diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index 8bb6124..c126c1e 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -1,6 +1,11 @@ import { NavLink } from 'react-router-dom' +import useLocalStorageState from 'use-local-storage-state' const Login = () => { + const [yearGroupId, setYearGroupId] = useLocalStorageState('yearGroupId', { + defaultValue: 1, + }) + return ( <>
diff --git a/frontend/src/views/coordinator/AddGroup.tsx b/frontend/src/views/coordinator/AddGroup.tsx index 2f70125..2b779ed 100644 --- a/frontend/src/views/coordinator/AddGroup.tsx +++ b/frontend/src/views/coordinator/AddGroup.tsx @@ -6,6 +6,7 @@ import { createGroup, CreateGroup } from '../../api/groups' import InputError from '../../components/InputError' import Select from 'react-select' import { getLeaders } from '../../api/leaders' +import useLocalStorageState from 'use-local-storage-state' type SelectValue = { value: string | number @@ -14,6 +15,8 @@ type SelectValue = { const AddGroup = () => { const [isAlertVisible, setIsAlertVisible] = useState(false) + const [yearGroupId] = useLocalStorageState('yearGroupId') + const { register, handleSubmit, @@ -29,13 +32,14 @@ const AddGroup = () => { { onSuccess: (data) => { setStudentOptions( - data?.data.students.filter(st => st.group === null) + data?.data.students + .filter((st) => st.group === null) .map(({ first_name, last_name, index }) => { - return { - value: index, - label: `${first_name} ${last_name} (${index})`, - } - }), + return { + value: index, + label: `${first_name} ${last_name} (${index})`, + } + }), ) }, }, @@ -46,12 +50,12 @@ const AddGroup = () => { { onSuccess: (data) => { setSupervisorOptions( - data?.data.project_supervisors.filter(ld => ld.count_groups < ld.limit_group).map( - ({ id, first_name, last_name }) => ({ + data?.data.project_supervisors + .filter((ld) => ld.count_groups < ld.limit_group) + .map(({ id, first_name, last_name }) => ({ value: id, label: `${first_name} ${last_name}`, - }), - ), + })), ) }, }, @@ -59,7 +63,7 @@ const AddGroup = () => { const { mutate: mutateCreateGroup } = useMutation( 'createGroup', - (payload: CreateGroup) => createGroup(payload), + (payload: CreateGroup) => createGroup(Number(yearGroupId), payload), { onSuccess: () => { setIsAlertVisible(true) diff --git a/frontend/src/views/coordinator/EditSchedule.tsx b/frontend/src/views/coordinator/EditSchedule.tsx index 99ac15e..522a2a7 100644 --- a/frontend/src/views/coordinator/EditSchedule.tsx +++ b/frontend/src/views/coordinator/EditSchedule.tsx @@ -1,3 +1,4 @@ +import { DateTime } from 'luxon' import { useState } from 'react' import { Controller, NestedValue, useForm } from 'react-hook-form' import { useMutation, useQuery } from 'react-query' @@ -24,7 +25,7 @@ const EditSchedule = ({ scheduleId: string }) => { const { register, handleSubmit, reset, control } = useForm<{ - committee: NestedValue + members_of_committee: NestedValue }>({ mode: 'onBlur' }) const [committeeOptions, setCommitteeOptions] = useState([]) @@ -55,7 +56,7 @@ const EditSchedule = ({ ) const onSubmit = (data: any) => { - data?.committee?.forEach((id: number) => { + data?.members_of_committee?.forEach((id: number) => { mutateAssignSupervisor({ scheduleId: Number(scheduleId), enrollmentId: eventData.id, @@ -65,41 +66,66 @@ const EditSchedule = ({ } return ( -
+
-

Termin

-
-
- - ( - + onChange(values.map((value) => value.value)) + } + onBlur={onBlur} + styles={{ + control: (styles) => ({ + ...styles, + padding: '0.3rem', + borderRadius: '0.5rem', + }), + }} + /> + )} /> - )} - /> -
-
- +
+
+ + + )}
) diff --git a/frontend/src/views/coordinator/Groups.tsx b/frontend/src/views/coordinator/Groups.tsx index 5aa5373..cdb5e79 100644 --- a/frontend/src/views/coordinator/Groups.tsx +++ b/frontend/src/views/coordinator/Groups.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames' import { useEffect, useState } from 'react' import { useMutation, useQuery } from 'react-query' import { useNavigate } from 'react-router-dom' +import useLocalStorageState from 'use-local-storage-state' import { deleteGroup, getGroups } from '../../api/groups' import { ReactComponent as IconRemove } from '../../assets/svg/icon-remove.svg' @@ -9,6 +10,7 @@ const Groups = () => { let navigate = useNavigate() const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) + const [yearGroupId] = useLocalStorageState('yearGroupId') const perPageOptions = [ { @@ -34,7 +36,7 @@ const Groups = () => { data: groups, refetch: refetchGroups, } = useQuery(['groups', page, perPage], () => - getGroups({ page, per_page: perPage }), + getGroups({ year_group_id: Number(yearGroupId), page, per_page: perPage }), ) const { mutate: mutateDelete } = useMutation( diff --git a/frontend/src/views/coordinator/Leaders.tsx b/frontend/src/views/coordinator/Leaders.tsx index df7e80d..c613ab4 100644 --- a/frontend/src/views/coordinator/Leaders.tsx +++ b/frontend/src/views/coordinator/Leaders.tsx @@ -4,11 +4,13 @@ import { useNavigate } from 'react-router-dom' import { getLeaders, deleteLeader } from '../../api/leaders' import classNames from 'classnames' import { ReactComponent as IconRemove } from '../../assets/svg/icon-remove.svg' +import useLocalStorageState from 'use-local-storage-state' const Leaders = () => { let navigate = useNavigate() const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) + const [yearGroupId] = useLocalStorageState('yearGroupId') const perPageOptions = [ { @@ -34,7 +36,7 @@ const Leaders = () => { data: leaders, refetch: refetchLeaders, } = useQuery(['project_supervisors', page, perPage], () => - getLeaders({ page, per_page: perPage }), + getLeaders({ year_group_id: Number(yearGroupId), page, per_page: perPage }), ) const { mutate: mutateDelete } = useMutation( diff --git a/frontend/src/views/coordinator/Schedule.tsx b/frontend/src/views/coordinator/Schedule.tsx index 6949a45..f29f4e1 100644 --- a/frontend/src/views/coordinator/Schedule.tsx +++ b/frontend/src/views/coordinator/Schedule.tsx @@ -2,11 +2,19 @@ import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' import { DateTime, Settings } from 'luxon' import { useCallback, useState } from 'react' import { useMutation, useQuery } from 'react-query' -import { createEvent, downloadSchedule, getEvents } from '../../api/schedule' +import { + createEvent, + downloadSchedule, + getTermsOfDefences, +} from '../../api/schedule' import { useParams } from 'react-router-dom' import Modal from 'react-modal' -import { useForm } from 'react-hook-form' +import { Controller, NestedValue, useForm } from 'react-hook-form' import EditSchedule from './EditSchedule' +import Select from 'react-select' +import { getLeaders } from '../../api/leaders' +import useLocalStorageState from 'use-local-storage-state' +import bigCalendarTranslations from '../../utils/bigCalendarTranslations' const customStyles = { content: { @@ -18,12 +26,17 @@ const customStyles = { transform: 'translate(-50%, -50%)', }, } +type SelectValue = { + value: string | number + label: string +} const Schedule = () => { Settings.defaultZone = DateTime.local().zoneName Settings.defaultLocale = 'pl' const { id } = useParams<{ id: string }>() + const [yearGroupId] = useLocalStorageState('yearGroupId') const [events, setEvents] = useState< { id: number @@ -47,41 +60,68 @@ const Schedule = () => { const [isEditModalOpen, setIsEditModalOpen] = useState(false) Modal.setAppElement('#root') - const { register, handleSubmit, reset } = useForm<{ + const { register, handleSubmit, reset, control } = useForm<{ from: string to: string + project_supervisors: NestedValue }>({ mode: 'onBlur' }) + const [committeeOptions, setCommitteeOptions] = useState([]) - const { refetch } = useQuery(['schedules'], () => getEvents(Number(id)), { - onSuccess: (data) => { - setEvents( - data.data.enrollments.map( - ({ - id, - start_date, - end_date, - title = 'Obrona', - committee, - group, - }) => { - return { - id, - title: `Obrona ${group?.name ?? ''}`, - start: new Date(start_date), - end: new Date(end_date), - resource: { - committee, - }, - } - }, - ), - ) + const { isLoading: areLeadersLoading } = useQuery( + 'leaders', + () => getLeaders({ year_group_id: Number(yearGroupId), per_page: 1000 }), + { + onSuccess: (data) => { + setCommitteeOptions( + data?.data.project_supervisors.map( + ({ id, first_name, last_name }) => ({ + value: id, + label: `${first_name} ${last_name}`, + }), + ), + ) + }, }, - }) + ) + + const { refetch } = useQuery( + ['schedules'], + () => getTermsOfDefences(Number(id)), + { + onSuccess: (data) => { + setEvents( + data.data.term_of_defences.map( + ({ + id, + start_date, + end_date, + title = 'Obrona', + members_of_committee, + group, + }) => { + return { + id, + title: `${group?.name ?? '-'}`, + start: new Date(start_date), + end: new Date(end_date), + resource: { + members_of_committee, + }, + } + }, + ), + ) + }, + }, + ) const { mutateAsync: mutateCreateEvent } = useMutation( ['createEvent'], - (data: { start_date: string; end_date: string; scheduleId: number }) => - createEvent(data), + (data: { + project_supervisors: number[] + start_date: string + end_date: string + scheduleId: number + }) => createEvent(data), ) const { mutate: mutateDownload } = useMutation( @@ -104,16 +144,16 @@ const Schedule = () => { if (view === Views.MONTH) { setIsModalOpen(true) } else { - await mutateCreateEvent({ - start_date: DateTime.fromJSDate(event.start).toFormat( - 'yyyy-LL-dd HH:mm:ss', - ), - end_date: DateTime.fromJSDate(event.end).toFormat( - 'yyyy-LL-dd HH:mm:ss', - ), - scheduleId: Number(id), - }) - refetch() + // await mutateCreateEvent({ + // start_date: DateTime.fromJSDate(event.start).toFormat( + // 'yyyy-LL-dd HH:mm:ss', + // ), + // end_date: DateTime.fromJSDate(event.end).toFormat( + // 'yyyy-LL-dd HH:mm:ss', + // ), + // scheduleId: Number(id), + // }) + // refetch() } } @@ -146,6 +186,7 @@ const Schedule = () => { .set({ hour: to[0], minute: to[1] }) .toFormat('yyyy-LL-dd HH:mm:ss'), scheduleId: Number(id), + project_supervisors: data?.project_supervisors, }) refetch() reset() @@ -153,6 +194,20 @@ const Schedule = () => { } } + const eventGetter = (event: any) => { + return event?.resource?.group + ? { + style: { + backgroundColor: '#3174ad', + }, + } + : { + style: { + backgroundColor: '#329f32', + }, + } + } + return (
- + {eventData.resource.committee.members.length > 0 && ( + <> + Komisja:{' '} +
    + {eventData.resource.committee.members.map((member: any) => ( +
  • + {member.first_name} {member.last_name} +
  • + ))} +
+ + )} + {eventData.resource.group && ( +

Grupa: {eventData.resource.group.name}

+ )} + {!eventData.resource.group && ( + <> +
+ + +
+ + + )} ) diff --git a/frontend/src/views/student/StudentSchedule.tsx b/frontend/src/views/student/StudentSchedule.tsx index 490f8f6..f7b8c2b 100644 --- a/frontend/src/views/student/StudentSchedule.tsx +++ b/frontend/src/views/student/StudentSchedule.tsx @@ -2,11 +2,12 @@ import { Calendar, luxonLocalizer, Views } from 'react-big-calendar' import { DateTime, Settings } from 'luxon' import { useCallback, useState } from 'react' import { useQuery } from 'react-query' -import { getStudentsSchedule } from '../../api/schedule' +import { getStudentsTermsOfDefences } from '../../api/schedule' import { useParams } from 'react-router-dom' import Modal from 'react-modal' import { useForm } from 'react-hook-form' import ScheduleAddGroup from './ScheduleAddGroup' +import bigCalendarTranslations from '../../utils/bigCalendarTranslations' const customStyles = { content: { @@ -54,26 +55,27 @@ const StudentSchedule = () => { const { refetch } = useQuery( ['studentSchedules'], - () => getStudentsSchedule(Number(id)), + () => getStudentsTermsOfDefences(Number(id)), { onSuccess: (data) => { setEvents( - data.data.enrollments.map( + data.data.term_of_defences.map( ({ id, start_date, end_date, title = 'Obrona', - committee, + members_of_committee, group, }) => { return { id, - title: `Obrona ${group?.name ?? ''}`, + title: `${group?.name ?? '-'}`, start: new Date(start_date), end: new Date(end_date), resource: { - committee, + group, + members_of_committee, }, } }, @@ -107,6 +109,20 @@ const StudentSchedule = () => { } const onSubmit = async (data: any) => {} + const eventGetter = (event: any) => { + return event?.resource?.group + ? { + style: { + backgroundColor: '#3174ad', + }, + } + : { + style: { + backgroundColor: '#329f32', + }, + } + } + return (

@@ -122,6 +138,10 @@ const StudentSchedule = () => { events={events} onView={onView} view={view} + eventPropGetter={eventGetter} + min={DateTime.fromObject({ hour: 8, minute: 0 }).toJSDate()} + max={DateTime.fromObject({ hour: 16, minute: 0 }).toJSDate()} + messages={bigCalendarTranslations} />