[CourseStat][Search] Use substitution_groups when searching for courses

This commit is contained in:
Aaro Koivukangas
2026-03-20 15:53:11 +02:00
parent cbe20f1ad3
commit 235d2b03b2
4 changed files with 55 additions and 44 deletions
@@ -1,4 +1,5 @@
import IconButton from '@mui/material/IconButton'
import Stack from '@mui/material/Stack'
import Table from '@mui/material/Table'
import TableBody from '@mui/material/TableBody'
import TableCell from '@mui/material/TableCell'
@@ -9,6 +10,7 @@ import Typography from '@mui/material/Typography'
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
import { DeleteIcon } from '@/theme'
import { SearchResultCourse } from '@/types/api/courses'
import { getActiveYears } from '../util'
export const CourseTable = ({
@@ -29,37 +31,55 @@ export const CourseTable = ({
const getEmptyListRow = () => (
<TableRow>
<TableCell align="center" colSpan={2}>
<TableCell align="center" colSpan={3}>
No results.
</TableCell>
</TableRow>
)
const toCourseRow = course => (
<TableRow
data-cy={`course-${course.code}`}
hover
key={course.id}
onClick={() => (course.min_attainment_date ? onSelectCourse(course) : null)}
style={{ cursor: course.min_attainment_date ? 'pointer' : 'default' }}
>
<TableCell>
<Typography variant="subtitle1">{getTextIn(course.name)}</Typography>
<Typography color="text.secondary" variant="body2">
{getActiveYears(course)}
const formatSubstitutionGroups = (course: SearchResultCourse): string[] =>
course.substitution_groups?.map(sb => (sb.length !== 1 ? `${sb.join(', ')}` : sb[0])) ?? ['???']
const SubstitutionGroups = (course: SearchResultCourse) => (
<Stack>
{formatSubstitutionGroups(course).map(sg => (
<Typography fontSize="0.9rem" key={sg}>
{sg}
</Typography>
</TableCell>
<TableCell>{combineSubstitutions ? [course.code, ...course.substitutions].join(', ') : course.code}</TableCell>
{title === 'Selected courses' && (
<TableCell align="right">
<IconButton>
<DeleteIcon />
</IconButton>
</TableCell>
)}
</TableRow>
))}
</Stack>
)
const toCourseRow = (course: SearchResultCourse) => {
return (
<TableRow
data-cy={`course-${course.code}`}
hover
key={course.id}
onClick={() => (course.min_attainment_date ? onSelectCourse(course) : null)}
style={{ cursor: course.min_attainment_date ? 'pointer' : 'default' }}
>
<TableCell>
<Typography variant="subtitle1">{getTextIn(course.name)}</Typography>
<Typography color="text.secondary" variant="body2">
{getActiveYears(course)}
</Typography>
</TableCell>
<TableCell>
<Typography fontSize="0.95rem">{course.code}</Typography>
</TableCell>
<TableCell>{combineSubstitutions ? SubstitutionGroups(course) : course.code}</TableCell>
{title === 'Selected courses' && (
<TableCell align="right">
<IconButton>
<DeleteIcon />
</IconButton>
</TableCell>
)}
</TableRow>
)
}
if (hidden) {
return null
}
@@ -70,7 +90,8 @@ export const CourseTable = ({
<TableHead>
<TableRow>
<TableCell>{title}</TableCell>
<TableCell>Code(s)</TableCell>
<TableCell>Main course code</TableCell>
<TableCell>Equivalent groups</TableCell>
{title === 'Selected courses' && <TableCell />}
</TableRow>
</TableHead>
@@ -21,6 +21,7 @@ import { useSearchHistory } from '@/hooks/searchHistory'
import { useToggle } from '@/hooks/toggle'
import { useGetCourseSearchResultQuery } from '@/redux/courseSearch'
import { AddIcon, SearchIcon } from '@/theme'
import { SearchResultCourse } from '@/types/api/courses'
import { SearchHistoryItem } from '@/types/searchHistory'
import { queryParamsToString } from '@/util/queryparams'
import { CourseTable } from './CourseTable'
@@ -37,7 +38,7 @@ export const SearchForm = () => {
const [courseCode, setCourseCode] = useState('')
const [debouncedCourseName, setDebouncedCourseName] = useDebouncedState('')
const [debouncedCourseCode, setDebouncedCourseCode] = useDebouncedState('')
const [selectedCourses, setSelectedCourses] = useState({})
const [selectedCourses, setSelectedCourses] = useState<Record<string, SearchResultCourse>>({})
const isInputValid = validateInputLength(courseName, 5) || validateInputLength(courseCode, 2)
const isDebouncedInputValid =
@@ -65,7 +66,7 @@ export const SearchForm = () => {
setDebouncedCourseCode(event.target.value)
}
const onSelectCourse = course => {
const onSelectCourse = (course: SearchResultCourse) => {
const isSelected = !!selectedCourses[course.code]
if (isSelected) {
@@ -148,7 +149,7 @@ export const SearchForm = () => {
const noSelectedCourses = !selected.length
const addAllCourses = () => {
const newSelectedCourses = courses.reduce((newSelected, course) => {
const newSelectedCourses = courses.reduce<Record<string, SearchResultCourse>>((newSelected, course) => {
newSelected[course.code] = { ...course }
return newSelected
}, {})
+5 -16
View File
@@ -1,21 +1,10 @@
import { Name } from '@oodikone/shared/types'
import { Unarray } from '@oodikone/shared/types'
import { CourseWithSubsId } from '@oodikone/shared/types/course'
export type SearchResultCourse = Unarray<GetCourseSearchResultResponse['courses']>
export type GetCourseSearchResultResponse = {
courses: Array<{
code: string
course_unit_type: string
coursetypecode: string
createdAt: string
id: string
is_study_module: boolean
mainCourseCode: string | null
max_attainment_date: string
min_attainment_date: string
name: Name
subsId: number
substitutions: string[]
updatedAt: string
}>
courses: Array<Omit<CourseWithSubsId, 'courseType' | 'credits' | 'enrollments' | 'organizations'>>
}
export type GetCourseSearchResultRequest = {
@@ -128,7 +128,7 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
course.substitution_groups = course.substitution_groups
.map(subGroup => subGroup.map(id => idToCodeMap.get(id) ?? null))
.map(subGroup => subGroup.filter(substitution_groups => !!substitution_groups))
.map(subGroup => subGroup.filter(substitution_groups => !!substitution_groups)) // Filter out undefined/null entries
course.mainCourseCode = [course.code, ...course.substitution_groups]
.sort((a, b) => getSubstitutionPriority(b) - getSubstitutionPriority(a))