[Study programme] Rewrite OverallStatsTable with MRT

This commit is contained in:
Riku Rauhala
2025-03-19 14:33:05 +02:00
parent e64e0d04fd
commit f80200d481
6 changed files with 335 additions and 265 deletions
@@ -132,9 +132,6 @@ export const studyProgrammeToolTips: Record<string, string> = {
Jos sivun yläosassa on valittuna ”All study rights” (oletus), mukana ovat myös ohjelmaan siirtyneet ja ohjelmasta pois siirtyneet opiskelijat.
`,
name: `
Pitkät kurssinimet on lyhennetty. Näet ne kokonaan laittamalla hiiren nimen yläpuolelle.
`,
notCompleted: `
Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
`,
@@ -1,262 +0,0 @@
import { range } from 'lodash'
import { Link } from 'react-router'
import { Item, Icon } from 'semantic-ui-react'
import { studyProgrammeToolTips } from '@/common/InfoToolTips'
import { GetTextIn, useLanguage } from '@/components/LanguagePicker/useLanguage'
import { SortableTable } from '@/components/SortableTable'
import { StudyProgrammeCourse } from '@/shared/types'
const getColumns = (getTextIn: GetTextIn, showStudents: boolean) => {
let columns = null
if (showStudents) {
columns = [
{
key: 'code',
title: 'Code',
getRowVal: course => course.code,
formatValue: code => (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5em' }}>
{code}
<Item
as={Link}
target="_blank"
to={`/coursestatistics?courseCodes=["${encodeURIComponent(code)}"]&separate=false&unifyOpenUniCourses=false`}
>
<Icon name="level up alternate" />
</Item>
</div>
),
},
{
key: 'name',
title: 'Name',
helpText: studyProgrammeToolTips.name,
getRowVal: course => getTextIn(course.name),
formatValue: name => (
<div style={{ maxWidth: '400px', overflow: 'hidden', textOverflow: 'ellipsis' }}>{name}</div>
),
cellProps: course => {
return { title: getTextIn(course.name) }
},
},
{
key: 'total',
title: 'Total',
filterType: 'range',
getRowVal: course => course.totalAllStudents,
},
{
key: 'breakdown',
title: 'Breakdown of total',
parent: true,
sortable: false,
filterable: false,
children: [
{
key: 'passed',
title: 'Passed',
filterType: 'range',
getRowVal: course => course.totalAllPassed,
},
{
key: 'not-completed',
title: 'Not\ncompleted',
filterType: 'range',
getRowVal: course => course.totalAllNotCompleted,
},
],
},
{
key: 'breakdown-passed',
title: 'Breakdown of passed students',
parent: true,
sortable: false,
filterable: false,
children: [
{
key: 'totalOwnProgramme',
title: 'Major\nstudents',
filterType: 'range',
getRowVal: course => course.totalProgrammeStudents,
},
{
key: 'totalOtherProgramme',
title: 'Non-major\nstudents',
filterType: 'range',
getRowVal: course => course.totalOtherProgrammeStudents,
},
{
key: 'totalWithoutStudyRight',
title: 'Non-degree\nstudents',
filterType: 'range',
getRowVal: course => course.totalWithoutStudyRightStudents,
},
],
},
{
key: 'exluded',
title: 'Not included in\ntotal nor passed',
parent: true,
sortable: false,
filterable: false,
children: [
{
key: 'transfer',
title: 'Transferred\nstudents',
helpText: studyProgrammeToolTips.transferredCourses,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalTransferStudents,
},
],
},
]
} else {
columns = [
{
key: 'code',
title: 'Code',
getRowVal: course => course.code,
formatValue: code => (
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5em' }}>
{code}
<Item
as={Link}
target="_blank"
to={`/coursestatistics?courseCodes=["${encodeURIComponent(code)}"]&separate=false&unifyOpenUniCourses=false`}
>
<Icon name="level up alternate" />
</Item>
</div>
),
},
{
key: 'name',
title: 'Name',
helpText: studyProgrammeToolTips.name,
getRowVal: course => getTextIn(course.name),
formatValue: name => (
<div style={{ maxWidth: '400px', overflow: 'hidden', textOverflow: 'ellipsis' }}>{name}</div>
),
cellProps: course => ({
title: getTextIn(course.name),
}),
},
{
key: 'total',
title: 'Total\ncredits',
filterType: 'range',
getRowVal: course => course.totalAllCredits,
},
{
key: 'totalOwnProgramme',
title: 'Major\ncredits',
filterType: 'range',
getRowVal: course => course.totalProgrammeCredits,
},
{
key: 'totalOtherProgramme',
title: 'Non-major\ncredits',
filterType: 'range',
getRowVal: course => course.totalOtherProgrammeCredits,
},
{
key: 'totalWithoutStudyRight',
title: 'Non-degree\ncredits',
filterType: 'range',
getRowVal: course => course.totalWithoutStudyRightCredits,
},
{
key: 'totalTransfer',
title: 'Transferred\ncredits',
filterType: 'range',
getRowVal: course => course.totalTransferCredits,
},
]
}
const typeColumn = {
key: 'type',
title: 'Type',
getRowVal: course => (course.isStudyModule ? 'Module' : 'Course'),
}
return [...columns, typeColumn]
}
const filterDataByYear = (data: StudyProgrammeCourse[], fromYear: number, toYear: number) => {
const yearRange = range(fromYear, Number(toYear) + 1)
const filteredAndMergedCourses = data
.filter(course => {
const arr = Object.keys(course.years).some(key => yearRange.includes(Number(key)))
return arr
})
.map(course => {
const values = Object.entries(course.years).reduce(
(acc, curr) => {
if (yearRange.includes(Number(curr[0]))) {
acc.totalAllStudents += curr[1].totalAllStudents
acc.totalAllPassed += curr[1].totalPassed
acc.totalAllNotCompleted += curr[1].totalNotCompleted
acc.totalAllCredits += curr[1].totalAllCredits
acc.totalProgrammeStudents += curr[1].totalProgrammeStudents
acc.totalProgrammeCredits += curr[1].totalProgrammeCredits
acc.totalOtherProgrammeStudents += curr[1].totalOtherProgrammeStudents
acc.totalOtherProgrammeCredits += curr[1].totalOtherProgrammeCredits
acc.totalWithoutStudyRightStudents += curr[1].totalWithoutStudyRightStudents
acc.totalWithoutStudyRightCredits += curr[1].totalWithoutStudyRightCredits
acc.totalTransferCredits += curr[1].totalTransferCredits
acc.totalTransferStudents += curr[1].totalTransferStudents
}
return acc
},
{
totalAllStudents: 0,
totalAllPassed: 0,
totalAllNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
totalOtherProgrammeStudents: 0,
totalOtherProgrammeCredits: 0,
totalWithoutStudyRightStudents: 0,
totalWithoutStudyRightCredits: 0,
totalTransferCredits: 0,
totalTransferStudents: 0,
}
)
return {
...values,
code: course.code,
name: course.name,
isStudyModule: course.isStudyModule,
}
})
return filteredAndMergedCourses
}
export const OverallStatsTable = ({
data,
fromYear,
showStudents,
toYear,
}: {
data: StudyProgrammeCourse[]
fromYear: number
showStudents: boolean
toYear: number
}) => {
const { getTextIn } = useLanguage()
return (
<SortableTable
columns={getColumns(getTextIn, showStudents)}
data={filterDataByYear(data, fromYear, toYear)}
defaultSort={['name', 'asc']}
featureName="programme_courses"
title="Student statistics for study programme courses"
/>
)
}
@@ -0,0 +1,19 @@
import { NorthEast as NorthEastIcon } from '@mui/icons-material'
import { IconButton, Stack, Typography } from '@mui/material'
import { Link } from 'react-router'
export const CodeCell = ({ code }: { code: string }) => {
return (
<Stack alignItems="center" direction="row" gap={1} justifyContent="space-between">
<Typography variant="body2">{code}</Typography>
<Link
title={`Open course statistics for ${code}`}
to={`/coursestatistics?courseCodes=["${encodeURIComponent(code)}"]&separate=false&unifyOpenUniCourses=false`}
>
<IconButton color="primary" data-cy={`${code.toLowerCase()}-course-statistics-link-button`}>
<NorthEastIcon fontSize="small" />
</IconButton>
</Link>
</Stack>
)
}
@@ -0,0 +1,13 @@
import { Box, Stack } from '@mui/material'
export const HeaderCell = ({ value }: { value: string }) => {
return (
<Box sx={{ textAlign: 'left' }}>
<Stack>
{value.split(' ').map(subString => (
<span key={subString}>{subString}</span>
))}
</Stack>
</Box>
)
}
@@ -0,0 +1,247 @@
import { MaterialReactTable, MRT_ColumnDef, useMaterialReactTable } from 'material-react-table'
import { useMemo } from 'react'
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
import { Name, StudyProgrammeCourse } from '@/shared/types'
import { CodeCell } from './CodeCell'
import { HeaderCell } from './HeaderCell'
import { filterDataByYear } from './util'
export const OverallStatsTable = ({
data,
fromYear,
showStudents,
toYear,
}: {
data: StudyProgrammeCourse[]
fromYear: number
showStudents: boolean
toYear: number
}) => {
const { getTextIn } = useLanguage()
const filteredData = useMemo(() => filterDataByYear(data, fromYear, toYear), [data, fromYear, toYear])
const columns = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorKey: 'isStudyModule',
header: 'Type',
Cell: ({ cell }) => (cell.getValue<boolean>() ? 'Module' : 'Course'),
},
{
accessorKey: 'code',
header: 'Code',
Cell: ({ cell }) => <CodeCell code={cell.getValue<string>()} />,
},
{
accessorKey: 'name',
header: 'Name',
Cell: ({ cell }) => getTextIn(cell.getValue<Name>()),
},
{
accessorKey: 'totalAllStudents',
header: 'Total',
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalProgrammeCredits',
header: 'Major credits',
Header: <HeaderCell value="Major credits" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalOtherProgrammeCredits',
header: 'Non-major credits',
Header: <HeaderCell value="Non-major credits" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalWithoutStudyRightCredits',
header: 'Non-degree credits',
Header: <HeaderCell value="Non-degree credits" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalTransferCredits',
header: 'Transferred credits',
Header: <HeaderCell value="Transferred credits" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
],
[getTextIn]
)
const columnsStudents = useMemo<MRT_ColumnDef<any>[]>(
() => [
{
accessorKey: 'isStudyModule',
header: 'Type',
Cell: ({ cell }) => (cell.getValue<boolean>() ? 'Module' : 'Course'),
},
{
accessorKey: 'code',
header: 'Code',
Cell: ({ cell }) => <CodeCell code={cell.getValue<string>()} />,
},
{
accessorKey: 'name',
header: 'Name',
Cell: ({ cell }) => getTextIn(cell.getValue<Name>()),
},
{
accessorKey: 'totalAllStudents',
header: 'Total',
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
id: 'breakdown',
header: 'Breakdown of total',
columns: [
{
accessorKey: 'totalAllPassed',
header: 'Passed',
Header: <HeaderCell value="Passed" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalAllNotCompleted',
header: 'Not completed',
Header: <HeaderCell value="Not completed" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
],
},
{
id: 'breakdown-passed',
header: 'Breakdown of passed students',
columns: [
{
accessorKey: 'totalProgrammeStudents',
header: 'Major students',
Header: <HeaderCell value="Major students" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalOtherProgrammeStudents',
header: 'Non-major students',
Header: <HeaderCell value="Non-major students" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
{
accessorKey: 'totalWithoutStudyRightStudents',
header: 'Non-degree students',
Header: <HeaderCell value="Non-degree students" />,
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
],
},
{
id: 'excluded',
header: 'Not included in total or passed',
columns: [
{
accessorKey: 'totalTransferStudents',
header: 'Transferred\nstudents',
Cell: ({ cell }) => cell.getValue<number>(),
muiTableHeadCellProps: {
align: 'right',
},
muiTableBodyCellProps: {
align: 'right',
},
},
],
},
],
[getTextIn]
)
const table = useMaterialReactTable({
columns: showStudents ? columnsStudents : columns,
data: filteredData,
defaultColumn: { size: 0 },
enableDensityToggle: false,
enableHiding: false,
initialState: {
density: 'compact',
pagination: {
pageSize: 20,
pageIndex: 0,
},
sorting: [
{
id: 'totalAllStudents',
desc: true,
},
],
},
})
return <MaterialReactTable table={table} />
}
@@ -0,0 +1,56 @@
import { range } from 'lodash'
import { StudyProgrammeCourse } from '@/shared/types'
export const filterDataByYear = (data: StudyProgrammeCourse[], fromYear: number, toYear: number) => {
const yearRange = range(fromYear, Number(toYear) + 1)
const filteredAndMergedCourses = data
.filter(course => {
const arr = Object.keys(course.years).some(key => yearRange.includes(Number(key)))
return arr
})
.map(course => {
const values = Object.entries(course.years).reduce(
(acc, curr) => {
if (yearRange.includes(Number(curr[0]))) {
acc.totalAllStudents += curr[1].totalAllStudents
acc.totalAllPassed += curr[1].totalPassed
acc.totalAllNotCompleted += curr[1].totalNotCompleted
acc.totalAllCredits += curr[1].totalAllCredits
acc.totalProgrammeStudents += curr[1].totalProgrammeStudents
acc.totalProgrammeCredits += curr[1].totalProgrammeCredits
acc.totalOtherProgrammeStudents += curr[1].totalOtherProgrammeStudents
acc.totalOtherProgrammeCredits += curr[1].totalOtherProgrammeCredits
acc.totalWithoutStudyRightStudents += curr[1].totalWithoutStudyRightStudents
acc.totalWithoutStudyRightCredits += curr[1].totalWithoutStudyRightCredits
acc.totalTransferCredits += curr[1].totalTransferCredits
acc.totalTransferStudents += curr[1].totalTransferStudents
}
return acc
},
{
totalAllStudents: 0,
totalAllPassed: 0,
totalAllNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
totalOtherProgrammeStudents: 0,
totalOtherProgrammeCredits: 0,
totalWithoutStudyRightStudents: 0,
totalWithoutStudyRightCredits: 0,
totalTransferCredits: 0,
totalTransferStudents: 0,
}
)
return {
...values,
code: course.code,
name: course.name,
isStudyModule: course.isStudyModule,
}
})
return filteredAndMergedCourses
}