[ProgrammeOverview][StudyTracks][Progress] Add study tracks to the table

This commit is contained in:
Aaro Koivukangas
2026-10-12 12:38:13 +03:00
parent 631b652553
commit 48e2b8256c
7 changed files with 158 additions and 22 deletions
+12
View File
@@ -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,
@@ -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>
@@ -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}
/>
)}