mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[FilteredCourses][Populations] Use groupId
still wip
This commit is contained in:
committed by
Aaro Koivukangas
parent
d3b3d82fee
commit
e69e1d2f16
@@ -29,7 +29,6 @@ import { findByCourseAndSemesters, getStartAndEndDates } from '../services/stude
|
||||
import { getFullStudyProgrammeRights, handleQueryArrays, hasFullAccessToStudentData } from '../util'
|
||||
import { parseUnification } from '@oodikone/shared/types/unification'
|
||||
import logger from '../util/logger'
|
||||
import { getAllCourseIds } from 'src/services/courses/helpers'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -88,6 +87,7 @@ const obfuscateStuff = ({
|
||||
}),
|
||||
coursestatistics: {
|
||||
courses: coursestatistics.courses,
|
||||
idToGroupIdMap: coursestatistics.idToGroupIdMap,
|
||||
enrollments: coursestatistics.enrollments.map(enrollment => ({
|
||||
...enrollment,
|
||||
studentnumber: encryptionMap.get(enrollment.studentnumber) ?? enrollment.studentnumber,
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import crypto from 'crypto'
|
||||
import { Op, fn as dbFn, col as dbCol, fn, col } from 'sequelize'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Credit, Enrollment } from '@oodikone/shared/models'
|
||||
import { Name, EnrollmentState, Unification, CreditTypeCode } from '@oodikone/shared/types'
|
||||
import {
|
||||
enrollmentTimeDateThreshold,
|
||||
getSemesterCodeAt,
|
||||
MAX_POPULATION_SIZE,
|
||||
yearCodeToYear,
|
||||
yearToYearCode,
|
||||
} from '@oodikone/shared/util'
|
||||
import { enrollmentTimeDateThreshold, getSemesterCodeAt, yearCodeToYear, yearToYearCode } from '@oodikone/shared/util'
|
||||
import { dateIsBetween } from '@oodikone/shared/util/datetime'
|
||||
import logger from '../../../src/util/logger'
|
||||
import { CourseModel, CreditModel, EnrollmentModel, OrganizationModel, SISStudyRightElementModel } from '../../models'
|
||||
@@ -21,9 +15,8 @@ import {
|
||||
getEnrollmentsForCourses,
|
||||
getStudentNumberToSrElementsMap,
|
||||
} from './creditsAndEnrollmentsOfCourse'
|
||||
import { FormattedProgramme, getAllCourseIds, getIsOpen } from './helpers'
|
||||
import { FormattedProgramme } from './helpers'
|
||||
import { CourseYearlyStats } from '@oodikone/shared/types/courseYearlyStats'
|
||||
import { difference } from 'lodash-es'
|
||||
|
||||
const formatStudyRightElement = (studyRightElement: SISStudyRightElementModel): FormattedProgramme => ({
|
||||
code: studyRightElement.code,
|
||||
@@ -471,7 +464,7 @@ export const getCourseProvidersForCourses = async (courseIds: string[]) =>
|
||||
|
||||
export const getCourseDetails = async (courseGroupIds: string[]) =>
|
||||
CourseModel.findAll({
|
||||
attributes: ['groupId', 'code', 'name', 'substitution_groups'],
|
||||
attributes: ['id', 'groupId', 'code', 'name', 'substitutionGroups', 'isStudyModule'],
|
||||
where: { groupId: { [Op.in]: courseGroupIds }, isPrimary: true },
|
||||
raw: true,
|
||||
})
|
||||
|
||||
@@ -77,9 +77,9 @@ export const getOptionsForStudents = (
|
||||
return optionMap
|
||||
}
|
||||
|
||||
export const getCourses = (courseIds: string[]): Promise<Array<CourseStats>> =>
|
||||
export const getCourseGroupIds = (courseIds: string[]): Promise<Array<Pick<CourseStats, 'id' | 'groupId'>>> =>
|
||||
CourseModel.findAll({
|
||||
attributes: ['id', 'groupId', 'code', 'name', 'substitutionGroups', 'isStudyModule'],
|
||||
attributes: ['id', 'groupId'],
|
||||
where: {
|
||||
id: { [Op.in]: courseIds },
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getDegreeProgrammeType } from '../../util'
|
||||
import { now } from '../../util/clock'
|
||||
import { getCriteria } from '../studyProgramme/studyProgrammeCriteria'
|
||||
import { getCourseDetails } from '../courses'
|
||||
import { formatStudentForAPI } from './formatStatisticsForApi'
|
||||
import {
|
||||
type StudentEnrollment,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
getCredits,
|
||||
getStudyRightElementsForStudyRight,
|
||||
} from './getStudentData'
|
||||
import { getCourses, getOptionsForStudents } from './shared'
|
||||
import { getCourseGroupIds, getOptionsForStudents } from './shared'
|
||||
|
||||
export type OptimizedStatisticsQuery = {
|
||||
userId: string
|
||||
@@ -66,7 +67,10 @@ export const statisticsOf = async (
|
||||
const courseIds = new Set<string>()
|
||||
for (const { course_id } of credits) courseIds.add(course_id)
|
||||
for (const { course_id } of enrollments) courseIds.add(course_id)
|
||||
const courses = await getCourses([...courseIds])
|
||||
const idToGroupIdRows = await getCourseGroupIds([...courseIds])
|
||||
const idToGroupIdMap = Object.fromEntries(idToGroupIdRows.map(({ id, groupId }) => [id, groupId]))
|
||||
const groupIds = [...new Set(idToGroupIdRows.map(({ groupId }) => groupId))]
|
||||
const courses = await getCourseDetails(groupIds)
|
||||
|
||||
const optionData = getOptionsForStudents(studyRightElementsForStudyRight, degreeProgrammeType)
|
||||
const formattedStudents = students.map(student => {
|
||||
@@ -79,6 +83,6 @@ export const statisticsOf = async (
|
||||
return {
|
||||
students: formattedStudents,
|
||||
criteria,
|
||||
coursestatistics: { courses, enrollments, credits },
|
||||
coursestatistics: { courses, idToGroupIdMap, enrollments, credits },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +200,9 @@ export const CoursePopulation = () => {
|
||||
studentNumberFilter(),
|
||||
ageFilter(),
|
||||
courseFilter({
|
||||
courses: population?.coursestatistics.courses,
|
||||
courses: population?.coursestatistics.courses ?? [],
|
||||
includeSubstitutions: substitutions === 'true',
|
||||
idToGroupIdMap: population?.coursestatistics.idToGroupIdMap ?? {},
|
||||
}),
|
||||
creditsEarnedFilter(),
|
||||
hetuFilter(),
|
||||
|
||||
@@ -61,7 +61,10 @@ export const CustomPopulationWrapper = ({
|
||||
const filtersList = [
|
||||
genderFilter(),
|
||||
ageFilter(),
|
||||
courseFilter({ courses: population?.coursestatistics.courses ?? [] }),
|
||||
courseFilter({
|
||||
courses: population?.coursestatistics.courses ?? [],
|
||||
idToGroupIdMap: population?.coursestatistics.idToGroupIdMap ?? {},
|
||||
}),
|
||||
creditsEarnedFilter(),
|
||||
transferredToProgrammeFilter(),
|
||||
startYearAtUniFilter(),
|
||||
|
||||
@@ -26,11 +26,11 @@ const filterTexts = {
|
||||
label: 'Enrolled, No Grade',
|
||||
},
|
||||
}
|
||||
const getSubstitutionTooltip = (substitution_groups: string[][]) => (
|
||||
const SubstitutionTooltip = ({ substitutionGroups }: { substitutionGroups: string[][] }) => (
|
||||
<Typography fontSize="0.9rem" whiteSpace="pre-line">
|
||||
Included course's equivalent groups:
|
||||
<br />
|
||||
{substitution_groups.map(group => group.join(', ')).join('\n\n')}
|
||||
{substitutionGroups.map(group => group.join(', ')).join('\n\n')}
|
||||
</Typography>
|
||||
)
|
||||
export const CourseCard = ({
|
||||
@@ -64,10 +64,10 @@ export const CourseCard = ({
|
||||
<Stack direction="row" sx={{ justifyContent: 'space-between' }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography>{getTextIn(course?.name)}</Typography>
|
||||
{course?.substitutions?.length ? (
|
||||
<Tooltip title={getSubstitutionTooltip(course.substitution_groups)}>
|
||||
{course?.substitutionGroups?.length ? (
|
||||
<Tooltip title={<SubstitutionTooltip substitutionGroups={course.substitutionGroups} />}>
|
||||
<Typography sx={{ color: 'text.secondary' }}>
|
||||
{course?.code}... +{course?.substitutions?.length}
|
||||
{course?.code}... +{course?.substitutionGroups?.length}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -75,7 +75,7 @@ export const CourseCard = ({
|
||||
)}
|
||||
</Box>
|
||||
<ClearIcon
|
||||
data-cy={`courseFilter-${course?.code}-clear`}
|
||||
data-cy={`courseFilter-${course?.groupId}-clear`}
|
||||
onClick={() => onChange(null)}
|
||||
sx={{
|
||||
color: theme => theme.palette.error.dark,
|
||||
@@ -86,7 +86,7 @@ export const CourseCard = ({
|
||||
/>
|
||||
</Stack>
|
||||
<FilterSelect
|
||||
filterKey={`courseFilter-${course?.code}`}
|
||||
filterKey={`courseFilter-${course?.groupId}`}
|
||||
label="Select course"
|
||||
onChange={({ target }) => onChange(Number(target.value))}
|
||||
options={dropdownOptions}
|
||||
|
||||
@@ -13,8 +13,9 @@ type Options = {
|
||||
courses: Record<string, CourseStats>
|
||||
substitutedBy: Record<string, string[][]>
|
||||
includeSubstitutions: boolean
|
||||
idToGroupIdMap: Record<string, string>
|
||||
}
|
||||
type Args = { courses: CourseStats[]; includeSubstitutions?: boolean }
|
||||
type Args = { courses: CourseStats[]; includeSubstitutions?: boolean; idToGroupIdMap: Record<string, string> }
|
||||
type Precompute = any
|
||||
|
||||
const CourseFilterCard = ({ options, onOptionsChange }: FilterTrayProps<Options, Args, Precompute>) => {
|
||||
@@ -24,18 +25,18 @@ const CourseFilterCard = ({ options, onOptionsChange }: FilterTrayProps<Options,
|
||||
const { getTextIn } = useLanguage()
|
||||
|
||||
const dropdownOptions = Object.values(courseStats)
|
||||
.filter(cs => !courseFilters[cs.id])
|
||||
.filter(cs => !courseFilters[cs.groupId])
|
||||
.sort((a, b) => getSortRank(b.code) - getSortRank(a.code))
|
||||
.map(cs => ({
|
||||
key: `courseFilter-option-${cs.code}`,
|
||||
key: `courseFilter-option-${cs.groupId}`,
|
||||
text: `${cs.code} - ${getTextIn(cs.name)}`,
|
||||
value: cs.code,
|
||||
value: cs.groupId,
|
||||
}))
|
||||
|
||||
const setCourseFilter = (code: string, type: FTValue | null) => {
|
||||
const setCourseFilter = (groupId: string, type: FTValue | null) => {
|
||||
const newOpts = structuredClone(options)
|
||||
if (type === null) delete newOpts.courseFilters[code]
|
||||
else newOpts.courseFilters[code] = type
|
||||
if (type === null) delete newOpts.courseFilters[groupId]
|
||||
else newOpts.courseFilters[groupId] = type
|
||||
|
||||
onOptionsChange(newOpts)
|
||||
}
|
||||
@@ -49,12 +50,12 @@ const CourseFilterCard = ({ options, onOptionsChange }: FilterTrayProps<Options,
|
||||
options={dropdownOptions}
|
||||
value={null}
|
||||
/>
|
||||
{Object.entries(courseFilters).map(([code, type]) => (
|
||||
{Object.entries(courseFilters).map(([groupId, type]) => (
|
||||
<CourseCard
|
||||
course={courseStats[code]}
|
||||
course={courseStats[groupId]}
|
||||
filterType={type}
|
||||
key={`courseFilter-selected-course-${code}`}
|
||||
onChange={type => setCourseFilter(code, type as FTValue)}
|
||||
key={`courseFilter-selected-course-${groupId}`}
|
||||
onChange={type => setCourseFilter(groupId, type as FTValue)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -71,25 +72,27 @@ export const courseFilter = createFilter<Options, Args, Precompute>({
|
||||
courses: {},
|
||||
substitutedBy: {},
|
||||
includeSubstitutions: true,
|
||||
idToGroupIdMap: {},
|
||||
},
|
||||
|
||||
precompute: ({
|
||||
args,
|
||||
options,
|
||||
}: {
|
||||
args: { courses: CourseStats[]; includeSubstitutions?: boolean }
|
||||
args: Args
|
||||
options: {
|
||||
courses?: Record<string, CourseStats>
|
||||
substitutedBy?: Record<string, string[][]>
|
||||
includeSubstitutions?: boolean
|
||||
idToGroupIdMap?: Record<string, string>
|
||||
}
|
||||
}) => {
|
||||
const substitutedBy = args.courses.reduce<Record<string, string[][]>>((acc, course: CourseStats) => {
|
||||
const { code, substitutionGroups } = course
|
||||
const { groupId, substitutionGroups } = course
|
||||
if (substitutionGroups) {
|
||||
for (const group of substitutionGroups) {
|
||||
acc[code] ??= []
|
||||
acc[code].push(group)
|
||||
acc[groupId] ??= []
|
||||
acc[groupId].push(group)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,36 +101,42 @@ export const courseFilter = createFilter<Options, Args, Precompute>({
|
||||
|
||||
/* option.courses maybe frozen even when it should be used only within the scope of createFilter factory. */ {
|
||||
delete options.courses
|
||||
options.courses = Object.fromEntries(args.courses.map(course => [course.code, course]))
|
||||
options.courses = Object.fromEntries(args.courses.map(course => [course.groupId, course]))
|
||||
}
|
||||
|
||||
options.includeSubstitutions = args.includeSubstitutions ?? true
|
||||
options.substitutedBy = substitutedBy
|
||||
options.idToGroupIdMap = args.idToGroupIdMap
|
||||
},
|
||||
|
||||
isActive: ({ courseFilters }) => Object.keys(courseFilters).length > 0,
|
||||
|
||||
filter(student: FormattedStudent, { options }) {
|
||||
const { courses, enrollments } = student
|
||||
const passedCourseIds = courses
|
||||
.filter(({ credittypecode }) => credittypecode !== CreditTypeCode.FAILED)
|
||||
.map(({ course_id }) => course_id)
|
||||
const toGroupIds = (courseIds: string[]) => courseIds.map(id => options.idToGroupIdMap[id]).filter(Boolean)
|
||||
|
||||
const courseIds = courses.map(({ course_id }) => course_id)
|
||||
const enrollmentCodes = enrollments.map(({ course_id }) => course_id)
|
||||
const passedCourseGroupIds = toGroupIds(
|
||||
courses
|
||||
.filter(({ credittypecode }) => credittypecode !== CreditTypeCode.FAILED)
|
||||
.map(({ course_id }) => course_id)
|
||||
)
|
||||
|
||||
for (const [mainCode, filterType] of Object.entries(options.courseFilters)) {
|
||||
const courseGroupIds = toGroupIds(courses.map(({ course_id }) => course_id))
|
||||
const enrollmentGroupIds = toGroupIds(enrollments.map(({ course_id }) => course_id))
|
||||
|
||||
for (const [mainGroupId, filterType] of Object.entries(options.courseFilters)) {
|
||||
let foundPassed = false
|
||||
let foundAttainment = false
|
||||
let foundEnrollment = false
|
||||
|
||||
;[[mainCode], ...(options.includeSubstitutions ? (options.substitutedBy?.[mainCode] ?? []) : [])].forEach(
|
||||
group => {
|
||||
foundPassed = foundPassed ? true : group.every(code => passedCourseIds.includes(code))
|
||||
foundAttainment = foundAttainment ? true : group.every(code => courseIds.includes(code))
|
||||
foundEnrollment = foundEnrollment ? true : group.every(code => enrollmentCodes.includes(code))
|
||||
}
|
||||
)
|
||||
;[
|
||||
[mainGroupId],
|
||||
...(options.includeSubstitutions ? (options.substitutedBy?.[mainGroupId] ?? []) : []),
|
||||
].forEach(group => {
|
||||
foundPassed = foundPassed ? true : group.every(groupId => passedCourseGroupIds.includes(groupId))
|
||||
foundAttainment = foundAttainment ? true : group.every(groupId => courseGroupIds.includes(groupId))
|
||||
foundEnrollment = foundEnrollment ? true : group.every(groupId => enrollmentGroupIds.includes(groupId))
|
||||
})
|
||||
|
||||
switch (filterType) {
|
||||
case FilterType.ALL:
|
||||
@@ -150,16 +159,16 @@ export const courseFilter = createFilter<Options, Args, Precompute>({
|
||||
|
||||
selectors: {
|
||||
// NOTE: Remember FilterType.ALL === 0 when checking if courseFilters[course] exists
|
||||
isCourseSelected: ({ courseFilters }, course) => Object.values(FilterType).includes(courseFilters[course]),
|
||||
selectedCourseName: ({ courses }, courseCodes) => courses[courseCodes[0]]?.name,
|
||||
isCourseSelected: ({ courseFilters }, courseGroupId) => Object.values(FilterType).includes(courseFilters[courseGroupId]),
|
||||
selectedCourseName: ({ courses }, courseGroupIds) => courses[courseGroupIds[0]]?.name,
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleCourseSelection: (options, code: string) => {
|
||||
if (!Object.values(FilterType).includes(options.courseFilters[code])) {
|
||||
options.courseFilters[code] = FilterType.ALL
|
||||
toggleCourseSelection: (options, groupId: string) => {
|
||||
if (!Object.values(FilterType).includes(options.courseFilters[groupId])) {
|
||||
options.courseFilters[groupId] = FilterType.ALL
|
||||
} else {
|
||||
delete options.courseFilters[code]
|
||||
delete options.courseFilters[groupId]
|
||||
}
|
||||
|
||||
return options
|
||||
|
||||
@@ -4,10 +4,16 @@ import { FilterToggleIcon } from '@/components/common/FilterToggleIcon'
|
||||
import { isCourseSelected, toggleCourseSelection } from '@/components/FilterView/filters/courses'
|
||||
import { useFilters } from '@/components/FilterView/useFilters'
|
||||
|
||||
export const CourseFilterToggle = ({ courseCode, courseName }: { courseCode: string; courseName: string }) => {
|
||||
export const CourseFilterToggle = ({
|
||||
courseGroupId,
|
||||
courseName,
|
||||
}: {
|
||||
courseGroupId: string
|
||||
courseName: string
|
||||
}) => {
|
||||
const { useFilterSelector, useFilterDispatch: filterDispatch } = useFilters()
|
||||
|
||||
const isActive = useFilterSelector(isCourseSelected(courseCode))
|
||||
const isActive = useFilterSelector(isCourseSelected(courseGroupId))
|
||||
|
||||
const title = (
|
||||
<span>
|
||||
@@ -18,7 +24,10 @@ export const CourseFilterToggle = ({ courseCode, courseName }: { courseCode: str
|
||||
return (
|
||||
<Tooltip arrow placement="top" title={title}>
|
||||
<div>
|
||||
<FilterToggleIcon isActive={isActive} onClick={() => filterDispatch(toggleCourseSelection(courseCode))} />
|
||||
<FilterToggleIcon
|
||||
isActive={isActive}
|
||||
onClick={() => filterDispatch(toggleCourseSelection(courseGroupId))}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ export const PassFailEnrollments = ({
|
||||
header: 'Name',
|
||||
cell: ({ row }) => {
|
||||
const name = getTextIn(row.original.name)
|
||||
const { code } = row.original
|
||||
const { code, groupId } = row.original
|
||||
|
||||
const expansionStyle = row.getIsExpanded() ? { transform: 'rotate(90deg)' } : {}
|
||||
const expansionArrow = row.getCanExpand() ? (
|
||||
@@ -100,7 +100,7 @@ export const PassFailEnrollments = ({
|
||||
const linkComponent =
|
||||
row.originalSubRows === undefined ? (
|
||||
<Stack flexDirection="row" sx={{ m: 'auto', mr: '0' }}>
|
||||
<CourseFilterToggle courseCode={code} courseName={name ?? ''} />
|
||||
<CourseFilterToggle courseGroupId={groupId!} courseName={name ?? ''} />
|
||||
{!onlyIamRights ? (
|
||||
<Link
|
||||
to={`/coursestatistics?courseCodes=["${encodeURIComponent(code)}"]&separate=false&combineSubstitutions=true`}
|
||||
|
||||
@@ -49,6 +49,7 @@ export const PopulationCourseStatsFlat = ({
|
||||
...course,
|
||||
name: course.course.name,
|
||||
code: course.course.code,
|
||||
groupId: course.course.groupId,
|
||||
stats,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -56,11 +56,11 @@ export const PopulationCourseStats = ({
|
||||
.flatMap(course => {
|
||||
const defaultProgrammeCourses: FilteredProgrammeCourse[] = curriculum.defaultProgrammeCourses
|
||||
.filter(pg => pg.code === course.course.code)
|
||||
.map(programmeCourse => ({ ...course, ...programmeCourse }))
|
||||
.map(programmeCourse => ({ ...course, ...programmeCourse, groupId: course.course.groupId }))
|
||||
|
||||
const secondProgrammeCourses: FilteredProgrammeCourse[] = curriculum.secondProgrammeCourses
|
||||
.filter(pg => pg.code === course.course.code)
|
||||
.map(programmeCourse => ({ ...course, ...programmeCourse }))
|
||||
.map(programmeCourse => ({ ...course, ...programmeCourse, groupId: course.course.groupId }))
|
||||
|
||||
return [defaultProgrammeCourses, secondProgrammeCourses].flat()
|
||||
})
|
||||
|
||||
@@ -194,7 +194,7 @@ export const PopulationStatistics = () => {
|
||||
const filters: GenericFilter[] = [
|
||||
!useUserHasRestrictedAccess() ? ageFilter() : null,
|
||||
citizenshipFilter(),
|
||||
courseFilter({ courses }),
|
||||
courseFilter({ courses, idToGroupIdMap: population?.coursestatistics.idToGroupIdMap ?? {} }),
|
||||
creditDateFilter(),
|
||||
creditsEarnedFilter(),
|
||||
curriculumPeriodFilter(),
|
||||
|
||||
@@ -37,6 +37,7 @@ export const useGetFilters = (
|
||||
filters.tagsFilter(),
|
||||
filters.courseFilter({
|
||||
courses: population?.coursestatistics.courses ?? [],
|
||||
idToGroupIdMap: population?.coursestatistics.idToGroupIdMap ?? {},
|
||||
}),
|
||||
filters.creditDateFilter(),
|
||||
filters.creditsEarnedFilter(),
|
||||
|
||||
@@ -20,9 +20,10 @@ export type CourseModule = Pick<Module, 'code'> & {
|
||||
name: Name
|
||||
courses: FilteredProgrammeCourse[]
|
||||
stats?: Partial<FilteredCourseStats>
|
||||
groupId?: string
|
||||
}
|
||||
export type FilteredProgrammeCourse = FilteredCourse & ProgrammeCourse
|
||||
type FilteredCourseModule = FilteredCourse & { name: Name; code: string }
|
||||
export type FilteredProgrammeCourse = FilteredCourse & ProgrammeCourse & { groupId: string }
|
||||
type FilteredCourseModule = FilteredCourse & { name: Name; code: string; groupId: string }
|
||||
export type UnionOfFilteredModuleCourse = (
|
||||
| (Omit<FilteredCourseModule, 'stats'> & { stats: Partial<FilteredCourseStats> })
|
||||
| CourseModule
|
||||
@@ -235,14 +236,14 @@ export const filterCourses = (
|
||||
if (!courseStatistics) return []
|
||||
|
||||
const coursestats = new Map(
|
||||
courseStatistics.courses.map(course => [course.id, { course, ...structuredClone(courseBaseStats) }])
|
||||
courseStatistics.courses.map(course => [course.groupId, { course, ...structuredClone(courseBaseStats) }])
|
||||
)
|
||||
|
||||
for (const { studentNumber, studyrightStart, enrollments, courses } of filteredStudents) {
|
||||
const studentStartingYear = new Date(studyrightStart).getFullYear()
|
||||
|
||||
for (const { course_id, state, enrollment_date_time } of enrollments) {
|
||||
const course = coursestats.get(course_id)
|
||||
const course = coursestats.get(courseStatistics.idToGroupIdMap[course_id])
|
||||
if (!course) continue
|
||||
|
||||
const initialDate = new Date(enrollment_date_time)
|
||||
@@ -255,7 +256,7 @@ export const filterCourses = (
|
||||
}
|
||||
|
||||
for (const { course_id, grade, credittypecode, date } of courses) {
|
||||
const course = coursestats.get(course_id)
|
||||
const course = coursestats.get(courseStatistics.idToGroupIdMap[course_id])
|
||||
if (!course) continue
|
||||
|
||||
const passingGrade = [CreditTypeCode.PASSED, CreditTypeCode.APPROVED].includes(credittypecode)
|
||||
|
||||
@@ -57,7 +57,9 @@ export type CloseToGraduationData = {
|
||||
}
|
||||
|
||||
export type PopulationCourseStats = {
|
||||
// One row per groupId (isPrimary=true course for that group)
|
||||
courses: CourseStats[]
|
||||
idToGroupIdMap: Record<string, string>
|
||||
enrollments: Pick<
|
||||
Enrollment,
|
||||
'course_id' | 'state' | 'enrollment_date_time' | 'semestercode' | 'studentnumber' | 'studyright_id'
|
||||
|
||||
Reference in New Issue
Block a user