3.6 Add schedules view for students, coordinator can now assign to committees

This commit is contained in:
adam-skowronek 2022-11-04 01:18:26 +01:00
parent c71531ca10
commit 0cc9482878
14 changed files with 524 additions and 11 deletions

View File

@ -15,6 +15,7 @@ class GroupSchema(Schema):
class EnrollmentSchema(Schema): class EnrollmentSchema(Schema):
id = fields.Integer()
start_date = fields.DateTime() start_date = fields.DateTime()
end_date = fields.DateTime() end_date = fields.DateTime()
committee = fields.Nested(CommitteeSchema) committee = fields.Nested(CommitteeSchema)

View File

@ -16,6 +16,9 @@ import Student from './views/student/Student'
import Supervisor from './views/supervisor/Supervisor' import Supervisor from './views/supervisor/Supervisor'
import Schedules from './views/coordinator/Schedules' import Schedules from './views/coordinator/Schedules'
import Schedule from './views/coordinator/Schedule' import Schedule from './views/coordinator/Schedule'
import SupervisorSchedules from './views/supervisor/SupervisorSchedules'
import StudentSchedules from './views/student/StudentSchedules'
import StudentSchedule from './views/student/StudentSchedule'
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@ -45,10 +48,13 @@ function App() {
<Route path="student" element={<Student />}> <Route path="student" element={<Student />}>
<Route index element={<Navigate to="enrollment" />} /> <Route index element={<Navigate to="enrollment" />} />
<Route path="enrollment" element={<Enrollment />} /> <Route path="enrollment" element={<Enrollment />} />
<Route path="schedule" element={<StudentSchedules />} />
<Route path="schedule/:id" element={<StudentSchedule />} />
</Route> </Route>
<Route path="supervisor" element={<Supervisor />}> <Route path="supervisor" element={<Supervisor />}>
<Route index element={<Navigate to="groups" />} /> <Route index element={<Navigate to="groups" />} />
<Route path="groups" element={<Groups />} /> <Route path="groups" element={<Groups />} />
<Route path="schedule" element={<SupervisorSchedules />} />
</Route> </Route>
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>

View File

@ -7,12 +7,32 @@ export const getEvents = (scheduleId: number) => {
start_date: string start_date: string
end_date: string end_date: string
title: string title: string
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`, `http://127.0.0.1:5000/api/examination_schedule/enrollments/${scheduleId}/coordinator-view/?per_page=10000`,
) )
} }
export const getStudentsSchedule = (scheduleId: number) => {
return axiosInstance.get<{
enrollments: {
id: number
start_date: string
end_date: string
title: string
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`,
)
}
export const getSchedules = () => { export const getSchedules = () => {
return axiosInstance.get<{ return axiosInstance.get<{
examination_schedules: { examination_schedules: {
@ -72,3 +92,37 @@ export const setEventDate = ({
}, },
) )
} }
export const assignGroup = ({
scheduleId,
enrollmentId,
studentIndex,
}: {
scheduleId: number
enrollmentId: number
studentIndex: number
}) => {
return axiosInstance.post(
`http://127.0.0.1:5000/api/students/${scheduleId}/enrollments/${enrollmentId}/`,
{
student_index: studentIndex,
},
)
}
export const assignSupervisor = ({
scheduleId,
enrollmentId,
supervisorId,
}: {
scheduleId: number
enrollmentId: number
supervisorId: number
}) => {
return axiosInstance.post(
`http://127.0.0.1:5000/api/project_supervisor/${scheduleId}/enrollments/${enrollmentId}/`,
{
project_supervisor_id: supervisorId,
},
)
}

View File

@ -23,7 +23,7 @@ const Login = () => {
<div className="flex flex-col mt-3"> <div className="flex flex-col mt-3">
<NavLink to="/coordinator">Koordynator</NavLink> <NavLink to="/coordinator">Koordynator</NavLink>
<NavLink to="/student">Student</NavLink> <NavLink to="/student">Student</NavLink>
<span>Opiekun</span> <NavLink to="/supervisor">Opiekun</NavLink>
</div> </div>
</div> </div>
</> </>

View File

@ -0,0 +1,108 @@
import { useState } from 'react'
import { Controller, NestedValue, useForm } from 'react-hook-form'
import { useMutation, useQuery } from 'react-query'
import Select from 'react-select'
import { getLeaders } from '../../api/leaders'
import { assignSupervisor } from '../../api/schedule'
type SelectValue = {
value: string | number
label: string
}
const EditSchedule = ({
eventData,
scheduleId,
}: {
eventData: {
start: Date
end: Date
title: string
id: number
resource: any
}
scheduleId: string
}) => {
const { register, handleSubmit, reset, control } = useForm<{
committee: NestedValue<any[]>
}>({ mode: 'onBlur' })
const [committeeOptions, setCommitteeOptions] = useState<SelectValue[]>([])
const { isLoading: areLeadersLoading } = useQuery(
'leaders',
() => getLeaders({ per_page: 1000 }),
{
onSuccess: (data) => {
setCommitteeOptions(
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}`,
})),
)
},
},
)
const { mutate: mutateAssignSupervisor } = useMutation(
['assignSupervisor'],
(data: {
scheduleId: number
enrollmentId: number
supervisorId: number
}) => assignSupervisor(data),
)
const onSubmit = (data: any) => {
data?.committee?.forEach((id: number) => {
mutateAssignSupervisor({
scheduleId: Number(scheduleId),
enrollmentId: eventData.id,
supervisorId: id,
})
})
}
return (
<div style={{ width: '300px', height: '250px' }}>
<form className="w-full flex flex-col " onSubmit={handleSubmit(onSubmit)}>
<h3>Termin</h3>
<div className="form-control">
<div className="form-control">
<label className="label" htmlFor="committee">
Komisja
</label>
<Controller
control={control}
name="committee"
rules={{ required: true }}
render={({ field: { onChange, onBlur } }) => (
<Select
closeMenuOnSelect={false}
options={committeeOptions}
placeholder="Wybierz komisje"
isMulti
onChange={(values) =>
onChange(values.map((value) => value.value))
}
onBlur={onBlur}
styles={{
control: (styles) => ({
...styles,
padding: '0.3rem',
borderRadius: '0.5rem',
}),
}}
/>
)}
/>
</div>
</div>
<button className="btn btn-success mt-10">ZAPISZ</button>
</form>
</div>
)
}
export default EditSchedule

View File

@ -6,6 +6,7 @@ import { createEvent, getEvents } from '../../api/schedule'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import Modal from 'react-modal' import Modal from 'react-modal'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import EditSchedule from './EditSchedule'
const customStyles = { const customStyles = {
content: { content: {
@ -29,6 +30,7 @@ const Schedule = () => {
title: string title: string
start: Date start: Date
end: Date end: Date
resource: any
}[] }[]
>([]) >([])
const [selectedDate, setSelectedDate] = useState<{ const [selectedDate, setSelectedDate] = useState<{
@ -36,11 +38,13 @@ const Schedule = () => {
end: Date end: Date
title: string title: string
id: number id: number
resource: any
}>() }>()
const [view, setView] = useState(Views.MONTH) const [view, setView] = useState(Views.MONTH)
const onView = useCallback((newView: any) => setView(newView), [setView]) const onView = useCallback((newView: any) => setView(newView), [setView])
const [isModalOpen, setIsModalOpen] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
Modal.setAppElement('#root') Modal.setAppElement('#root')
const { register, handleSubmit, reset } = useForm<{ const { register, handleSubmit, reset } = useForm<{
@ -52,12 +56,22 @@ const Schedule = () => {
onSuccess: (data) => { onSuccess: (data) => {
setEvents( setEvents(
data.data.enrollments.map( data.data.enrollments.map(
({ id, start_date, end_date, title = 'Obrona' }) => { ({
id,
start_date,
end_date,
title = 'Obrona',
committee,
group,
}) => {
return { return {
id, id,
title, title: `Obrona ${group?.name ?? ''}`,
start: new Date(start_date), start: new Date(start_date),
end: new Date(end_date), end: new Date(end_date),
resource: {
committee,
},
} }
}, },
), ),
@ -89,8 +103,16 @@ const Schedule = () => {
} }
const handleSelectEvent = useCallback( const handleSelectEvent = useCallback(
(event: { id: number; title: string; start: Date; end: Date }) => (event: {
window.alert(event.title), id: number
title: string
start: Date
end: Date
resource: any
}) => {
setSelectedDate(event)
setIsEditModalOpen(true)
},
[], [],
) )
@ -165,6 +187,16 @@ const Schedule = () => {
<button className="btn btn-success mt-4">Dodaj termin</button> <button className="btn btn-success mt-4">Dodaj termin</button>
</form> </form>
</Modal> </Modal>
<Modal
isOpen={isEditModalOpen}
onRequestClose={() => setIsEditModalOpen(false)}
contentLabel="modal"
style={customStyles}
>
{selectedDate && id ? (
<EditSchedule eventData={selectedDate} scheduleId={id} />
) : null}
</Modal>
</div> </div>
) )
} }

View File

@ -45,9 +45,11 @@ const Schedules = () => {
</div> </div>
<button className="btn btn-success mt-4">Stwórz zapisy</button> <button className="btn btn-success mt-4">Stwórz zapisy</button>
</form> </form>
<h2 className="text-2xl font-bold mb-2">Wybierz zapisy:</h2>
{schedules && {schedules &&
schedules?.data?.examination_schedules.map((schedule) => ( schedules?.data?.examination_schedules.map((schedule) => (
<h3 className="text-xl font-bold" key={schedule.title}> <h3 className="text-xl " key={schedule.title}>
-{' '}
<Link to={`/coordinator/schedule/${schedule.id}`}> <Link to={`/coordinator/schedule/${schedule.id}`}>
{schedule.title} {schedule.title}
</Link> </Link>

View File

@ -0,0 +1,77 @@
import { DateTime } from 'luxon'
import React, { useState } from 'react'
import { useForm } from 'react-hook-form'
import { useMutation } from 'react-query'
import { assignGroup } from '../../api/schedule'
const ScheduleAddGroup = ({
eventData,
scheduleId,
}: {
eventData: {
start: Date
end: Date
title: string
id: number
resource: any
}
scheduleId: string
}) => {
const { register, handleSubmit, reset, control } = useForm<{
student_index: number
}>({ mode: 'onBlur' })
const { mutate: mutateAssignGroup } = useMutation(
['assignGroup'],
(data: {
scheduleId: number
enrollmentId: number
studentIndex: number
}) => assignGroup(data),
)
const onSubmit = (data: any) => {
mutateAssignGroup({
scheduleId: Number(scheduleId),
enrollmentId: eventData.id,
studentIndex: data.student_index,
})
}
return (
<div>
<form className="w-full flex flex-col " onSubmit={handleSubmit(onSubmit)}>
<h3>
Termin{' '}
{DateTime.fromJSDate(eventData.start).toFormat('yyyy-LL-dd HH:mm:ss')}{' '}
- {DateTime.fromJSDate(eventData.end).toFormat('yyyy-LL-dd HH:mm:ss')}
</h3>
Komisja:{' '}
<ul className="list-disc">
{eventData.resource.committee.members.map((member: any) => (
<li
key={`${member.first_name} ${member.last_name}`}
className="ml-4"
>
{member.first_name} {member.last_name}
</li>
))}
</ul>
<div className="form-control">
<label className="label" htmlFor="student_index">
Indeks
</label>
<input
className="input input-bordered"
id="to"
type="text"
{...register('student_index', { required: true })}
/>
</div>
<button className="btn btn-success mt-4">ZAPISZ</button>
</form>
</div>
)
}
export default ScheduleAddGroup

View File

@ -4,7 +4,12 @@ import TopBar from '../../components/TopBar'
const Student = () => { const Student = () => {
return ( return (
<> <>
<TopBar routes={[{ name: 'Zapisy', path: '/student/enrollment' }]} /> <TopBar
routes={[
{ name: 'Zapisy', path: '/student/enrollment' },
{ name: 'Harmonogram', path: '/student/schedule' },
]}
/>
<div className="m-10"> <div className="m-10">
<Outlet /> <Outlet />
</div> </div>

View File

@ -0,0 +1,175 @@
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 { useParams } from 'react-router-dom'
import Modal from 'react-modal'
import { useForm } from 'react-hook-form'
import ScheduleAddGroup from './ScheduleAddGroup'
const customStyles = {
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)',
},
}
const StudentSchedule = () => {
Settings.defaultZone = DateTime.local().zoneName
Settings.defaultLocale = 'pl'
const { id } = useParams<{ id: string }>()
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.WEEK)
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 } = useQuery(
['studentSchedules'],
() => getStudentsSchedule(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 handleSelectSlot = async (event: any) => {
setSelectedDate(event)
}
const handleSelectEvent = useCallback(
(event: {
id: number
title: string
start: Date
end: Date
resource: any
}) => {
console.log(event)
setSelectedDate(event)
setIsEditModalOpen(true)
},
[],
)
function closeModal() {
setIsModalOpen(false)
}
const onSubmit = async (data: any) => {}
return (
<div>
<h1 className="text-2xl font-bold mb-2 text-center">
Wybierz i zatwierdź termin obrony dla swojej grupy
</h1>
<Calendar
localizer={luxonLocalizer(DateTime)}
startAccessor="start"
endAccessor="end"
style={{ height: '85vh' }}
onSelectEvent={handleSelectEvent}
onSelectSlot={handleSelectSlot}
events={events}
onView={onView}
view={view}
/>
<Modal
isOpen={isModalOpen}
onRequestClose={closeModal}
contentLabel="modal"
style={customStyles}
>
<form
className="w-full flex flex-col "
onSubmit={handleSubmit(onSubmit)}
>
<h3>Dostępne godziny</h3>
<div className="form-control">
<label className="label" htmlFor="from">
Od
</label>
<input
className="input input-bordered"
id="from"
type="text"
{...register('from', { required: true })}
/>
<label className="label" htmlFor="to">
Do
</label>
<input
className="input input-bordered"
id="to"
type="text"
{...register('to', { required: true })}
/>
</div>
<button className="btn btn-success mt-4">Dodaj termin</button>
</form>
</Modal>
<Modal
isOpen={isEditModalOpen}
onRequestClose={() => setIsEditModalOpen(false)}
contentLabel="modal"
style={customStyles}
>
{selectedDate && id ? (
<ScheduleAddGroup eventData={selectedDate} scheduleId={id} />
) : null}
</Modal>
</div>
)
}
export default StudentSchedule

View File

@ -0,0 +1,24 @@
import { useQuery } from 'react-query'
import { getSchedules } from '../../api/schedule'
import { Link } from 'react-router-dom'
const StudentSchedules = () => {
const { data: schedules } = useQuery(['getSchedules'], () => getSchedules())
return (
<div>
<h2 className="text-2xl font-bold mb-2">Wybierz zapisy:</h2>
{schedules &&
schedules?.data?.examination_schedules.map((schedule) => (
<h3 className="text-xl" key={schedule.title}>
-{' '}
<Link to={`/student/schedule/${schedule.id}`}>
{schedule.title}
</Link>
</h3>
))}
</div>
)
}
export default StudentSchedules

View File

@ -4,7 +4,12 @@ import TopBar from '../../components/TopBar'
const Supervisor = () => { const Supervisor = () => {
return ( return (
<> <>
<TopBar routes={[{ name: 'Grupy', path: '/supervisor/groups' }]} /> <TopBar
routes={[
{ name: 'Grupy', path: '/supervisor/groups' },
{ name: 'Harmonogram', path: '/supervisor/schedule' },
]}
/>
<div className="m-10"> <div className="m-10">
<Outlet /> <Outlet />
</div> </div>

View File

@ -0,0 +1,24 @@
import { useQuery } from 'react-query'
import { getSchedules } from '../../api/schedule'
import { Link } from 'react-router-dom'
const SupervisorSchedules = () => {
const { data: schedules } = useQuery(['getSchedules'], () => getSchedules())
return (
<div>
<h2 className="text-2xl font-bold mb-2">Wybierz zapisy:</h2>
{schedules &&
schedules?.data?.examination_schedules.map((schedule) => (
<h3 className="text-xl" key={schedule.title}>
-{' '}
<Link to={`/coordinator/schedule/${schedule.id}`}>
{schedule.title}
</Link>
</h3>
))}
</div>
)
}
export default SupervisorSchedules