mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[Completed courses search] Delete old components
This commit is contained in:
@@ -1,233 +0,0 @@
|
||||
import qs from 'query-string'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router'
|
||||
import { Button, Form, Loader, Modal, TextArea } from 'semantic-ui-react'
|
||||
|
||||
import { SearchHistory } from '@/components/SearchHistory'
|
||||
import {
|
||||
useCreateCourseListMutation,
|
||||
useDeleteCourseListMutation,
|
||||
useGetSavedCourseListsQuery,
|
||||
useUpdateCourseListMutation,
|
||||
} from '@/redux/completedCoursesSearch'
|
||||
|
||||
export const CompletedCoursesSearch = ({ setValues }) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [modal, setModal] = useState(false)
|
||||
const [courseInput, setCourseInput] = useState('')
|
||||
const [studentInput, setStudentInput] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const courseLists = useGetSavedCourseListsQuery()
|
||||
const [searchList, setSearches] = useState(null)
|
||||
const [selectedSearchId, setSelectedSearchId] = useState('')
|
||||
const isFetchingOrLoading = courseLists.isLoading || courseLists.isFetching
|
||||
const isError = courseLists.isError || (courseLists.isSuccess && !courseLists.data)
|
||||
const [updateCourseList, { isLoading: updateIsLoading, data: updatedData }] = useUpdateCourseListMutation()
|
||||
const [createCourseList, { isLoading: createIsLoading, data: createdData }] = useCreateCourseListMutation()
|
||||
const [deleteCourseList, { isLoading: deleteIsLoading, data: deletedData }] = useDeleteCourseListMutation()
|
||||
|
||||
const parseQueryFromUrl = () => {
|
||||
const query = qs.parse(location.search)
|
||||
if (!Array.isArray(query.courseList)) query.courseList = [query.courseList]
|
||||
if (!Array.isArray(query.studentList)) query.studentList = [query.studentList]
|
||||
return query
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (courseLists.isSuccess && courseLists.data) {
|
||||
setSearches(courseLists.data)
|
||||
}
|
||||
}, [courseLists])
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedData) {
|
||||
const updatedSearches = searchList.map(s => (s.id === updatedData.id ? updatedData : s))
|
||||
setSearches(updatedSearches)
|
||||
}
|
||||
}, [updatedData])
|
||||
|
||||
useEffect(() => {
|
||||
if (createdData && !createdData.error) {
|
||||
const newList = searchList.concat(createdData)
|
||||
setSearches(newList)
|
||||
}
|
||||
}, [createdData])
|
||||
|
||||
useEffect(() => {
|
||||
if (deletedData) {
|
||||
const filteredSearches = searchList.filter(s => s.id !== deletedData)
|
||||
setSearches(filteredSearches)
|
||||
}
|
||||
}, [deletedData])
|
||||
|
||||
useEffect(() => {
|
||||
if (location.search) {
|
||||
const query = parseQueryFromUrl()
|
||||
setValues(query)
|
||||
}
|
||||
}, [location.search])
|
||||
|
||||
const clearForm = () => {
|
||||
setCourseInput('')
|
||||
setStudentInput('')
|
||||
setSelectedSearchId('')
|
||||
}
|
||||
|
||||
const pushQueryToUrl = query => {
|
||||
const searchString = qs.stringify(query)
|
||||
navigate({ search: searchString })
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setModal(false)
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
deleteCourseList({ id: selectedSearchId })
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
const courseList = courseInput
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim())
|
||||
.filter(code => code !== '')
|
||||
if (selectedSearchId !== '') {
|
||||
updateCourseList({ id: selectedSearchId, courseList })
|
||||
} else {
|
||||
createCourseList({ courseList, name })
|
||||
}
|
||||
}
|
||||
const onSelectSearch = selectedId => {
|
||||
if (!selectedId) {
|
||||
clearForm()
|
||||
return
|
||||
}
|
||||
const { id: selectedSearchId } = selectedId
|
||||
const selectedSearch = searchList?.find(search => search.id === selectedSearchId)
|
||||
if (selectedSearch) {
|
||||
setCourseInput(selectedSearch.courseList.join(', '))
|
||||
setName(selectedSearch.name)
|
||||
setSelectedSearchId(selectedSearch.id)
|
||||
}
|
||||
}
|
||||
|
||||
const onClicker = event => {
|
||||
event.preventDefault()
|
||||
|
||||
const courseList = courseInput
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim())
|
||||
.filter(code => code !== '')
|
||||
const studentList = studentInput
|
||||
.split(/[;\s,]+/)
|
||||
.map(code => code.trim())
|
||||
.filter(s => s !== '')
|
||||
.map(s => (s.length === 8 ? `0${s}` : s))
|
||||
|
||||
if (courseList.length === 0 || studentList.length === 0 || studentList[0] === null) {
|
||||
return
|
||||
}
|
||||
|
||||
setValues({
|
||||
studentList,
|
||||
courseList,
|
||||
})
|
||||
|
||||
pushQueryToUrl({ courseList, studentList })
|
||||
|
||||
handleClose()
|
||||
}
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
open={modal}
|
||||
size="small"
|
||||
trigger={
|
||||
<Button color="blue" data-cy="open-completed-courses-modal-button" onClick={() => setModal(true)} size="small">
|
||||
Search completed courses of students
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Form>
|
||||
<h2>Search completed courses of students</h2>
|
||||
<Form.Field>
|
||||
<em>Insert one or more student numbers, separated by a space, a newline, a comma, or a semicolon.</em>
|
||||
<TextArea
|
||||
data-cy="student-no-input"
|
||||
onChange={event => setStudentInput(event.target.value)}
|
||||
placeholder="012345678, 12345678"
|
||||
value={studentInput}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<em>Insert one or more courses, separated by a space, a newline, or a comma.</em>
|
||||
<TextArea
|
||||
data-cy="course-list-input"
|
||||
onChange={event => setCourseInput(event.target.value)}
|
||||
placeholder="TKT12345, PSYK-123"
|
||||
value={courseInput}
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Field data-cy="search-name">
|
||||
<em> Insert name for this course list if you wish to save it </em>
|
||||
<Form.Input
|
||||
disabled={selectedSearchId !== ''}
|
||||
onChange={event => setName(event.target.value)}
|
||||
placeholder="name"
|
||||
value={name}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Field>
|
||||
<SearchHistory
|
||||
handleSearch={onSelectSearch}
|
||||
header="Saved courselists"
|
||||
items={
|
||||
searchList
|
||||
? searchList.map(list => ({
|
||||
...list,
|
||||
text: list.name,
|
||||
timestamp: new Date(list.updatedAt),
|
||||
params: { id: list.id },
|
||||
}))
|
||||
: []
|
||||
}
|
||||
updateItem={() => null}
|
||||
/>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
content="Save"
|
||||
data-cy="save-courselist"
|
||||
disabled={!name || updateIsLoading || createIsLoading}
|
||||
floated="left"
|
||||
icon="save"
|
||||
loading={updateIsLoading || createIsLoading}
|
||||
onClick={onSave}
|
||||
/>
|
||||
<Button
|
||||
content="Delete"
|
||||
disabled={!selectedSearchId || deleteIsLoading}
|
||||
floated="left"
|
||||
icon="trash"
|
||||
negative
|
||||
onClick={onDelete}
|
||||
/>
|
||||
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button data-cy="completed-courses-search-button" onClick={event => onClicker(event)} positive>
|
||||
Search
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
import moment from 'moment'
|
||||
import { Icon, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { hiddenNameAndEmailForExcel } from '@/common/columns'
|
||||
import { StudentInfoItem } from '@/components/common/StudentInfoItem'
|
||||
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import { RightsNotification } from '@/components/RightsNotification'
|
||||
import { SortableTable } from '@/components/SortableTable'
|
||||
import { StudentNameVisibilityToggle, useStudentNameVisibility } from '@/components/StudentNameVisibilityToggle'
|
||||
import { ISO_DATE_FORMAT } from '@/constants/date'
|
||||
import { useGetCompletedCoursesQuery } from '@/redux/completedCoursesSearch'
|
||||
import { isWithinSixMonths } from '@/util/timeAndDate'
|
||||
|
||||
const getColumns = (courses, showStudentNames, getTextIn) => {
|
||||
const isPassed = credit => [4, 7, 9].includes(credit)
|
||||
// 4=completed, 7=improved, 9=transferred, 10=failed
|
||||
|
||||
const getCompletion = (student, courseCode, { icon }) => {
|
||||
const completion = student.credits.find(credit => credit.courseCode === courseCode && isPassed(credit.creditType))
|
||||
const enrollment = student.enrollments[courseCode]
|
||||
if (completion === undefined) {
|
||||
if (!enrollment) {
|
||||
return icon ? null : ''
|
||||
}
|
||||
if (icon) {
|
||||
if (isWithinSixMonths(enrollment.date)) {
|
||||
return <Icon color="yellow" fitted name="minus" />
|
||||
}
|
||||
return <Icon color="grey" fitted name="minus" />
|
||||
}
|
||||
|
||||
return `Latest enrollment: ${moment(enrollment.date).format(ISO_DATE_FORMAT)}`
|
||||
}
|
||||
|
||||
const substitutionString = completion.substitution ? ` as ${completion.substitution}` : ''
|
||||
|
||||
return icon ? <Icon color="green" fitted name="check" /> : `Passed${substitutionString}`
|
||||
}
|
||||
|
||||
const getTotalPassed = student => student.credits.filter(credit => isPassed(credit.creditType)).length
|
||||
|
||||
const getTotalUnfinished = student => Object.values(student.enrollments).length
|
||||
|
||||
const style = {
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center',
|
||||
}
|
||||
|
||||
const studentNbrColumn = [
|
||||
{
|
||||
key: 'studentnumber',
|
||||
title: 'Student number',
|
||||
cellProps: { style },
|
||||
getRowVal: student => student.studentNumber,
|
||||
getRowContent: student => <StudentInfoItem showSisuLink student={student} />,
|
||||
},
|
||||
]
|
||||
|
||||
const getCellProps = (student, courseCode) => {
|
||||
const credit = student.credits.find(credit => credit.courseCode === courseCode)
|
||||
const enrollment = student.enrollments[courseCode]
|
||||
if (!credit && !enrollment) {
|
||||
return { style }
|
||||
}
|
||||
const title = credit
|
||||
? `Passed on ${moment(credit.date).format(ISO_DATE_FORMAT)}\nCourse code: ${
|
||||
credit.substitution ? credit.substitution : credit.courseCode
|
||||
}`
|
||||
: `Last enrollment on ${moment(enrollment.date).format(ISO_DATE_FORMAT)}\nCourse code ${
|
||||
enrollment.substitution ? enrollment.substitution : enrollment.courseCode
|
||||
}`
|
||||
return { style, title }
|
||||
}
|
||||
|
||||
const completionStatusColumns = courses.map(course => {
|
||||
return {
|
||||
key: course.code,
|
||||
title: (
|
||||
<div key={course.code} style={{ marginLeft: '-6px', width: '8em', whiteSpace: 'normal' }}>
|
||||
<div key={`key1${course.code}`}>{course.code}</div>
|
||||
<div key={`key2${course.code}`} style={{ color: 'gray', fontWeight: 'normal' }}>
|
||||
{getTextIn(course.name)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: course.code,
|
||||
cellProps: student => getCellProps(student, course.code),
|
||||
headerProps: { title: course.code },
|
||||
getRowVal: student => getCompletion(student, course.code, { icon: false }),
|
||||
getRowContent: student => getCompletion(student, course.code, { icon: true }),
|
||||
}
|
||||
})
|
||||
|
||||
const statisticColumns = [
|
||||
{
|
||||
key: 'passed',
|
||||
title: 'Passed',
|
||||
cellProps: { style },
|
||||
headerProps: { title: 'Passed' },
|
||||
getRowVal: student => getTotalPassed(student),
|
||||
},
|
||||
{
|
||||
key: 'unfinished',
|
||||
title: 'Unfinished',
|
||||
headerProps: { title: 'Unfinished' },
|
||||
getRowVal: student => getTotalUnfinished(student),
|
||||
cellProps: { style },
|
||||
},
|
||||
]
|
||||
|
||||
const statisticHeader = {
|
||||
key: 'statistics',
|
||||
title: <b>Totals:</b>,
|
||||
export: false,
|
||||
textTitle: null,
|
||||
children: statisticColumns,
|
||||
}
|
||||
|
||||
const emailColumn = showStudentNames
|
||||
? [
|
||||
{
|
||||
key: 'email',
|
||||
title: 'Email',
|
||||
cellProps: { style },
|
||||
headerProps: { title: 'Email' },
|
||||
getRowVal: student => (student.email ? student.email : ''),
|
||||
export: false,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const nameColumns = showStudentNames
|
||||
? [
|
||||
{
|
||||
key: 'lastname',
|
||||
title: 'Last name',
|
||||
cellProps: { style },
|
||||
getRowVal: student => student.lastname,
|
||||
export: false,
|
||||
},
|
||||
{
|
||||
key: 'firstnames',
|
||||
title: 'First names',
|
||||
cellProps: { style },
|
||||
getRowVal: student => student.firstnames,
|
||||
export: false,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
return [...nameColumns, ...studentNbrColumn, ...completionStatusColumns, statisticHeader, ...emailColumn]
|
||||
}
|
||||
|
||||
export const CompletedCoursesSearchResults = ({ searchValues }) => {
|
||||
const { courseList, studentList } = searchValues
|
||||
const showStudentNames = useStudentNameVisibility()
|
||||
const { data, isLoading, isFetching, isError } = useGetCompletedCoursesQuery({ courseList, studentList })
|
||||
const { getTextIn } = useLanguage()
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetching || isLoading || !data) return <Loader active style={{ marginTop: '15em' }} />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StudentNameVisibilityToggle />
|
||||
{data.discardedStudentNumbers?.length > 0 && (
|
||||
<RightsNotification discardedStudentNumbers={data.discardedStudentNumbers} />
|
||||
)}
|
||||
<div
|
||||
data-cy="completed-courses-table-div"
|
||||
style={{ maxWidth: '100vh', overflowX: 'auto', paddingBottom: '50px', padding: '0.5em' }}
|
||||
>
|
||||
<SortableTable
|
||||
columns={getColumns(data.courses, showStudentNames.visible, getTextIn)}
|
||||
data={data.students}
|
||||
featureName="completed_courses"
|
||||
onlyExportColumns={hiddenNameAndEmailForExcel}
|
||||
title="Completed courses search"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import { Icon, Message } from 'semantic-ui-react'
|
||||
|
||||
import { useTitle } from '@/common/hooks'
|
||||
import { CompletedCoursesSearch } from './CompletedCoursesSearch'
|
||||
import { CompletedCoursesSearchResults } from './CompletedCoursesSearchResults'
|
||||
|
||||
export const CompletedCourses = () => {
|
||||
useTitle('Search completed courses')
|
||||
const [searchValues, setValues] = useState({ courseList: [], studentList: [] })
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
|
||||
<Message.Header>Check completed courses</Message.Header>
|
||||
<p>
|
||||
Here you can search by a list of student and course numbers to see whether students have completed certain
|
||||
courses yet. The tool will also show if students have enrolled on the course, if they have not yet completed
|
||||
it. Course substitutions are taken into account.
|
||||
</p>
|
||||
<p>
|
||||
<Icon color="green" fitted name="check" />: Student has completed the course with a passing grade. <br />
|
||||
<Icon color="yellow" fitted name="minus" />: Student has not completed the course, but has an active
|
||||
enrollment from less than 6 months ago. <br />
|
||||
<Icon color="grey" fitted name="minus" />: Student has not completed the course, but has an enrollment from
|
||||
more than 6 months ago. <br />
|
||||
<b>Empty cell</b>: Student has no completion or enrollment for the course.
|
||||
</p>
|
||||
</Message>
|
||||
<CompletedCoursesSearch setValues={setValues} />
|
||||
<div style={{ paddingTop: '25px' }}>
|
||||
{searchValues && searchValues.courseList?.length > 0 && searchValues.studentList?.length > 0 && (
|
||||
<CompletedCoursesSearchResults searchValues={searchValues} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user