mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[ProgrammeOverview][StudyTracks][Progress] Add study tracks to the table
This commit is contained in:
@@ -380,6 +380,18 @@ test.describe('Degree programme overview', () => {
|
||||
await checkTableStats(page, tableContents, 'study-programme-progress')
|
||||
})
|
||||
|
||||
test('Years in the student progress table can be expanded and study track data will be shown', async ({ page }) => {
|
||||
const table = page.getByTestId('study-programme-progress-data-table').locator('tbody')
|
||||
const row = table.locator('tr').filter({ hasText: '2020 - 2021' })
|
||||
const studyTracks = ['MAT-EKO', 'MAT-MAT', 'MAT-TIE', 'MAT-TIL']
|
||||
|
||||
await row.getByRole('button').click()
|
||||
await Promise.all(studyTracks.map(studyTrack => expect(table.getByText(studyTrack)).toBeVisible()))
|
||||
|
||||
await row.getByRole('button').click()
|
||||
await Promise.all(studyTracks.map(studyTrack => expect(table.getByText(studyTrack)).not.toBeVisible()))
|
||||
})
|
||||
|
||||
test('Progress section', async ({ page }) => {
|
||||
const barChart = page.getByTestId('programme-progress-bar-chart-section')
|
||||
await expect(barChart.getByText('Less than 30 credits')).toBeVisible()
|
||||
|
||||
@@ -54,6 +54,7 @@ const createYearlyTitles = (limitList: CreditLimits) => {
|
||||
const calculateProgressStats = (
|
||||
level: 'bachelor' | 'master' | 'bcMs' | 'doctor',
|
||||
creditCounts: Record<string, number[]>,
|
||||
graduatedCount: Record<string, number>,
|
||||
yearlyTitles: Record<string, string[][]>,
|
||||
progressStats: Record<string, Record<string, number[][]>>,
|
||||
progId: string
|
||||
@@ -76,7 +77,7 @@ const calculateProgressStats = (
|
||||
if (level !== 'doctor' && !(level in yearlyTitles)) {
|
||||
yearlyTitles[level] = []
|
||||
for (const year of Object.values(limitsForYears)) {
|
||||
yearlyTitles[level].unshift(createYearlyTitles(year))
|
||||
yearlyTitles[level].unshift([...createYearlyTitles(year), 'Graduated'])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +94,7 @@ const calculateProgressStats = (
|
||||
const correctIndex = limit.length - 1 - limit.findIndex(element => credits >= (element[0] ?? 0))
|
||||
statsForYear[correctIndex] += 1
|
||||
}
|
||||
progressStats[level][progId].unshift(statsForYear)
|
||||
progressStats[level][progId].unshift([...statsForYear, graduatedCount[year] ?? 0])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,11 +166,11 @@ export const combineFacultyStudentProgress = async (
|
||||
const level = programmeTypes[degreeProgrammeType]
|
||||
|
||||
updateCreditAndGraduatedCount(level, stats.creditCounts, stats.graduatedCount)
|
||||
calculateProgressStats(level, stats.creditCounts, yearlyTitles, progressStats, progId)
|
||||
calculateProgressStats(level, stats.creditCounts, stats.graduatedCount, yearlyTitles, progressStats, progId)
|
||||
|
||||
if (level === 'master' && Object.keys(stats.creditCountsCombo).length > 0) {
|
||||
updateCreditAndGraduatedCount('bcMs', stats.creditCountsCombo, stats.graduatedCount)
|
||||
calculateProgressStats('bcMs', stats.creditCountsCombo, yearlyTitles, progressStats, progId)
|
||||
calculateProgressStats('bcMs', stats.creditCountsCombo, stats.graduatedCount, yearlyTitles, progressStats, progId)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -453,8 +453,8 @@ const getMainStatsByTrackAndYear = async (
|
||||
? getYearsObject({ years: years.filter(year => year !== 'Total'), emptyArrays: true })
|
||||
: {}
|
||||
|
||||
creditCountsComboByTrack[studyTrack][startYear].push(
|
||||
getCreditCount(studyRight.student.credits, startedInProgramme)
|
||||
creditCountsComboByTrack[studyTrack][bachelorsStartYear]?.push(
|
||||
getCreditCount(studyRight.student.credits, startedInBachelor)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import { generateGradientColors } from '@/util/color'
|
||||
import { NameWithCode } from '@oodikone/shared/types'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
|
||||
export const ProgrammeProgressChart = ({
|
||||
data,
|
||||
@@ -14,7 +14,7 @@ export const ProgrammeProgressChart = ({
|
||||
}: {
|
||||
data: number[][]
|
||||
labels: string[]
|
||||
longLabels: Record<string, NameWithCode>
|
||||
longLabels: Record<string, Name | string>
|
||||
names: string[]
|
||||
}) => {
|
||||
const { getTextIn } = useLanguage()
|
||||
@@ -75,7 +75,7 @@ export const ProgrammeProgressChart = ({
|
||||
type: 'bar',
|
||||
stack: 'total',
|
||||
itemStyle: {
|
||||
color: colors[index],
|
||||
color: seriesItem.name === 'Graduated' ? '#ddd' : colors[index],
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
|
||||
+23
-3
@@ -2,7 +2,11 @@ import Stack from '@mui/material/Stack'
|
||||
import Typography from '@mui/material/Typography'
|
||||
|
||||
import { ProgressBarChart } from '@/components/Charts/ProgressBarChart'
|
||||
import { ProgressTable } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/ProgressTable'
|
||||
import {
|
||||
ProgressTable,
|
||||
StudyTrackProgressStats,
|
||||
} from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/ProgressTable'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
|
||||
type ProgressStats = {
|
||||
chartStats: {
|
||||
@@ -17,11 +21,17 @@ export const ProgressOfStudents = ({
|
||||
progressComboStats,
|
||||
progressStats,
|
||||
studyProgramme,
|
||||
studyTrackComboStats,
|
||||
studyTrackStats,
|
||||
studyTracks,
|
||||
years,
|
||||
}: {
|
||||
progressComboStats: ProgressStats | null
|
||||
progressStats: ProgressStats | null
|
||||
studyProgramme: string
|
||||
studyTrackComboStats?: StudyTrackProgressStats | null
|
||||
studyTrackStats?: StudyTrackProgressStats | null
|
||||
studyTracks?: Record<string, string | Name>
|
||||
years: string[]
|
||||
}) => {
|
||||
return (
|
||||
@@ -37,7 +47,12 @@ export const ProgressOfStudents = ({
|
||||
years,
|
||||
}}
|
||||
/>
|
||||
<ProgressTable data={progressComboStats.tableStats} titles={progressComboStats.tableTitles} />
|
||||
<ProgressTable
|
||||
data={progressComboStats.tableStats}
|
||||
studyTrackStats={studyTrackComboStats}
|
||||
studyTracks={studyTracks}
|
||||
titles={progressComboStats.tableTitles}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
{progressStats !== null && (
|
||||
@@ -51,7 +66,12 @@ export const ProgressOfStudents = ({
|
||||
years,
|
||||
}}
|
||||
/>
|
||||
<ProgressTable data={progressStats.tableStats} titles={progressStats.tableTitles} />
|
||||
<ProgressTable
|
||||
data={progressStats.tableStats}
|
||||
studyTrackStats={studyTrackStats}
|
||||
studyTracks={studyTracks}
|
||||
titles={progressStats.tableTitles}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
+74
-10
@@ -1,14 +1,35 @@
|
||||
import Box from '@mui/material/Box'
|
||||
import Collapse from '@mui/material/Collapse'
|
||||
import TableBody from '@mui/material/TableBody'
|
||||
import TableCell from '@mui/material/TableCell'
|
||||
import TableContainer from '@mui/material/TableContainer'
|
||||
import TableHead from '@mui/material/TableHead'
|
||||
import TableRow from '@mui/material/TableRow'
|
||||
|
||||
import { Fragment, useState } from 'react'
|
||||
|
||||
import { ExpandableRow } from '@/components/common/ExpandableRow'
|
||||
import { StyledTable } from '@/components/common/StyledTable'
|
||||
import { ProgrammeProgressChart } from '@/components/Faculties/ProgrammeProgressChart'
|
||||
import { getCellKey, getRowKey } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/StudyTrackDataTable/util'
|
||||
import { Name } from '@oodikone/shared/types'
|
||||
|
||||
/** Progress category counts of each study track by starting year, e.g. `{ 'MAT-MAT': { '2020 - 2021': [2, 1, 0] } }` */
|
||||
export type StudyTrackProgressStats = Record<string, Record<string, number[]>>
|
||||
|
||||
export const ProgressTable = ({
|
||||
data,
|
||||
studyTrackStats,
|
||||
studyTracks,
|
||||
titles,
|
||||
}: {
|
||||
data: (string | number)[][]
|
||||
studyTrackStats?: StudyTrackProgressStats | null
|
||||
studyTracks?: Record<string, string | Name>
|
||||
titles: string[]
|
||||
}) => {
|
||||
const [visibleYears, setVisibleYears] = useState<Record<string, boolean>>({})
|
||||
|
||||
export const ProgressTable = ({ data, titles }: { data: (string | number)[][]; titles: string[] }) => {
|
||||
if (!data.length || !titles) {
|
||||
return null
|
||||
}
|
||||
@@ -19,6 +40,11 @@ export const ProgressTable = ({ data, titles }: { data: (string | number)[][]; t
|
||||
return parseInt(String(b[0]).split(' - ')[0], 10) - parseInt(String(a[0]).split(' - ')[0], 10)
|
||||
})
|
||||
|
||||
const studyTrackCodes = Object.keys(studyTrackStats ?? {}).toSorted()
|
||||
const isExpandable = !!studyTrackStats && studyTrackCodes.length > 0
|
||||
|
||||
const toggleVisibility = (year: string) => setVisibleYears(prev => ({ ...prev, [year]: !prev[year] }))
|
||||
|
||||
return (
|
||||
<TableContainer component={Box}>
|
||||
<StyledTable data-cy="study-programme-progress-data-table" nowrapBody showCellBorders>
|
||||
@@ -32,15 +58,53 @@ export const ProgressTable = ({ data, titles }: { data: (string | number)[][]; t
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedData.map((row, index) => (
|
||||
<TableRow key={getRowKey(row[0].toString(), index)}>
|
||||
{row.map((value, index) => (
|
||||
<TableCell align={index === 0 ? 'center' : 'right'} key={getCellKey(value.toString(), index)}>
|
||||
{value}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
{sortedData.map((row, index) => {
|
||||
const year = row[0].toString()
|
||||
|
||||
if (isExpandable && year !== 'Total') {
|
||||
return (
|
||||
<Fragment key={getRowKey(year, index)}>
|
||||
<ExpandableRow
|
||||
cypress={
|
||||
visibleYears[year]
|
||||
? 'study-programme-progress-hide-button'
|
||||
: 'study-programme-progress-show-button'
|
||||
}
|
||||
toggleVisibility={() => toggleVisibility(year)}
|
||||
visible={!!visibleYears[year]}
|
||||
yearArray={row}
|
||||
yearIndex={index}
|
||||
/>
|
||||
{visibleYears[year] ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={100}>
|
||||
<Collapse in={visibleYears[year]} timeout="auto" unmountOnExit>
|
||||
<ProgrammeProgressChart
|
||||
data={studyTrackCodes.map(
|
||||
code => studyTrackStats[code][year] ?? new Array(titles.length - 2).fill(0)
|
||||
)}
|
||||
labels={studyTrackCodes}
|
||||
longLabels={studyTracks ?? {}}
|
||||
names={titles.slice(2)}
|
||||
/>
|
||||
</Collapse>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={getRowKey(year, index)}>
|
||||
{row.map((value, index) => (
|
||||
<TableCell align={index === 0 ? 'center' : 'right'} key={getCellKey(value.toString(), index)}>
|
||||
{value}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</StyledTable>
|
||||
</TableContainer>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useLanguage } from '@/components/LanguagePicker/useLanguage'
|
||||
import { Section } from '@/components/Section'
|
||||
import { exportStudentTable } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/exportStudentTable'
|
||||
import { ProgressOfStudents } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/ProgressOfStudents'
|
||||
import { StudyTrackProgressStats } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/ProgressTable'
|
||||
import { StudentProgressPercentiles } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/StudentProgressPercentiles'
|
||||
import { StudyTrackDataTable } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/StudyTrackDataTable'
|
||||
import { StudyTrackSelector } from '@/pages/StudyProgramme/StudyTracksAndClassStatisticsTab/StudyTrackSelector'
|
||||
@@ -123,6 +124,41 @@ export const StudyTracksAndClassStatisticsTab = ({
|
||||
}
|
||||
|
||||
const hasStudyTracks = Object.keys(studyTrackStats?.studyTracks ?? {}).length > 1 && studyTrack === studyProgramme
|
||||
|
||||
const getStudyTrackProgressStats = (combo = false): StudyTrackProgressStats | null => {
|
||||
if (!hasStudyTracks || !studyTrackStats) return null
|
||||
|
||||
const creditCountsByTrack = combo ? studyTrackStats.creditCountsComboByTrack : studyTrackStats.creditCountsByTrack
|
||||
const maximumAmountOfCredits = combo ? targetCredits + 180 : targetCredits
|
||||
// console.log('Study track stats:', studyTrackStats, maximumAmountOfCredits)
|
||||
|
||||
// Tracks whose students have all graduated have no credit counts, but should still be shown
|
||||
const tracks = new Set([
|
||||
...Object.keys(creditCountsByTrack ?? {}),
|
||||
...Object.keys(studyTrackStats.graduatedCountByTrack ?? {}),
|
||||
])
|
||||
tracks.delete(studyProgramme)
|
||||
const emptyCreditCounts = Object.fromEntries(
|
||||
studyTrackStats.years.filter(year => year !== 'Total').map(year => [year, [] as number[]])
|
||||
)
|
||||
|
||||
return Object.fromEntries(
|
||||
[...tracks].map(track => {
|
||||
const stats = calculateStats(
|
||||
{ ...emptyCreditCounts, ...creditCountsByTrack?.[track] },
|
||||
studyTrackStats.graduatedCountByTrack[track],
|
||||
maximumAmountOfCredits
|
||||
)
|
||||
const statsByYear = Object.fromEntries(
|
||||
(stats?.tableStats ?? [])
|
||||
.filter(([year]) => year !== 'Total')
|
||||
.map(([year, , ...counts]) => [year, counts as number[]])
|
||||
)
|
||||
return [track, statsByYear]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const isStudyProgrammeMode = studyTrack === '' || studyTrack === studyProgramme
|
||||
|
||||
const calculateStudyTrackStats = (combo = false) => {
|
||||
@@ -284,6 +320,9 @@ export const StudyTracksAndClassStatisticsTab = ({
|
||||
progressComboStats={getProgressComboStats()}
|
||||
progressStats={getProgressStats()}
|
||||
studyProgramme={studyProgramme}
|
||||
studyTrackComboStats={getStudyTrackProgressStats(true)}
|
||||
studyTrackStats={getStudyTrackProgressStats(false)}
|
||||
studyTracks={studyTrackStats.studyTracks}
|
||||
years={studyTrackStats.years}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user