diff --git a/backend/app/students/routes/enrollments.py b/backend/app/students/routes/enrollments.py index 990e15b..d8884aa 100644 --- a/backend/app/students/routes/enrollments.py +++ b/backend/app/students/routes/enrollments.py @@ -142,6 +142,8 @@ def list_term_of_defences(examination_schedule_id: int, data: dict) -> dict: if student is None: abort(404, "Not found student!") ################ + show_available = data.get('show_available') + term_of_defences = ( TermOfDefence.query.filter( TermOfDefence.examination_schedule_id == examination_schedule_id @@ -150,18 +152,19 @@ def list_term_of_defences(examination_schedule_id: int, data: dict) -> dict: .all() ) - term_of_defences = list( - filter( - lambda n: len( - [ - d.id - for d in n.members_of_committee - if d.id == student.groups[0].project_supervisor.id - ] + if show_available is True: + term_of_defences = list( + filter( + lambda n: len( + [ + d.id + for d in n.members_of_committee + if len(student.groups) > 0 and d.id == student.groups[0].project_supervisor.id + ] + ) + > 0, + term_of_defences, ) - > 0, - term_of_defences, ) - ) return {"term_of_defences": term_of_defences} diff --git a/backend/app/students/schemas.py b/backend/app/students/schemas.py index a6d438a..819aa9b 100644 --- a/backend/app/students/schemas.py +++ b/backend/app/students/schemas.py @@ -24,6 +24,7 @@ class ProjectSupervisorQuerySchema(Schema): class TemporaryStudentSchema(Schema): student_id = fields.Integer(required=True) + show_available = fields.Boolean(required=False) class ExaminationScheduleStudentSchema(Schema): diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 85938c0..2cd3cce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,6 +31,8 @@ import WorkloadStatistics from './views/coordinator/WorkloadStatistics' import { MainContext } from './context/MainContext' import WithAxios from './context/WithAxios' import BaseModal, { ModalData } from './components/BaseModal' +import StudentGroup from './views/student/StudentGroup' +import StudentGradeCard from './views/student/StudentGradeCard' require('dayjs/locale/pl') dayjs.locale('pl') @@ -85,6 +87,11 @@ function App() { } /> } /> } /> + } /> + } + /> }> } /> diff --git a/frontend/src/api/grades.ts b/frontend/src/api/grades.ts index 085d51a..511f1c9 100644 --- a/frontend/src/api/grades.ts +++ b/frontend/src/api/grades.ts @@ -31,3 +31,13 @@ export const getGradesForSupervisor = ( `project_supervisor/project-grade-sheet/group/${groupId}/?term=${term}&id=${supervisorId}`, ) } + +export const getGradesForStudent = ( + yearGroupId: number, + term: number, + studentId: number, +) => { + return axiosInstance.get<{ [key: string]: any }>( + `students/project-grade-sheet/year-group/${yearGroupId}/?term=${term}&student_id=${studentId}`, + ) +} diff --git a/frontend/src/api/schedule.ts b/frontend/src/api/schedule.ts index fec6147..a5ad2a1 100644 --- a/frontend/src/api/schedule.ts +++ b/frontend/src/api/schedule.ts @@ -31,24 +31,33 @@ export const getTermsOfDefencesWithGroups = (scheduleId: number) => { export const getStudentsTermsOfDefences = ( scheduleId: number, studentId: number, + showAvailable: boolean, ) => { return axiosInstance.get<{ term_of_defences: TermOfDefences[] }>( - `students/examination-schedule/${scheduleId}/enrollments?student_id=${studentId}`, + `students/examination-schedule/${scheduleId}/enrollments?student_id=${studentId}&show_available=${showAvailable}`, ) } -export const getSupervisorTermsOfDefences = (scheduleId: number) => { +export const getSupervisorTermsOfDefences = ( + scheduleId: number, + supervisorId: number, +) => { return axiosInstance.get<{ term_of_defences: TermOfDefences[] - }>(`project_supervisor/${scheduleId}/term-of-defences?id=1`) //fix hardcode id + }>(`project_supervisor/${scheduleId}/term-of-defences?id=${supervisorId}`) } -export const getAvailabilityForSupervisor = (scheduleId: number) => { +export const getAvailabilityForSupervisor = ( + scheduleId: number, + supervisorId: number, +) => { return axiosInstance.get<{ free_times: { id: number; start_date: string; end_date: string }[] - }>(`project_supervisor/${scheduleId}/temporary-availabilities?id=1`) //fix hardcode id + }>( + `project_supervisor/${scheduleId}/temporary-availabilities?id=${supervisorId}`, + ) } export const getAvailabilityForCoordinator = (scheduleId: number) => { @@ -221,3 +230,18 @@ export const geWorkloadStatistics = (scheduleId: number) => { }[] }>(`coordinator/examination_schedule/${scheduleId}/workloads/`) } + +export const supervisorDeleteAvailability = ( + scheduleId: number, + availabilityId: number, + supervisorId: number, +) => { + return axiosInstance.delete( + `project_supervisor/${scheduleId}/enrollments/${availabilityId}/`, + { + data: { + id: supervisorId, + }, + }, + ) +} diff --git a/frontend/src/api/students.ts b/frontend/src/api/students.ts index d7f907d..ff8644d 100644 --- a/frontend/src/api/students.ts +++ b/frontend/src/api/students.ts @@ -54,3 +54,6 @@ export const downloadStudents = (mode: boolean, year_group_id: number) => responseType: 'blob', }, ) + +export const getStudentDetails = (id: number) => + axiosInstance.get(`coordinator/students/${id}/detail/`) diff --git a/frontend/src/components/ConfirmationModal.tsx b/frontend/src/components/ConfirmationModal.tsx new file mode 100644 index 0000000..49844df --- /dev/null +++ b/frontend/src/components/ConfirmationModal.tsx @@ -0,0 +1,40 @@ +import Modal from 'react-modal' + +const customStyles = { + content: { + top: '50%', + left: '50%', + right: 'auto', + bottom: 'auto', + marginRight: '-50%', + transform: 'translate(-50%, -50%)', + }, +} + +const ConfirmationModal = ({ + isOpen, + close, + confirmFn, +}: { + isOpen: boolean + close: () => void + confirmFn: () => void +}) => { + return ( + +
+

