Fix export/import of students, add grade card view

This commit is contained in:
adam-skowronek 2022-12-16 01:01:07 +01:00
parent 5d94bba15f
commit 292ef17be6
9 changed files with 823 additions and 9 deletions

View File

@ -23,6 +23,8 @@ import SupervisorSchedule from './views/supervisor/SupervisorSchedule'
import Home from './views/coordinator/Home'
import SupervisorAvailabilities from './views/coordinator/SupervisorAvailabilities'
import AvailabilitySchedule from './views/coordinator/AvailabilitySchedule'
import GradeCard from './views/GradeCard'
import Group from './views/coordinator/Group'
const queryClient = new QueryClient({
defaultOptions: {
@ -41,6 +43,7 @@ function App() {
<Route path="coordinator" element={<Coordinator />}>
<Route index element={<Home />} />
<Route path="groups" element={<Groups />} />
<Route path="groups/:id" element={<Group />} />
<Route path="students" element={<Students />} />
<Route path="leaders" element={<Leaders />} />
<Route path="add-student" element={<AddStudent />} />
@ -52,6 +55,7 @@ function App() {
path="supervisors_availability"
element={<SupervisorAvailabilities />}
/>
<Route path="groups/:id/grade-card" element={<GradeCard />} />
<Route
path="supervisors_availability/:id"
element={<AvailabilitySchedule />}

View File

@ -0,0 +1,33 @@
import axiosInstance from './axiosInstance'
export const updateGradesFirstTerm = (
groupId: number,
supervisorId: number,
payload: any,
) => {
return axiosInstance.patch(
`project_supervisor/project-grade-sheet/group/${groupId}/first-term/?id=${supervisorId}`,
payload,
)
}
export const updateGradesSecondTerm = (
groupId: number,
supervisorId: number,
payload: any,
) => {
return axiosInstance.patch(
`project_supervisor/project-grade-sheet/group/${groupId}/second-term/?id=${supervisorId}`,
payload,
)
}
export const getGradesForSupervisor = (
groupId: number,
term: number,
supervisorId: number,
) => {
return axiosInstance.get<{ [key: string]: any }>(
`project_supervisor/project-grade-sheet/group/${groupId}/?term=${term}&id=${supervisorId}`,
)
}

View File

@ -38,3 +38,6 @@ export const createGroup = (year_group_id: number, payload: CreateGroup) =>
export const deleteGroup = (id: number) =>
axiosInstance.delete(`coordinator/groups/${id}/`)
export const getGroup = (id: number) =>
axiosInstance.get<Group>(`coordinator/groups/${id}/detail/`)

View File

@ -35,13 +35,21 @@ export const getStudents = (
export const createStudent = (payload: Student & { year_group_id: number }) =>
axiosInstance.post('coordinator/students/', payload)
export const uploadStudents = (payload: FormData) =>
axiosInstance.post('coordinator/students/upload/', payload)
export const uploadStudents = (payload: FormData, year_group_id: number) =>
axiosInstance.post(
`coordinator/students/upload/?id=${year_group_id}`,
payload,
)
export const deleteStudent = (index: number) =>
axiosInstance.delete(`coordinator/students/${index}/`)
export const downloadStudents = (mode: boolean) =>
axiosInstance.post(`coordinator/students/download/?mode=${Number(mode)}`, {
export const downloadStudents = (mode: boolean, year_group_id: number) =>
axiosInstance.post(
`coordinator/students/download/?mode=${Number(
mode,
)}&year_group_id=${year_group_id}`,
{
responseType: 'blob',
})
},
)

View File

@ -0,0 +1,13 @@
.grade-table td,
.grade-table th {
border: 1px solid lightgray;
}
.grade-table th {
background: #f2f2f2;
}
.grade-table input {
background: #f2f2f2;
padding: 1rem;
text-align: center;
width: 100px;
}

View File

@ -0,0 +1,721 @@
import { useForm } from 'react-hook-form'
import { useMutation, useQuery } from 'react-query'
import { useParams } from 'react-router-dom'
import useLocalStorageState from 'use-local-storage-state'
import {
getGradesForSupervisor,
updateGradesFirstTerm,
updateGradesSecondTerm,
} from '../api/grades'
import styles from './GradeCard.module.css' // Import css modules stylesheet as styles
const GradeCard = () => {
const { register, handleSubmit, setValue } = useForm()
const { id } = useParams<{ id: string }>()
const [supervisorId] = useLocalStorageState('supervisorId')
useQuery(
['getGradesFirst'],
() => getGradesForSupervisor(Number(id), 1, Number(supervisorId)),
{
onSuccess: (data) => {
for (const [key, value] of Object.entries(data.data)) {
if (key !== 'id') setValue(key, value)
}
},
},
)
useQuery(
['getGradesSecond'],
() => getGradesForSupervisor(Number(id), 2, Number(supervisorId)),
{
onSuccess: (data) => {
for (const [key, value] of Object.entries(data.data)) {
if (key !== 'id') setValue(key, value)
}
},
},
)
const { mutate: mutateGradesFirstTerm } = useMutation(
'update_grades_first',
(payload: any) => updateGradesFirstTerm(1, 1, payload),
)
const { mutate: mutateGradesSecondTerm } = useMutation(
'update_grades_second',
(payload: any) => updateGradesSecondTerm(1, 1, payload),
)
const onSubmit = (data: any) => {
let firstTermGrades: { [key: string]: any } = {}
let secondTermGrades: { [key: string]: any } = {}
for (const [key, value] of Object.entries(data)) {
if (key.endsWith('_1')) {
firstTermGrades[key] = Number(value)
} else {
secondTermGrades[key] = Number(value)
}
}
mutateGradesFirstTerm(firstTermGrades)
mutateGradesSecondTerm(secondTermGrades)
}
return (
<div className={styles['grade-table']}>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex justify-end">
<button className="btn btn-success mb-2">ZAPISZ</button>
</div>
<table className=" table-normal border-collapse">
<thead>
<tr>
<th>Kryterium</th>
<th>Waga I semestr</th>
<th>Waga II semestr</th>
<th>Punkty I semestr [0,1,3,4]</th>
<th>Punkty II semestr [0,1,3,4]</th>
<th>Udział I</th>
<th>Udział II</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<h3 className="text-xl font-bold">Prezentacja</h3>
</td>
<td>1,5</td>
<td>1,5</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>
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)
</td>
<td>3</td>
<td>1</td>
<td>
<input
type="text"
{...register('presentation_required_content_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('presentation_required_content_2', {})}
/>
</td>
<td>3,5%</td>
<td>1,2%</td>
</tr>
<tr>
<td>
Czy prezentacja (omówienie projektu i jego demonstracja) była
przeprowadzona zgodnie ze sztuką? (oceniane podczas obrony
semestralnej)
</td>
<td>4</td>
<td>4</td>
<td>
<input
type="text"
{...register('presentation_was_compatible_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('presentation_was_compatible_2', {})}
/>
</td>
<td>4,6%</td>
<td>4,6%</td>
</tr>
<tr>
<td>
Demonstracja systemu * (oceniane podczas obrony semestralnej)
</td>
<td>4</td>
<td>6</td>
<td>
<input
type="text"
{...register('presentation_showing_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('presentation_showing_2', {})}
/>
</td>
<td>4,6%</td>
<td>6,9%</td>
</tr>
<tr>
<td>
Odpowiedzi na pytania komisji (oceniane podczas obrony
semestralnej)
</td>
<td>2</td>
<td>2</td>
<td>
<input
type="text"
{...register(
'presentation_answers_to_questions_from_committee_1',
{},
)}
/>
</td>
<td>
<input
type="text"
{...register(
'presentation_answers_to_questions_from_committee_2',
{},
)}
/>
</td>
<td>2,3%</td>
<td>2,3%</td>
</tr>
{/* */}
<tr>
<td>
{' '}
<h3 className="text-xl font-bold">Dokumentacja</h3>
</td>
<td>2</td>
<td>1</td>
<td></td>
<td></td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>Dokument wizji projektu *</td>
<td>3</td>
<td>0</td>
<td>
<input
type="text"
{...register('documentation_project_vision_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('documentation_project_vision_2', {})}
/>
</td>
<td>7,5%</td>
<td>0,0%</td>
</tr>
<tr>
<td>Dokument wymagań projektowych *</td>
<td>3</td>
<td>2</td>
<td>
<input
type="text"
{...register('documentation_requirements_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('documentation_requirements_2', {})}
/>
</td>
<td>7,5%</td>
<td>2,9%</td>
</tr>
<tr>
<td>Dokumentacja dla klienta/grupy docelowej/użytkownika</td>
<td>0</td>
<td>2</td>
<td>
<input
type="text"
{...register('documentation_for_clients_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('documentation_for_clients_2', {})}
/>
</td>
<td>0,0%</td>
<td>2,9%</td>
</tr>
<tr>
<td>Dokumentacja deweloperska</td>
<td>1</td>
<td>2</td>
<td>
<input
type="text"
{...register('documentation_for_developers_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('documentation_for_developers_2', {})}
/>
</td>
<td>2,5%</td>
<td>2,9%</td>
</tr>
<tr>
<td>Licencja i podział praw własności</td>
<td>1</td>
<td>1</td>
<td>
<input
type="text"
{...register('documentation_license_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('documentation_license_2', {})}
/>
</td>
<td>2,5%</td>
<td>1,4%</td>
</tr>
{/* */}
<tr>
<td>Praca grupy w semestrze</td>
<td>3</td>
<td>3</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>Systematyczność pracy w semestrze *</td>
<td>5</td>
<td>5</td>
<td>
<input
type="text"
{...register('group_work_regularity_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_regularity_2', {})}
/>
</td>
<td>7,9%</td>
<td>6,5%</td>
</tr>
<tr>
<td>Podział prac w semestrze * (obowiązkowe w II sem)</td>
<td>3</td>
<td>3</td>
<td>
<input
type="text"
{...register('group_work_division_of_work_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_division_of_work_2', {})}
/>
</td>
<td>4,7%</td>
<td>3,9%</td>
</tr>
<tr>
<td>
Kontakt z klientem/grupą docelową, testy w grupie docelowej
</td>
<td>4</td>
<td>4</td>
<td>
<input
type="text"
{...register('group_work_contact_with_client_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_contact_with_client_2', {})}
/>
</td>
<td>6,3%</td>
<td>5,2%</td>
</tr>
<tr>
<td>Zarządzanie ryzykiem i zakresem projektu</td>
<td>2</td>
<td>3</td>
<td>
<input
type="text"
{...register('group_work_management_of_risk_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_management_of_risk_2', {})}
/>
</td>
<td>3,2%</td>
<td>3,9%</td>
</tr>
<tr>
<td>Metodyka pracy i narzędzia wspierające *</td>
<td>3</td>
<td>3</td>
<td>
<input
type="text"
{...register('group_work_work_methodology_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_work_methodology_2', {})}
/>
</td>
<td>4,7%</td>
<td>3,9%</td>
</tr>
<tr>
<td>Zarządzanie kodem źródłowym *</td>
<td>2</td>
<td>2</td>
<td>
<input
type="text"
{...register('group_work_management_of_source_code_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('group_work_management_of_source_code_2', {})}
/>
</td>
<td>3,2%</td>
<td>2,6%</td>
</tr>
<tr>
<td>DevOps</td>
<td>0</td>
<td>3</td>
<td>
<input type="text" {...register('group_work_devops_1', {})} />
</td>
<td>
<input type="text" {...register('group_work_devops_2', {})} />
</td>
<td>0,0%</td>
<td>3,9%</td>
</tr>
{/* */}
<tr>
<td>Produkty projektu</td>
<td>3,5</td>
<td>4,5</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>
Czy złożoność produktu projektu odpowiada wielkości zespołu? Czy
jakość produktu odpowiada wielkości projektu *
</td>
<td>3</td>
<td>5</td>
<td>
<input
type="text"
{...register('products_project_complexity_of_product_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_complexity_of_product_2', {})}
/>
</td>
<td>4,8%</td>
<td>6,6%</td>
</tr>
<tr>
<td>
Dostęp do produktu projektu dla komisji do testów podczas
prezentacji * (obowiązkowe w II sem) (oceniane podczas obrony
semestralnej)
</td>
<td>1</td>
<td>1</td>
<td>
<input
type="text"
{...register('products_project_access_to_application_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_access_to_application_2', {})}
/>
</td>
<td>1,6%</td>
<td>1,3%</td>
</tr>
<tr>
<td>
Brak krytycznych błędów w tym: bezpieczeństwa oraz
uniemożliwiających korzystanie z systemu * (oceniane podczas
obrony semestralnej)
</td>
<td>0</td>
<td>2</td>
<td>
<input
type="text"
{...register('products_project_security_issues_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_security_issues_2', {})}
/>
</td>
<td>0,0%</td>
<td>2,6%</td>
</tr>
<tr>
<td>
Dostęp do produktu projektu dla komisji do testów tydzień przed
prezentacją (oceniane podczas obrony semestralnej)
</td>
<td>0</td>
<td>5</td>
<td>
<input
type="text"
{...register(
'products_project_access_to_test_application_1',
{},
)}
/>
</td>
<td>
<input
type="text"
{...register(
'products_project_access_to_test_application_2',
{},
)}
/>
</td>
<td>0,0%</td>
<td>6,6%</td>
</tr>
<tr>
<td>Spełnienie kryteriów akceptacji *</td>
<td>5</td>
<td>5</td>
<td>
<input
type="text"
{...register('products_project_acceptance_criteria_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_acceptance_criteria_2', {})}
/>
</td>
<td>8,0%</td>
<td>6,6%</td>
</tr>
<tr>
<td>
Czy wdrożone zostały wszystkie zakładane na dany semestr
funkcjonalności (ocenia prowadzący)?
</td>
<td>2</td>
<td>2</td>
<td>
<input
type="text"
{...register('products_project_expected_functionality_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_expected_functionality_2', {})}
/>
</td>
<td>3,2%</td>
<td>2,6%</td>
</tr>
<tr>
<td>Czy projekt dobrze rokuje?</td>
<td>5</td>
<td>0</td>
<td>
<input
type="text"
{...register('products_project_promises_well_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_promises_well_2', {})}
/>
</td>
<td>8,0%</td>
<td>0,0%</td>
</tr>
<tr>
<td>Czy projekt został wdrożony?</td>
<td>0</td>
<td>3</td>
<td>
<input
type="text"
{...register('products_project_has_been_implemented_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_has_been_implemented_2', {})}
/>
</td>
<td>0,0%</td>
<td>4,0%</td>
</tr>
<tr>
<td>
Czy projekt jest użyteczny dla grupy docelowej; Czy spełnia
kryteria użyteczności i został przetestowany pod tym kątem?
</td>
<td>3</td>
<td>5</td>
<td>
<input
type="text"
{...register('products_project_is_useful_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_is_useful_2', {})}
/>
</td>
<td>4,8%</td>
<td>6,6%</td>
</tr>
<tr>
<td>Czy przygotowano prototyp projektu zgodnie ze sztuką? *</td>
<td>2</td>
<td>0</td>
<td>
<input
type="text"
{...register('products_project_prototype_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_prototype_2', {})}
/>
</td>
<td>3,2%</td>
<td>0,0%</td>
</tr>
<tr>
<td>
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
</td>
<td>0</td>
<td>4</td>
<td>
<input
type="text"
{...register('products_project_tests_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_tests_2', {})}
/>
</td>
<td>0,0%</td>
<td>5,3%</td>
</tr>
<tr>
<td>
Dobór techologii i architektury systemu do rozwiązywanego
problemu
</td>
<td>1</td>
<td>2</td>
<td>
<input
type="text"
{...register('products_project_technology_1', {})}
/>
</td>
<td>
<input
type="text"
{...register('products_project_technology_2', {})}
/>
</td>
<td>1,6%</td>
<td>2,6%</td>
</tr>
</tbody>
</table>
<div className="flex justify-end">
<button className="btn btn-success mt-2">ZAPISZ</button>
</div>
</form>
</div>
)
}
export default GradeCard

View File

@ -0,0 +1,24 @@
import { useQuery } from 'react-query'
import { Link, useParams } from 'react-router-dom'
import { getGroup } from '../../api/groups'
const Group = () => {
const { id } = useParams<{ id: string }>()
const { data: groups } = useQuery(['getGroup'], () => getGroup(Number(id)))
const { name, project_supervisor } = groups?.data || {}
return (
<div>
<h1 className="font-bold text-xl">{name}</h1>
<h2 className="">
Opiekun: {project_supervisor?.first_name}{' '}
{project_supervisor?.last_name}
</h2>
<Link to={`/coordinator/groups/${id}/grade-card`}>
<button className="btn btn-success mt-2">KARTA OCENY</button>
</Link>
</div>
)
}
export default Group

View File

@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'
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'
const Groups = () => {
let navigate = useNavigate()
@ -94,7 +95,14 @@ const Groups = () => {
points_for_second_term,
}) => (
<tr key={id}>
<td>{name}</td>
<td>
<Link
to={`/coordinator/groups/${id}`}
className="underline font-bold"
>
{name}
</Link>
</td>
<td>
{`${project_supervisor.first_name} ${project_supervisor.last_name}`}
</td>

View File

@ -53,7 +53,7 @@ const Students = () => {
const { mutate: mutateDownload } = useMutation(
'downloadStudents',
(mode: boolean) => downloadStudents(mode),
(mode: boolean) => downloadStudents(mode, Number(yearGroupId)),
{
onSuccess: (res) => {
const url = window.URL.createObjectURL(new Blob([res.data]))
@ -68,7 +68,7 @@ const Students = () => {
const { mutate: mutateUpload } = useMutation(
'uploadStudents',
(payload: FormData) => uploadStudents(payload),
(payload: FormData) => uploadStudents(payload, Number(yearGroupId)),
{
onSuccess: () => refetchStudents(),
},