Update login, update api requests, add schedule view for supervisors

This commit is contained in:
adam-skowronek 2022-11-17 21:36:36 +01:00
parent 8dcb7528f9
commit 2120ab9690
15 changed files with 544 additions and 101 deletions

View File

@ -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() {
<Route index element={<Navigate to="groups" />} />
<Route path="groups" element={<Groups />} />
<Route path="schedule" element={<SupervisorSchedules />} />
<Route path="schedule/:id" element={<SupervisorSchedule />} />
</Route>
</Routes>
</QueryClientProvider>

View File

@ -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<Leader>) =>
axiosInstance.post('coordinator/project_supervisor/', payload)
export const deleteLeader = (id: number) =>

View File

@ -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 }[]
term_of_defences: TermOfDefences[]
}>(
`students/examination-schedule/${scheduleId}/enrollments?student_index=${studentIndex}`,
)
}
group: { name: string }
}[]
}>(`students/examination-schedule/${scheduleId}/enrollments/`)
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,
)
}

View File

@ -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,
},
)

View File

@ -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<SelectValue[]>([])
const [yearGroupOptions, setYearGroupOptions] = useState<SelectValue[]>([])
const [selectedYear, setSelectedYear] = useState<any>()
const [selectedRole, setSelectedRole] = useState<any>()
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 = () => {
<h1 className="text-xl font-bold mx-auto">System PRI</h1>
</div>
<div className="w-full lg:w-1/4 flex flex-col mx-auto mt-20 px-10 py-5 bg-gray-300 rounded-lg shadow">
<div className="form-control">
<label className="label">Rok</label>
<Select
closeMenuOnSelect={true}
options={yearGroupOptions}
placeholder="Wybierz rok"
onChange={onYearChange}
styles={{
control: (styles) => ({
...styles,
padding: '0.3rem',
borderRadius: '0.5rem',
}),
}}
/>
<label className="label">Student</label>
<Select
closeMenuOnSelect={true}
options={studentOptions}
placeholder="Wybierz studenta"
onChange={onStudentChange}
styles={{
control: (styles) => ({
...styles,
padding: '0.3rem',
borderRadius: '0.5rem',
}),
}}
/>
{/* <div className="form-control">
<label className="label" htmlFor="login">
Login
</label>
@ -23,11 +113,17 @@ const Login = () => {
Hasło
</label>
<input className="input input-bordered" id="password" type="text" />
</div>
<button className="btn mt-5 text-lg">Zaloguj</button>
</div> */}
<button
className="btn mt-5 text-lg"
disabled={!selectedRole || !selectedYear}
onClick={() => navigate('/student')}
>
Zaloguj
</button>
<div className="flex flex-col mt-3">
<NavLink to="/coordinator">Koordynator</NavLink>
<NavLink to="/student">Student</NavLink>
{/* <NavLink to="/student">Student</NavLink> */}
<NavLink to="/supervisor">Opiekun</NavLink>
</div>
</div>

View File

@ -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(

View File

@ -83,7 +83,7 @@ const AddLeader = () => {
<InputError>Email jest wymagany</InputError>
)}
</div>
<div className="form-control">
{/* <div className="form-control">
<label className="label" htmlFor="limit_group">
Limit grup
</label>
@ -99,49 +99,7 @@ const AddLeader = () => {
{errors.limit_group?.type === 'pattern' && (
<InputError>Limit grup musi być liczbą dodatnią</InputError>
)}
</div>
<div className="form-control gap-2">
<label className="label">Tryb studiów</label>
<div className="flex gap-2">
<input
className="radio"
id="mode-1"
type="radio"
{...register('mode', {
required: true,
})}
value="0"
/>
<label htmlFor="mode-1">Stacjonarny</label>
</div>
<div className="flex gap-2">
<input
className="radio"
id="mode-0"
type="radio"
{...register('mode', {
required: true,
})}
value="1"
/>
<label htmlFor="mode-0">Niestacjonarny</label>
</div>
<div className="flex gap-2">
<input
className="radio"
id="mode-2"
type="radio"
{...register('mode', {
required: true,
})}
value="2"
/>
<label htmlFor="mode-2">Oba</label>
</div>
{errors.mode?.type === 'required' && (
<InputError>Wybierz tryb studiów</InputError>
)}
</div>
</div> */}
<button className="btn btn-success mt-4">Dodaj opiekuna</button>
</form>
)