Czy na pewno chcesz usunąć?

+ +
+
+ ) +} + +export default ConfirmationModal diff --git a/frontend/src/views/GradeCard.tsx b/frontend/src/views/GradeCard.tsx index bc86880..64695a3 100644 --- a/frontend/src/views/GradeCard.tsx +++ b/frontend/src/views/GradeCard.tsx @@ -16,44 +16,77 @@ const GradeCard = () => { const { handleSubmit, setValue, control, watch, getValues } = useForm() const { id } = useParams<{ id: string }>() const [supervisorId] = useLocalStorageState('userId') + const [userType] = useLocalStorageState('userType') + + const { + data: groupDetails, + refetch: refetchGroup, + isLoading: isGroupLoading, + isSuccess, + } = useQuery(['getGroup', id], () => getGroup(Number(id))) useQuery( ['getGradesFirst'], - () => getGradesForSupervisor(Number(id), 1, Number(supervisorId)), + () => + getGradesForSupervisor( + Number(id), + 1, + userType === 'coordinator' + ? Number(groupDetails?.data?.project_supervisor?.id) + : Number(supervisorId), + ), { onSuccess: (data) => { for (const [key, value] of Object.entries(data.data)) { if (key !== 'id') setValue(key, value) } }, + enabled: isSuccess, }, ) useQuery( ['getGradesSecond'], - () => getGradesForSupervisor(Number(id), 2, Number(supervisorId)), + () => + getGradesForSupervisor( + Number(id), + 2, + userType === 'coordinator' + ? Number(groupDetails?.data?.project_supervisor?.id) + : Number(supervisorId), + ), { onSuccess: (data) => { for (const [key, value] of Object.entries(data.data)) { if (key !== 'id') setValue(key, value) } }, + enabled: isSuccess, }, ) - const { - data: groupDetails, - refetch: refetchGroup, - isLoading: isGroupLoading, - } = useQuery(['getGroup', id], () => getGroup(Number(id))) const { mutateAsync: mutateGradesFirstTerm } = useMutation( 'update_grades_first', - (payload: any) => updateGradesFirstTerm(1, 1, payload), + (payload: any) => + updateGradesFirstTerm( + Number(id), + userType === 'coordinator' + ? Number(groupDetails?.data?.project_supervisor?.id) + : Number(supervisorId), + payload, + ), ) const { mutateAsync: mutateGradesSecondTerm } = useMutation( 'update_grades_second', - (payload: any) => updateGradesSecondTerm(1, 1, payload), + (payload: any) => + updateGradesSecondTerm( + Number(id), + userType === 'coordinator' + ? Number(groupDetails?.data?.project_supervisor?.id) + : Number(supervisorId), + payload, + ), ) const onSubmit = async (data: any) => { diff --git a/frontend/src/views/coordinator/AvailabilitySchedule.tsx b/frontend/src/views/coordinator/AvailabilitySchedule.tsx index 87fdf11..bc2c1e8 100644 --- a/frontend/src/views/coordinator/AvailabilitySchedule.tsx +++ b/frontend/src/views/coordinator/AvailabilitySchedule.tsx @@ -37,7 +37,7 @@ const SupervisorSchedule = () => { ({ id, start_date, end_date, project_supervisor }) => { return { id, - title: `${project_supervisor.first_name} ${project_supervisor.last_name}`, + title: `${project_supervisor.first_name[0]}${project_supervisor.last_name[0]}`, start: new Date(start_date), end: new Date(end_date), resource: {}, diff --git a/frontend/src/views/coordinator/CoordinatorGroups.tsx b/frontend/src/views/coordinator/CoordinatorGroups.tsx index ecf9fa0..b8e59a5 100644 --- a/frontend/src/views/coordinator/CoordinatorGroups.tsx +++ b/frontend/src/views/coordinator/CoordinatorGroups.tsx @@ -6,6 +6,7 @@ import useLocalStorageState from 'use-local-storage-state' import { deleteGroup, getGroups } from '../../api/groups' import { ReactComponent as IconRemove } from '../../assets/svg/icon-remove.svg' import { Link } from 'react-router-dom' +import ConfirmationModal from '../../components/ConfirmationModal' const Groups = () => { let navigate = useNavigate() @@ -14,6 +15,8 @@ const Groups = () => { const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) const [yearGroupId] = useLocalStorageState('yearGroupId') + const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false) + const [groupToDeleteId, setGroupToDeleteId] = useState(0) const perPageOptions = [ { @@ -135,7 +138,12 @@ const Groups = () => { KARTA OCENY - @@ -181,6 +189,14 @@ const Groups = () => { + setIsConfirmModalOpen(false)} + confirmFn={() => { + mutateDelete(groupToDeleteId) + setIsConfirmModalOpen(false) + }} + /> ) } diff --git a/frontend/src/views/coordinator/Group.tsx b/frontend/src/views/coordinator/Group.tsx index b35741a..c928c91 100644 --- a/frontend/src/views/coordinator/Group.tsx +++ b/frontend/src/views/coordinator/Group.tsx @@ -6,6 +6,7 @@ import useLocalStorageState from 'use-local-storage-state' import { CreateGroup, editGroup, getGroup } from '../../api/groups' import { getStudents } from '../../api/students' import { ReactComponent as IconRemove } from '../../assets/svg/icon-remove.svg' +import ConfirmationModal from '../../components/ConfirmationModal' type SelectValue = { value: string | number @@ -44,6 +45,8 @@ const Group = () => { }, ) const [newStudent, setNewStudent] = useState(0) + const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false) + const [studentsToDelete, setStudentsToDelete] = useState([]) const { mutate: mutateEditGroup } = useMutation( ['editGroup'], (data: { id: number; payload: CreateGroup }) => @@ -96,17 +99,14 @@ const Group = () => { + setIsConfirmModalOpen(false)} + confirmFn={() => { + if (name && project_supervisor) { + mutateEditGroup({ + id: Number(groupId), + payload: { + name, + project_supervisor_id: project_supervisor.id, + students: studentsToDelete, + }, + }) + setIsConfirmModalOpen(false) + } + }} + /> ) } diff --git a/frontend/src/views/coordinator/Schedule.tsx b/frontend/src/views/coordinator/Schedule.tsx index 1a2bce4..d684b8f 100644 --- a/frontend/src/views/coordinator/Schedule.tsx +++ b/frontend/src/views/coordinator/Schedule.tsx @@ -283,7 +283,7 @@ const Schedule = () => { ROZPOCZNIJ @@ -228,6 +220,14 @@ const Students = () => { » + setIsConfirmModalOpen(false)} + confirmFn={() => { + mutateDelete(studentToDeleteId) + setIsConfirmModalOpen(false) + }} + /> )} diff --git a/frontend/src/views/student/Enrollment.tsx b/frontend/src/views/student/Enrollment.tsx index 7c765dd..43717b2 100644 --- a/frontend/src/views/student/Enrollment.tsx +++ b/frontend/src/views/student/Enrollment.tsx @@ -62,7 +62,6 @@ const Enrollment = () => { Imię Nazwisko Email - Tryb Wolne miejsca @@ -76,7 +75,6 @@ const Enrollment = () => { {first_name} {last_name} {email} - {mode ? 'Stacjonarny' : 'Niestacjonarny'} {available_groups} ))} diff --git a/frontend/src/views/student/Student.tsx b/frontend/src/views/student/Student.tsx index 0ad0c66..81c7a79 100644 --- a/frontend/src/views/student/Student.tsx +++ b/frontend/src/views/student/Student.tsx @@ -1,16 +1,46 @@ -import { Outlet } from 'react-router-dom' +import { useState } from 'react' +import { useQuery } from 'react-query' +import { Outlet, useNavigate } from 'react-router-dom' +import useLocalStorageState from 'use-local-storage-state' +import { getStudentDetails } from '../../api/students' import TopBar from '../../components/TopBar' const Student = () => { + const [studentId] = useLocalStorageState('userId') + const [routes, setRoutes] = useState< + { + name: string + path: string + }[] + >([{ name: 'Zapisy', path: '/student/enrollment' }]) + let navigate = useNavigate() + + useQuery( + ['getStudent', studentId], + () => getStudentDetails(Number(studentId)), + { + onSuccess: (data) => { + if (data?.data.groups?.length) { + navigate(`/student/groups/${data?.data.groups[0].id}`) + } + setRoutes( + data?.data.groups?.length + ? [ + { + name: 'Grupa', + path: `/student/groups/${data?.data.groups[0].id}`, + }, + { name: 'Harmonogram', path: '/student/schedule' }, + ] + : [{ name: 'Zapisy', path: '/student/enrollment' }], + ) + }, + }, + ) + return ( <> - +
diff --git a/frontend/src/views/student/StudentGradeCard.tsx b/frontend/src/views/student/StudentGradeCard.tsx new file mode 100644 index 0000000..242a3b1 --- /dev/null +++ b/frontend/src/views/student/StudentGradeCard.tsx @@ -0,0 +1,1331 @@ +import { useForm, Controller } from 'react-hook-form' +import { useMutation, useQuery } from 'react-query' +import { useParams } from 'react-router-dom' +import ReactSelect from 'react-select' +import useLocalStorageState from 'use-local-storage-state' +import { getGradesForStudent } from '../../api/grades' +import { getGroup } from '../../api/groups' +import { gradeOptions } from '../../utils/gradeCard' +import styles from '../GradeCard.module.css' + +const StudentGradeCard = () => { + const { handleSubmit, setValue, control, watch, getValues } = useForm() + const { id } = useParams<{ id: string }>() + const [studentId] = useLocalStorageState('userId') + const [yearGroupId] = useLocalStorageState('yearGroupId') + + const { + data: groupDetails, + refetch: refetchGroup, + isLoading: isGroupLoading, + isSuccess, + } = useQuery(['getGroup', id], () => getGroup(Number(id))) + + useQuery( + ['getGradesFirstStudent'], + () => getGradesForStudent(Number(yearGroupId), 1, Number(studentId)), + { + onSuccess: (data) => { + for (const [key, value] of Object.entries(data.data)) { + if (key !== 'id') setValue(key, value) + } + }, + enabled: isSuccess, + }, + ) + + useQuery( + ['getGradesSecondStudent'], + () => getGradesForStudent(Number(yearGroupId), 2, Number(studentId)), + { + onSuccess: (data) => { + for (const [key, value] of Object.entries(data.data)) { + if (key !== 'id') setValue(key, value) + } + }, + enabled: isSuccess, + }, + ) + + const gradeColor: { [key: number]: string } = { + 0: '#f8696b', + 1: 'orange', + 3: '#9cec9c', + 4: '#63BE7B', + } + + const getStyles = (value: number) => { + return { + control: (styles: any) => ({ + ...styles, + padding: '0.3rem', + borderRadius: '0.5rem', + width: '300px', + borderColor: gradeColor[value], + borderWidth: 3, + }), + singleValue: (styles: any) => ({ + ...styles, + overflow: 'visible', + textOverflow: 'initial', + whiteSpace: 'initial', + display: '-webkit-box', + '-webkit-line-clamp': '2', + '-webkit-box-orient': 'vertical', + }), + placeholder: (styles: any) => ({ + ...styles, + color: 'black', + display: '-webkit-box', + '-webkit-line-clamp': '2', + '-webkit-box-orient': 'vertical', + }), + } + } + + const SumRow = () => { + return ( + + + Suma uzyskanych punktów: + + + {groupDetails?.data?.points_for_first_term}% + + + {groupDetails?.data?.points_for_second_term}% + + + ) + } + + return ( +
+
+ {/*
+ +
*/} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
KryteriumPunkty I semestrPunkty II semestr
+

Prezentacja

+
+ Czy prezentacja zawierała wszystkie wymagane treści (cel i + założenia projektowe, podział prac na semestry, przegląd + zrealizowanych funkcjonalności, architektura i użyte + technologie, role osób w zespole, demo systemu)? * (oceniane + podczas obrony semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Czy prezentacja (omówienie projektu i jego demonstracja) była + przeprowadzona zgodnie ze sztuką? (oceniane podczas obrony + semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Demonstracja systemu * (oceniane podczas obrony semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Odpowiedzi na pytania komisji (oceniane podczas obrony + semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ {' '} +

Dokumentacja

+
Dokument wizji projektu * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Dokument wymagań projektowych * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Dokumentacja dla klienta/grupy docelowej/użytkownika + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Dokumentacja deweloperska + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Licencja i podział praw własności + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ {' '} +

Praca grupy w semestrze

+
  
Systematyczność pracy w semestrze * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Podział prac w semestrze * (obowiązkowe w II sem) + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Kontakt z klientem/grupą docelową, testy w grupie docelowej + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Zarządzanie ryzykiem i zakresem projektu + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Metodyka pracy i narzędzia ją wspierające * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Zarządzanie kodem źródłowym * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
DevOps + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + />{' '} +
+ {' '} +

Produkty projektu

+
  
+ Czy złożoność produktu projektu odpowiada wielkości zespołu? Czy + jakość produktu odpowiada wielkości projektu * + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Dostęp do produktu projektu dla komisji do testów podczas + prezentacji * (obowiązkowe w II sem) (oceniane podczas obrony + semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Brak krytycznych błędów w tym: bezpieczeństwa oraz + uniemożliwiających korzystanie z systemu * (oceniane podczas + obrony semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Dostęp do produktu projektu dla komisji do testów tydzień przed + prezentacją (oceniane podczas obrony semestralnej) + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Spełnienie kryteriów akceptacji * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Czy wdrożone zostały wszystkie zakładane na dany semestr + funkcjonalności (ocenia prowadzący)? + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Czy projekt dobrze rokuje? + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Czy projekt został wdrożony? + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Czy projekt jest użyteczny dla grupy docelowej; Czy spełnia + kryteria użyteczności i został przetestowany pod tym kątem? + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
Czy przygotowano prototyp projektu zgodnie ze sztuką? * + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Czy przeprowadzono i udokumentowano testy, w tym testy: + jednostkowe, integracyjne, akceptacyjne, obciążeniowe, + funkcjonalne (kryterium nie obejmuje testów użyteczności) oraz + statyczna analiza jakości kodu + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ Dobór techologii i architektury systemu do rozwiązywanego + problemu + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> + + ( + Number(v.value) === value) + ?.label + } + closeMenuOnSelect={true} + styles={getStyles(value)} + isDisabled={true} + /> + )} + /> +
+ {/*
+ +
*/} +
+
+ ) +} + +export default StudentGradeCard diff --git a/frontend/src/views/student/StudentGroup.tsx b/frontend/src/views/student/StudentGroup.tsx new file mode 100644 index 0000000..dbdac80 --- /dev/null +++ b/frontend/src/views/student/StudentGroup.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' +import { useQuery } from 'react-query' +import { Link, useLocation, useParams } from 'react-router-dom' +import ReactSelect from 'react-select' +import useLocalStorageState from 'use-local-storage-state' +import { getGroup } from '../../api/groups' +import { getStudents } from '../../api/students' +import { ReactComponent as IconRemove } from '../../assets/svg/icon-remove.svg' + +type SelectValue = { + value: string | number + label: string +} + +const StudentGroup = () => { + const { id: groupId } = useParams<{ id: string }>() + const location = useLocation() + + const { + data: groups, + refetch, + isLoading: isGroupLoading, + } = useQuery(['getGroup', groupId], () => getGroup(Number(groupId))) + const { name, project_supervisor, students } = groups?.data || {} + const [yearGroupId] = useLocalStorageState('yearGroupId') + + const [studentOptions, setStudentOptions] = useState([]) + const { isLoading: areStudentsLoading, refetch: refetchStudents } = useQuery( + 'students', + () => getStudents({ year_group_id: Number(yearGroupId), per_page: 1000 }), + { + onSuccess: (data) => { + setStudentOptions( + data?.data.students + .filter(({ groups }) => !groups?.length) + .map(({ first_name, last_name, index, id }) => { + return { + value: id, + label: `${first_name} ${last_name} (${index})`, + } + }), + ) + }, + }, + ) + + if (isGroupLoading || areStudentsLoading) { + return
Ładowanie
+ } + return ( +
+

{name}

+

+ Opiekun: {project_supervisor?.first_name}{' '} + {project_supervisor?.last_name} +

+ + + + + +
+ + + + + + + + + + {students?.map(({ first_name, last_name, index, id }) => ( + + + + + + ))} + +
ImięNazwiskoIndeks
{first_name}{last_name}{index}
+
+
+ ) +} +export default StudentGroup diff --git a/frontend/src/views/student/StudentSchedule.tsx b/frontend/src/views/student/StudentSchedule.tsx index 51248f2..604dcee 100644 --- a/frontend/src/views/student/StudentSchedule.tsx +++ b/frontend/src/views/student/StudentSchedule.tsx @@ -42,6 +42,7 @@ const StudentSchedule = () => { id: number resource: any }>() + const [showAvailable, setShowAvailable] = useState(false) const [view, setView] = useState(Views.WEEK) const onView = useCallback((newView: any) => setView(newView), [setView]) @@ -55,23 +56,35 @@ const StudentSchedule = () => { }>({ mode: 'onBlur' }) const { refetch } = useQuery( - ['studentSchedules'], - () => getStudentsTermsOfDefences(Number(id), Number(studentId)), + ['studentSchedules', showAvailable], + () => + getStudentsTermsOfDefences(Number(id), Number(studentId), showAvailable), { onSuccess: (data) => { setEvents( data.data.term_of_defences.map( - ({ - id, - start_date, - end_date, - title = 'Obrona', - members_of_committee, - group, - }) => { + ({ id, start_date, end_date, members_of_committee, group }) => { + let initials = '' + let title = '' + members_of_committee.forEach((member) => { + initials += + `${member.first_name} ${member.last_name}` + .split(' ') + .map((n) => n[0]) + .join('') + ' ' + }) + if (group?.name && initials) { + title = `${group.name} | ${initials}` + } else if (group?.name) { + title = group.name + } else if (initials) { + title = `- | ${initials}` + } else { + title = '-' + } return { id, - title: `${group?.name ?? '-'}`, + title: title, start: new Date(start_date), end: new Date(end_date), resource: { @@ -98,7 +111,6 @@ const StudentSchedule = () => { end: Date resource: any }) => { - console.log(event) setSelectedDate(event) setIsEditModalOpen(true) }, @@ -129,11 +141,23 @@ const StudentSchedule = () => {

Wybierz i zatwierdź termin obrony dla swojej grupy

+
+ +
{ Nazwa Opiekun - Semestr 1 + Punkty Semestr 1 Semestr 2 @@ -125,8 +125,8 @@ const SupervisorGroups = () => { {`${project_supervisor.first_name} ${project_supervisor.last_name}`} - {points_for_first_term} - {points_for_second_term} + {points_for_first_term}% + {points_for_second_term}%
{ id: number resource: any }>() - const [view, setView] = useState(Views.MONTH) + const [view, setView] = useState(Views.WEEK) const onView = useCallback((newView: any) => setView(newView), [setView]) const [isModalOpen, setIsModalOpen] = useState(false) + const [isWeekModalOpen, setIsWeekModalOpen] = useState(false) const [isEditModalOpen, setIsEditModalOpen] = useState(false) Modal.setAppElement('#root') @@ -65,7 +69,7 @@ const SupervisorSchedule = () => { const { refetch, isFetching } = useQuery( ['schedules'], - () => getSupervisorTermsOfDefences(Number(id)), + () => getSupervisorTermsOfDefences(Number(id), Number(supervisorId)), { onSuccess: (data) => { setEvents( @@ -96,7 +100,7 @@ const SupervisorSchedule = () => { const { refetch: refetchAvailability } = useQuery( ['availability'], - () => getAvailabilityForSupervisor(Number(id)), + () => getAvailabilityForSupervisor(Number(id), Number(supervisorId)), { onSuccess: (data) => { setEvents([ @@ -125,9 +129,32 @@ const SupervisorSchedule = () => { }) => addAvailability(data), ) + const { mutate: mutateDeleteAvailability } = useMutation( + ['deleteAvailability'], + (data: { + availabilityId: number + scheduleId: number + supervisorId: number + }) => + supervisorDeleteAvailability( + data.scheduleId, + data.availabilityId, + data.supervisorId, + ), + { + onSuccess: async () => { + setIsEditModalOpen(false) + await refetch() + await refetchAvailability() + }, + }, + ) + const handleSelectSlot = async (event: any) => { setSelectedDate(event) - if (view === Views.MONTH) { + if (view === Views.WEEK || view === Views.DAY) { + setIsWeekModalOpen(true) + } else if (view === Views.MONTH) { setIsModalOpen(true) } } @@ -149,6 +176,7 @@ const SupervisorSchedule = () => { function closeModal() { setIsModalOpen(false) } + const onSubmit = async (data: any) => { if (selectedDate && view === Views.MONTH) { const from = data.from.split(':') @@ -166,12 +194,26 @@ const SupervisorSchedule = () => { project_supervisor_id: Number(supervisorId), }) setEvents([]) - refetch() - refetchAvailability() + await refetch() + await refetchAvailability() reset() closeModal() } } + const submitSlot = async () => { + if (selectedDate && (view === Views.WEEK || view === Views.DAY)) { + await mutateAddAvailability({ + start_date: dayjs(selectedDate.start).format('YYYY-MM-DD HH:mm:ss'), + end_date: dayjs(selectedDate.end).format('YYYY-MM-DD HH:mm:ss'), + scheduleId: Number(id), + project_supervisor_id: Number(supervisorId), + }) + setEvents([]) + await refetch() + await refetchAvailability() + setIsWeekModalOpen(false) + } + } const eventGetter = (event: any) => { return event?.title === '-' @@ -205,7 +247,26 @@ const SupervisorSchedule = () => { max={dayjs().set('hour', 16).set('minute', 0).toDate()} messages={bigCalendarTranslations} /> - + setIsWeekModalOpen(false)} + contentLabel="modal" + style={customStyles} + > +

Dostępne godziny

+ {dayjs(selectedDate?.start).format('YYYY-MM-DD')} +
+

+ Od {dayjs(selectedDate?.start).format('HH:mm')} +

+

+ Do {dayjs(selectedDate?.end).format('HH:mm')} +

+
+ +
{ - {/* setIsEditModalOpen(false)} contentLabel="modal" style={customStyles} > {selectedDate && id ? ( - +
+

Dostępność

+

+ {dayjs(selectedDate.start).format('YYYY-MM-DD HH:mm')} -{' '} + {dayjs(selectedDate.end).format('HH:mm')} +

+ +
) : null} -
*/} +
) }