From 2120ab969003d844b4f0957e9aa208d1d6812a57 Mon Sep 17 00:00:00 2001 From: adam-skowronek Date: Thu, 17 Nov 2022 21:36:36 +0100 Subject: [PATCH] Update login, update api requests, add schedule view for supervisors --- frontend/src/App.tsx | 2 + frontend/src/api/leaders.ts | 3 +- frontend/src/api/schedule.ts | 86 ++++-- frontend/src/api/yearGroups.ts | 25 ++ frontend/src/views/Login.tsx | 106 +++++++- frontend/src/views/coordinator/AddGroup.tsx | 4 +- frontend/src/views/coordinator/AddLeader.tsx | 46 +--- .../src/views/coordinator/EditSchedule.tsx | 4 +- frontend/src/views/coordinator/Leaders.tsx | 7 +- frontend/src/views/coordinator/Schedule.tsx | 60 +++- frontend/src/views/coordinator/Schedules.tsx | 21 +- .../src/views/student/ScheduleAddGroup.tsx | 19 +- .../src/views/student/StudentSchedule.tsx | 4 +- .../views/supervisor/SupervisorSchedule.tsx | 256 ++++++++++++++++++ .../views/supervisor/SupervisorSchedules.tsx | 2 +- 15 files changed, 544 insertions(+), 101 deletions(-) create mode 100644 frontend/src/api/yearGroups.ts create mode 100644 frontend/src/views/supervisor/SupervisorSchedule.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ba2683d..ec47ce0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,7 @@ import Schedule from './views/coordinator/Schedule' import SupervisorSchedules from './views/supervisor/SupervisorSchedules' import StudentSchedules from './views/student/StudentSchedules' import StudentSchedule from './views/student/StudentSchedule' +import SupervisorSchedule from './views/supervisor/SupervisorSchedule' const queryClient = new QueryClient({ defaultOptions: { @@ -55,6 +56,7 @@ function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/leaders.ts b/frontend/src/api/leaders.ts index 9f33f7f..54cda5a 100644 --- a/frontend/src/api/leaders.ts +++ b/frontend/src/api/leaders.ts @@ -14,7 +14,6 @@ export interface Leader { email: string limit_group: number count_groups: number - mode: number } export const getLeaders = ( @@ -32,7 +31,7 @@ export const getLeaders = ( { params }, ) -export const createLeader = (payload: Leader) => +export const createLeader = (payload: Partial) => axiosInstance.post('coordinator/project_supervisor/', payload) export const deleteLeader = (id: number) => diff --git a/frontend/src/api/schedule.ts b/frontend/src/api/schedule.ts index 7a26be2..a45e783 100644 --- a/frontend/src/api/schedule.ts +++ b/frontend/src/api/schedule.ts @@ -1,34 +1,45 @@ import axiosInstance from './axiosInstance' +interface TermOfDefences { + id: number + start_date: string + end_date: string + title: string + members_of_committee: { + members: { first_name: string; last_name: string }[] + } + group: { name: string } +} + export const getTermsOfDefences = (scheduleId: number) => { return axiosInstance.get<{ - term_of_defences: { - id: number - start_date: string - end_date: string - title: string - members_of_committee: { - members: { first_name: string; last_name: string }[] - } - group: { name: string } - }[] + term_of_defences: TermOfDefences[] }>(`coordinator/enrollments/${scheduleId}/term-of-defences/`) } -export const getStudentsTermsOfDefences = (scheduleId: number) => { +export const getStudentsTermsOfDefences = ( + scheduleId: number, + studentIndex: number, +) => { return axiosInstance.get<{ - term_of_defences: { - id: number - start_date: string - end_date: string - title: string - members_of_committee: { - members: { first_name: string; last_name: string }[] - } - group: { name: string } - }[] - }>(`students/examination-schedule/${scheduleId}/enrollments/`) + term_of_defences: TermOfDefences[] + }>( + `students/examination-schedule/${scheduleId}/enrollments?student_index=${studentIndex}`, + ) } + +export const getSupervisorTermsOfDefences = (scheduleId: number) => { + return axiosInstance.get<{ + term_of_defences: TermOfDefences[] + }>(`project_supervisor/${scheduleId}/term-of-defences?id=1`) //fix hardcode id +} + +export const getAvailabilityForSupervisor = (scheduleId: number) => { + return axiosInstance.get<{ + free_times: { id: number; start_date: string; end_date: string }[] + }>(`project_supervisor/${scheduleId}/temporary-availabilities?id=1`) //fix hardcode id +} + export const getSchedules = (year_group_id: number = 1) => { return axiosInstance.get<{ examination_schedules: { @@ -125,3 +136,34 @@ export const downloadSchedule = (scheduleId: number) => responseType: 'blob', }, ) + +export const addAvailability = ({ + start_date, + end_date, + scheduleId, + project_supervisor_id, +}: { + start_date: string + end_date: string + scheduleId: number + project_supervisor_id: number +}) => { + return axiosInstance.post(`project_supervisor/${scheduleId}/enrollments/`, { + start_date, + end_date, + project_supervisor_id, + }) +} + +export const setDateOfExaminationSchedule = ( + scheduleId: number, + payload: { + start_date_for_enrollment_students: string + end_date_for_enrollment_students: string + }, +) => { + return axiosInstance.put( + `coordinator/examination_schedule/${scheduleId}/date`, + payload, + ) +} diff --git a/frontend/src/api/yearGroups.ts b/frontend/src/api/yearGroups.ts new file mode 100644 index 0000000..1599d4a --- /dev/null +++ b/frontend/src/api/yearGroups.ts @@ -0,0 +1,25 @@ +import axiosInstance from './axiosInstance' + +export interface CreateYearGroup { + name: string + mode: string +} + +export interface YearGroup { + id: number + mode: string + name: string +} + +export const getYearGroups = ( + params: Partial<{ + page: number + per_page: number + }>, +) => + axiosInstance.get<{ max_pages: number; year_groups: YearGroup[] }>( + `coordinator/year-group`, + { + params, + }, + ) diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index c126c1e..c9108ca 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -1,10 +1,71 @@ -import { NavLink } from 'react-router-dom' +import { useState } from 'react' +import { useQuery } from 'react-query' +import { NavLink, useNavigate } from 'react-router-dom' +import { InputActionMeta } from 'react-select' +import Select from 'react-select' import useLocalStorageState from 'use-local-storage-state' +import { getStudents } from '../api/students' +import { getYearGroups } from '../api/yearGroups' + +type SelectValue = { + value: string | number + label: string +} const Login = () => { + const navigate = useNavigate() + const [yearGroupId, setYearGroupId] = useLocalStorageState('yearGroupId', { defaultValue: 1, }) + const [studentId, setStudentId] = useLocalStorageState('studentId') + const [supervisorId, setSupervisorId] = useLocalStorageState('supervisorId', { + defaultValue: 1, + }) + const [studentOptions, setStudentOptions] = useState([]) + const [yearGroupOptions, setYearGroupOptions] = useState([]) + const [selectedYear, setSelectedYear] = useState() + const [selectedRole, setSelectedRole] = useState() + + useQuery( + 'students', + () => getStudents({ year_group_id: yearGroupId, per_page: 1000 }), + { + onSuccess: (data) => { + setStudentOptions( + data?.data.students.map(({ first_name, last_name, index }) => { + return { + value: index, + label: `${first_name} ${last_name} (${index})`, + } + }), + ) + }, + }, + ) + + useQuery('year_groups', () => getYearGroups({ per_page: 100 }), { + onSuccess: (data) => { + setYearGroupOptions( + data?.data.year_groups.map(({ name, id }) => { + return { + value: id, + label: name, + } + }), + ) + }, + }) + + const onStudentChange = (v: any) => { + setSelectedRole(v?.value) + setStudentId(v.value) + } + + const onYearChange = (v: any) => { + setSelectedYear(v?.value) + setYearGroupId(v?.value) + } return ( <> @@ -12,7 +73,36 @@ const Login = () => {

System PRI

-
+ + ({ + ...styles, + padding: '0.3rem', + borderRadius: '0.5rem', + }), + }} + /> + + {/*
@@ -23,11 +113,17 @@ const Login = () => { Hasło -
- +
*/} +
Koordynator - Student + {/* Student */} Opiekun
diff --git a/frontend/src/views/coordinator/AddGroup.tsx b/frontend/src/views/coordinator/AddGroup.tsx index 2b779ed..f626343 100644 --- a/frontend/src/views/coordinator/AddGroup.tsx +++ b/frontend/src/views/coordinator/AddGroup.tsx @@ -28,7 +28,7 @@ const AddGroup = () => { const { isLoading: areStudentsLoading } = useQuery( 'students', - () => getStudents({ per_page: 1000 }), + () => getStudents({ year_group_id: Number(yearGroupId), per_page: 1000 }), { onSuccess: (data) => { setStudentOptions( @@ -46,7 +46,7 @@ const AddGroup = () => { ) const { isLoading: areLeadersLoading } = useQuery( 'leaders', - () => getLeaders({ per_page: 1000 }), + () => getLeaders({ year_group_id: Number(yearGroupId), per_page: 1000 }), { onSuccess: (data) => { setSupervisorOptions( diff --git a/frontend/src/views/coordinator/AddLeader.tsx b/frontend/src/views/coordinator/AddLeader.tsx index 2e1d992..005705c 100644 --- a/frontend/src/views/coordinator/AddLeader.tsx +++ b/frontend/src/views/coordinator/AddLeader.tsx @@ -83,7 +83,7 @@ const AddLeader = () => { Email jest wymagany )} -
+ {/*
@@ -99,49 +99,7 @@ const AddLeader = () => { {errors.limit_group?.type === 'pattern' && ( Limit grup musi być liczbą dodatnią )} -
-
- -
- - -
-
- - -
-
- - -
- {errors.mode?.type === 'required' && ( - Wybierz tryb studiów - )} -
+
*/} ) diff --git a/frontend/src/views/coordinator/EditSchedule.tsx b/frontend/src/views/coordinator/EditSchedule.tsx index 522a2a7..d85141d 100644 --- a/frontend/src/views/coordinator/EditSchedule.tsx +++ b/frontend/src/views/coordinator/EditSchedule.tsx @@ -73,11 +73,11 @@ const EditSchedule = ({ {DateTime.fromJSDate(eventData.start).toFormat('yyyy-LL-dd HH:mm:ss')}{' '} - {DateTime.fromJSDate(eventData.end).toFormat('yyyy-LL-dd HH:mm:ss')}

- {eventData.resource.committee.members.length ? ( + {eventData.resource?.members_of_committee?.length ? ( <> Komisja:{' '}
    - {eventData.resource.committee.members.map((member: any) => ( + {eventData.resource.members_of_committee.map((member: any) => (
  • { Email Limit grup Liczba grup - Tryb + {/* Tryb */} @@ -98,7 +98,6 @@ const Leaders = () => { email, limit_group, count_groups, - mode, }) => ( {first_name} @@ -106,13 +105,13 @@ const Leaders = () => { {email} {limit_group} {count_groups} - + {/* {mode === 0 ? 'Stacjonarny' : mode === 1 ? 'Niestacjonarny' : 'Nie/stacjonarny'} - + */} + + @@ -74,9 +83,15 @@ const Schedules = () => { schedules?.data?.examination_schedules.map((schedule) => (

    -{' '} - + { + setScheduleData(schedule) + }} + > {schedule.title} + 1

    ))} diff --git a/frontend/src/views/student/ScheduleAddGroup.tsx b/frontend/src/views/student/ScheduleAddGroup.tsx index fbfdc76..3d2604e 100644 --- a/frontend/src/views/student/ScheduleAddGroup.tsx +++ b/frontend/src/views/student/ScheduleAddGroup.tsx @@ -2,6 +2,7 @@ import { DateTime } from 'luxon' import React, { useState } from 'react' import { useForm } from 'react-hook-form' import { useMutation } from 'react-query' +import useLocalStorageState from 'use-local-storage-state' import { assignGroup } from '../../api/schedule' const ScheduleAddGroup = ({ @@ -20,6 +21,7 @@ const ScheduleAddGroup = ({ const { register, handleSubmit, reset, control } = useForm<{ student_index: number }>({ mode: 'onBlur' }) + const [studentId] = useLocalStorageState('studentId') const { mutate: mutateAssignGroup } = useMutation( ['assignGroup'], @@ -34,7 +36,7 @@ const ScheduleAddGroup = ({ mutateAssignGroup({ scheduleId: Number(scheduleId), enrollmentId: eventData.id, - studentIndex: data.student_index, + studentIndex: Number(studentId), }) } @@ -46,11 +48,11 @@ const ScheduleAddGroup = ({ {DateTime.fromJSDate(eventData.start).toFormat('yyyy-LL-dd HH:mm:ss')}{' '} - {DateTime.fromJSDate(eventData.end).toFormat('yyyy-LL-dd HH:mm:ss')} - {eventData.resource.committee.members.length > 0 && ( + {eventData.resource?.members_of_committee?.length > 0 && ( <> Komisja:{' '}
      - {eventData.resource.committee.members.map((member: any) => ( + {eventData.resource.members_of_committee.map((member: any) => (
    • -
      - - -
      )} diff --git a/frontend/src/views/student/StudentSchedule.tsx b/frontend/src/views/student/StudentSchedule.tsx index f7b8c2b..30f18ed 100644 --- a/frontend/src/views/student/StudentSchedule.tsx +++ b/frontend/src/views/student/StudentSchedule.tsx @@ -8,6 +8,7 @@ import Modal from 'react-modal' import { useForm } from 'react-hook-form' import ScheduleAddGroup from './ScheduleAddGroup' import bigCalendarTranslations from '../../utils/bigCalendarTranslations' +import useLocalStorageState from 'use-local-storage-state' const customStyles = { content: { @@ -24,6 +25,7 @@ const StudentSchedule = () => { Settings.defaultZone = DateTime.local().zoneName Settings.defaultLocale = 'pl' + const [studentId] = useLocalStorageState('studentId') const { id } = useParams<{ id: string }>() const [events, setEvents] = useState< { @@ -55,7 +57,7 @@ const StudentSchedule = () => { const { refetch } = useQuery( ['studentSchedules'], - () => getStudentsTermsOfDefences(Number(id)), + () => getStudentsTermsOfDefences(Number(id), Number(studentId)), { onSuccess: (data) => { setEvents( diff --git a/frontend/src/views/supervisor/SupervisorSchedule.tsx b/frontend/src/views/supervisor/SupervisorSchedule.tsx new file mode 100644 index 0000000..28424c8 --- /dev/null +++ b/frontend/src/views/supervisor/SupervisorSchedule.tsx @@ -0,0 +1,256 @@ +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 { + addAvailability, + getAvailabilityForSupervisor, + getSupervisorTermsOfDefences, +} from '../../api/schedule' +import { useParams } from 'react-router-dom' +import Modal from 'react-modal' +import { useForm } from 'react-hook-form' +import EditSchedule from '../coordinator/EditSchedule' +import useLocalStorageState from 'use-local-storage-state' +import bigCalendarTranslations from '../../utils/bigCalendarTranslations' + +const customStyles = { + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + }, +} +type SelectValue = { + value: string | number + label: string +} + +const SupervisorSchedule = () => { + Settings.defaultZone = DateTime.local().zoneName + Settings.defaultLocale = 'pl' + + const { id } = useParams<{ id: string }>() + const [yearGroupId] = useLocalStorageState('yearGroupId') + const [supervisorId] = useLocalStorageState('supervisorId') + const [events, setEvents] = useState< + { + id: number + title: string + start: Date + end: Date + resource: any + }[] + >([]) + const [selectedDate, setSelectedDate] = useState<{ + start: Date + end: Date + title: string + id: number + resource: any + }>() + const [view, setView] = useState(Views.MONTH) + const onView = useCallback((newView: any) => setView(newView), [setView]) + + const [isModalOpen, setIsModalOpen] = useState(false) + const [isEditModalOpen, setIsEditModalOpen] = useState(false) + Modal.setAppElement('#root') + + const { register, handleSubmit, reset } = useForm<{ + from: string + to: string + }>({ mode: 'onBlur' }) + + const { refetch, isSuccess } = useQuery( + ['schedules'], + () => getSupervisorTermsOfDefences(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 { refetch: refetchAvailability } = useQuery( + ['availability'], + () => getAvailabilityForSupervisor(Number(id)), + { + onSuccess: (data) => { + setEvents([ + ...events, + ...data.data.free_times.map(({ id, start_date, end_date }) => { + return { + id, + title: 'Moja dostępność', + start: new Date(start_date), + end: new Date(end_date), + resource: {}, + } + }), + ]) + }, + enabled: isSuccess, + }, + ) + const { mutateAsync: mutateAddAvailability } = useMutation( + ['createEvent'], + (data: { + project_supervisor_id: number + start_date: string + end_date: string + scheduleId: number + }) => addAvailability(data), + ) + + const handleSelectSlot = async (event: any) => { + setSelectedDate(event) + if (view === Views.MONTH) { + setIsModalOpen(true) + } + } + + const handleSelectEvent = useCallback( + (event: { + id: number + title: string + start: Date + end: Date + resource: any + }) => { + setSelectedDate(event) + setIsEditModalOpen(true) + }, + [], + ) + + function closeModal() { + setIsModalOpen(false) + } + const onSubmit = async (data: any) => { + if (selectedDate && view === Views.MONTH) { + const from = data.from.split(':') + const to = data.to.split(':') + await mutateAddAvailability({ + start_date: DateTime.fromJSDate(selectedDate.start) + .set({ hour: from[0], minute: from[1] }) + .toFormat('yyyy-LL-dd HH:mm:ss'), + end_date: DateTime.fromJSDate(selectedDate.start) + .set({ hour: to[0], minute: to[1] }) + .toFormat('yyyy-LL-dd HH:mm:ss'), + scheduleId: Number(id), + project_supervisor_id: Number(supervisorId), + }) + setEvents([]) + refetch() + refetchAvailability() + reset() + closeModal() + } + } + + const eventGetter = (event: any) => { + return event?.title === '-' + ? { + style: { + backgroundColor: '#3174ad', + }, + } + : { + style: { + backgroundColor: '#329f32', + }, + } + } + + return ( +
      + + + +
      +

      Dostępne godziny

      +
      + + + + +
      + +
      +
      + setIsEditModalOpen(false)} + contentLabel="modal" + style={customStyles} + > + {selectedDate && id ? ( + + ) : null} + +
      + ) +} + +export default SupervisorSchedule diff --git a/frontend/src/views/supervisor/SupervisorSchedules.tsx b/frontend/src/views/supervisor/SupervisorSchedules.tsx index 79fdb90..2c1c80c 100644 --- a/frontend/src/views/supervisor/SupervisorSchedules.tsx +++ b/frontend/src/views/supervisor/SupervisorSchedules.tsx @@ -12,7 +12,7 @@ const SupervisorSchedules = () => { schedules?.data?.examination_schedules.map((schedule) => (

      -{' '} - + {schedule.title}