3.6 Add schedules view for students, coordinator can now assign to committees
This commit is contained in:
parent
c71531ca10
commit
0cc9482878
@ -15,6 +15,7 @@ class GroupSchema(Schema):
|
||||
|
||||
|
||||
class EnrollmentSchema(Schema):
|
||||
id = fields.Integer()
|
||||
start_date = fields.DateTime()
|
||||
end_date = fields.DateTime()
|
||||
committee = fields.Nested(CommitteeSchema)
|
||||
|
@ -16,6 +16,9 @@ import Student from './views/student/Student'
|
||||
import Supervisor from './views/supervisor/Supervisor'
|
||||
import Schedules from './views/coordinator/Schedules'
|
||||
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({
|
||||
defaultOptions: {
|
||||
@ -45,10 +48,13 @@ function App() {
|
||||
<Route path="student" element={<Student />}>
|
||||
<Route index element={<Navigate to="enrollment" />} />
|
||||
<Route path="enrollment" element={<Enrollment />} />
|
||||
<Route path="schedule" element={<StudentSchedules />} />
|
||||
<Route path="schedule/:id" element={<StudentSchedule />} />
|
||||
</Route>
|
||||
<Route path="supervisor" element={<Supervisor />}>
|
||||
<Route index element={<Navigate to="groups" />} />
|
||||
<Route path="groups" element={<Groups />} />
|
||||
<Route path="schedule" element={<SupervisorSchedules />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</QueryClientProvider>
|
||||
|
@ -7,12 +7,32 @@ export const getEvents = (scheduleId: 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}/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 = () => {
|
||||
return axiosInstance.get<{
|
||||
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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const Login = () => {
|
||||
<div className="flex flex-col mt-3">
|
||||
<NavLink to="/coordinator">Koordynator</NavLink>
|
||||
<NavLink to="/student">Student</NavLink>
|
||||
<span>Opiekun</span>
|
||||
<NavLink to="/supervisor">Opiekun</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
108
frontend/src/views/coordinator/EditSchedule.tsx
Normal file
108
frontend/src/views/coordinator/EditSchedule.tsx
Normal 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
|
@ -6,6 +6,7 @@ import { createEvent, getEvents } from '../../api/schedule'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import Modal from 'react-modal'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import EditSchedule from './EditSchedule'
|
||||
|
||||
const customStyles = {
|
||||
content: {
|
||||
@ -29,6 +30,7 @@ const Schedule = () => {
|
||||
title: string
|
||||
start: Date
|
||||
end: Date
|
||||
resource: any
|
||||
}[]
|
||||
>([])
|
||||
const [selectedDate, setSelectedDate] = useState<{
|
||||
@ -36,11 +38,13 @@ const Schedule = () => {
|
||||
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<{
|
||||
@ -52,12 +56,22 @@ const Schedule = () => {
|
||||
onSuccess: (data) => {
|
||||
setEvents(
|
||||
data.data.enrollments.map(
|
||||
({ id, start_date, end_date, title = 'Obrona' }) => {
|
||||
({
|
||||
id,
|
||||
start_date,
|
||||
end_date,
|
||||
title = 'Obrona',
|
||||
committee,
|
||||
group,
|
||||
}) => {
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
title: `Obrona ${group?.name ?? ''}`,
|
||||
start: new Date(start_date),
|
||||
end: new Date(end_date),
|
||||
resource: {
|
||||
committee,
|
||||
},
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -89,8 +103,16 @@ const Schedule = () => {
|
||||
}
|
||||
|
||||
const handleSelectEvent = useCallback(
|
||||
(event: { id: number; title: string; start: Date; end: Date }) =>
|
||||
window.alert(event.title),
|
||||
(event: {
|
||||
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>
|
||||
</form>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={isEditModalOpen}
|
||||
onRequestClose={() => setIsEditModalOpen(false)}
|
||||
contentLabel="modal"
|
||||
style={customStyles}
|
||||
>
|
||||
{selectedDate && id ? (
|
||||
<EditSchedule eventData={selectedDate} scheduleId={id} />
|
||||
) : null}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -45,9 +45,11 @@ const Schedules = () => {
|
||||
</div>
|
||||
<button className="btn btn-success mt-4">Stwórz zapisy</button>
|
||||
</form>
|
||||
<h2 className="text-2xl font-bold mb-2">Wybierz zapisy:</h2>
|
||||
{schedules &&
|
||||
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}`}>
|
||||
{schedule.title}
|
||||
</Link>
|
||||
|
77
frontend/src/views/student/ScheduleAddGroup.tsx
Normal file
77
frontend/src/views/student/ScheduleAddGroup.tsx
Normal 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
|
@ -4,7 +4,12 @@ import TopBar from '../../components/TopBar'
|
||||
const Student = () => {
|
||||
return (
|
||||
<>
|
||||
<TopBar routes={[{ name: 'Zapisy', path: '/student/enrollment' }]} />
|
||||
<TopBar
|
||||
routes={[
|
||||
{ name: 'Zapisy', path: '/student/enrollment' },
|
||||
{ name: 'Harmonogram', path: '/student/schedule' },
|
||||
]}
|
||||
/>
|
||||
<div className="m-10">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
175
frontend/src/views/student/StudentSchedule.tsx
Normal file
175
frontend/src/views/student/StudentSchedule.tsx
Normal 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
|
24
frontend/src/views/student/StudentSchedules.tsx
Normal file
24
frontend/src/views/student/StudentSchedules.tsx
Normal 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
|
@ -4,7 +4,12 @@ import TopBar from '../../components/TopBar'
|
||||
const Supervisor = () => {
|
||||
return (
|
||||
<>
|
||||
<TopBar routes={[{ name: 'Grupy', path: '/supervisor/groups' }]} />
|
||||
<TopBar
|
||||
routes={[
|
||||
{ name: 'Grupy', path: '/supervisor/groups' },
|
||||
{ name: 'Harmonogram', path: '/supervisor/schedule' },
|
||||
]}
|
||||
/>
|
||||
<div className="m-10">
|
||||
<Outlet />
|
||||
</div>
|
||||
|
24
frontend/src/views/supervisor/SupervisorSchedules.tsx
Normal file
24
frontend/src/views/supervisor/SupervisorSchedules.tsx
Normal 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
|
Loading…
Reference in New Issue
Block a user