View File

@ -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')}
</p>
{eventData.resource.committee.members.length ? (
{eventData.resource?.members_of_committee?.length ? (
<>
Komisja:{' '}
<ul className="list-disc">
{eventData.resource.committee.members.map((member: any) => (
{eventData.resource.members_of_committee.map((member: any) => (
<li
key={`${member.first_name} ${member.last_name}`}
className="ml-4"

View File

@ -85,7 +85,7 @@ const Leaders = () => {
<th>Email</th>
<th>Limit grup</th>
<th>Liczba grup</th>
<th>Tryb</th>
{/* <th>Tryb</th> */}
<th></th>
</tr>
</thead>
@ -98,7 +98,6 @@ const Leaders = () => {
email,
limit_group,
count_groups,
mode,
}) => (
<tr key={id}>
<td>{first_name}</td>
@ -106,13 +105,13 @@ const Leaders = () => {
<td>{email}</td>
<td>{limit_group}</td>
<td>{count_groups}</td>
<td>
{/* <td>
{mode === 0
? 'Stacjonarny'
: mode === 1
? 'Niestacjonarny'
: 'Nie/stacjonarny'}
</td>
</td> */}
<td>
<button onClick={() => mutateDelete(id)}>
<IconRemove />

View File

@ -1,11 +1,12 @@
import { Calendar, luxonLocalizer, Views } from 'react-big-calendar'
import { DateTime, Settings } from 'luxon'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useMutation, useQuery } from 'react-query'
import {
createEvent,
downloadSchedule,
getTermsOfDefences,
setDateOfExaminationSchedule,
} from '../../api/schedule'
import { useParams } from 'react-router-dom'
import Modal from 'react-modal'
@ -15,6 +16,7 @@ import Select from 'react-select'
import { getLeaders } from '../../api/leaders'
import useLocalStorageState from 'use-local-storage-state'
import bigCalendarTranslations from '../../utils/bigCalendarTranslations'
import DatePicker from 'react-date-picker'
const customStyles = {
content: {
@ -66,6 +68,12 @@ const Schedule = () => {
project_supervisors: NestedValue<any[]>
}>({ mode: 'onBlur' })
const [committeeOptions, setCommitteeOptions] = useState<SelectValue[]>([])
const [startDate, setStartDate] = useState(new Date())
const [endDate, setEndDate] = useState(new Date())
const [scheduleData, setScheduleData] = useLocalStorageState<{
start_date_for_enrollment_students: string
end_date_for_enrollment_students: string
}>('scheduleData')
const { isLoading: areLeadersLoading } = useQuery(
'leaders',
@ -194,6 +202,30 @@ const Schedule = () => {
}
}
const { mutate: mutateSetDateOfExaminationSchedule } = useMutation(
['dateOfExaminationSchedule'],
(data: {
start_date_for_enrollment_students: string
end_date_for_enrollment_students: string
}) => setDateOfExaminationSchedule(Number(id), data),
{
onSuccess: () => {
setScheduleData({
...scheduleData,
start_date_for_enrollment_students: startDate.toISOString(),
end_date_for_enrollment_students: endDate.toISOString(),
})
},
},
)
useEffect(() => {
if (scheduleData) {
setStartDate(new Date(scheduleData.start_date_for_enrollment_students))
setEndDate(new Date(scheduleData.end_date_for_enrollment_students))
}
}, [scheduleData])
const eventGetter = (event: any) => {
return event?.resource?.group
? {
@ -210,6 +242,32 @@ const Schedule = () => {
return (
<div className="flex flex-col">
<div>
Start zapisów dla studentów: <label htmlFor="end_date">Od </label>
<DatePicker
onChange={setStartDate}
value={startDate}
format={'yyyy-MM-dd'}
/>
<label htmlFor="end_date">Do </label>
<DatePicker
onChange={setEndDate}
value={endDate}
format={'yyyy-MM-dd'}
/>
<button
className="btn btn-success ml-2"
onClick={() =>
mutateSetDateOfExaminationSchedule({
start_date_for_enrollment_students: startDate.toISOString(),
end_date_for_enrollment_students: endDate.toISOString(),
})
}
>
ZAPISZ
</button>
</div>
<button
className="btn btn-success btn-xs md:btn-md self-end mb-4"
onClick={() => mutateDownload(Number(id))}

View File

@ -13,6 +13,7 @@ const Schedules = () => {
mode: 'onBlur',
})
const [yearGroupId] = useLocalStorageState('yearGroupId')
const [scheduleData, setScheduleData] = useLocalStorageState('scheduleData')
const [startDate, setStartDate] = useState(new Date())
const [endDate, setEndDate] = useState(new Date())
@ -59,13 +60,21 @@ const Schedules = () => {
Od
</label>
<DatePicker onChange={setStartDate} value={startDate} />
<DatePicker
onChange={setStartDate}
value={startDate}
format={'yyyy-MM-dd'}
/>
<label className="label" htmlFor="end_date">
Do
</label>
<DatePicker onChange={setEndDate} value={endDate} />
<DatePicker
onChange={setEndDate}
value={endDate}
format={'yyyy-MM-dd'}
/>
</div>
<button className="btn btn-success mt-4">Stwórz zapisy</button>
</form>
@ -74,9 +83,15 @@ const Schedules = () => {
schedules?.data?.examination_schedules.map((schedule) => (
<h3 className="text-xl " key={schedule.title}>
-{' '}
<Link to={`/coordinator/schedule/${schedule.id}`}>
<Link
to={`/coordinator/schedule/${schedule.id}`}
onClick={() => {
setScheduleData(schedule)
}}
>
{schedule.title}
</Link>
1
</h3>
))}
</div>

View File

@ -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')}
</h3>
{eventData.resource.committee.members.length > 0 && (
{eventData.resource?.members_of_committee?.length > 0 && (
<>
Komisja:{' '}
<ul className="list-disc">
{eventData.resource.committee.members.map((member: any) => (
{eventData.resource.members_of_committee.map((member: any) => (
<li
key={`${member.first_name} ${member.last_name}`}
className="ml-4"
@ -66,17 +68,6 @@ const ScheduleAddGroup = ({
)}
{!eventData.resource.group && (
<>
<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>
</>
)}

View File

@ -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(

View File

@ -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 (
<div className="flex flex-col">
<Calendar
localizer={luxonLocalizer(DateTime)}
startAccessor="start"
endAccessor="end"
selectable
style={{ height: '80vh' }}
onSelectEvent={handleSelectEvent}
onSelectSlot={handleSelectSlot}
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}
/>
<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 dostępność</button>
</form>
</Modal>
<Modal
isOpen={isEditModalOpen}
onRequestClose={() => setIsEditModalOpen(false)}
contentLabel="modal"
style={customStyles}
>
{selectedDate && id ? (
<EditSchedule eventData={selectedDate} scheduleId={id} />
) : null}
</Modal>
</div>
)
}
export default SupervisorSchedule

View File

@ -12,7 +12,7 @@ const SupervisorSchedules = () => {
schedules?.data?.examination_schedules.map((schedule) => (
<h3 className="text-xl" key={schedule.title}>
-{' '}
<Link to={`/coordinator/schedule/${schedule.id}`}>
<Link to={`/supervisor/schedule/${schedule.id}`}>
{schedule.title}
</Link>
</h3>