Skip to content

Commit 4aa9879

Browse files
Copilotstasel
andauthored
Add average test score to cohorts API and UI (#281)
* Initial plan * Add average test score to cohorts API and UI Co-authored-by: stasel <[email protected]> * Simplify reduce logic per code review feedback Co-authored-by: stasel <[email protected]> * Extract average calculation to separate function and use null instead of undefined Co-authored-by: stasel <[email protected]> * Fix binding issue causing calculateAverageTestScore to be undefined Co-authored-by: stasel <[email protected]> * Make calculateAverageTestScore static to avoid binding issues Co-authored-by: stasel <[email protected]> * Minor fixes * Improved average calculation --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: stasel <[email protected]>
1 parent 7d13008 commit 4aa9879

File tree

5 files changed

+58
-4
lines changed

5 files changed

+58
-4
lines changed

client/src/components/cohorts/CohortAccordion.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export const CohortAccordion = ({ cohortInfo }: CohortAccordionProps) => {
5555
<TableCell sx={headerStyle} width={50}>
5656
Work Permit
5757
</TableCell>
58+
<TableCell sx={headerStyle} width={100}>
59+
Avg Score
60+
</TableCell>
5861
<TableCell sx={headerStyle} width={50}>
5962
Strikes
6063
</TableCell>
@@ -83,6 +86,9 @@ export const CohortAccordion = ({ cohortInfo }: CohortAccordionProps) => {
8386
</TableCell>
8487
<TableCell>{trainee.location}</TableCell>
8588
<TableCell>{convertToString(trainee.hasWorkPermit)}</TableCell>
89+
<TableCell sx={{ color: getScoreColor(trainee.averageTestScore) }}>
90+
{trainee.averageTestScore !== null ? trainee.averageTestScore.toFixed(1) : '-'}
91+
</TableCell>
8692
<TableCell>{trainee.strikes}</TableCell>
8793
<TableCell sx={{ whiteSpace: 'nowrap' }} onClick={(e) => e.stopPropagation()}>
8894
<div>
@@ -128,3 +134,16 @@ const convertToString = (value: boolean | null | undefined) => {
128134
}
129135
return value ? 'Yes' : 'No';
130136
};
137+
138+
const getScoreColor = (score: number | null) => {
139+
if (score === null) {
140+
return 'inherit';
141+
}
142+
if (score < 7) {
143+
return 'orange';
144+
}
145+
if (score >= 8.5) {
146+
return 'green';
147+
}
148+
return 'inherit';
149+
};

client/src/models/Cohorts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ export interface TraineeSummary {
1919
LearningStatus: LearningStatus;
2020
JobPath: string;
2121
strikes: number;
22+
averageTestScore: number | null;
2223
}

server/src/controllers/CohortsController.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request, Response } from 'express';
22
import { TraineesRepository } from '../repositories';
3-
import { LearningStatus, Trainee } from '../models';
3+
import { LearningStatus, calculateAverageTestScore, Trainee } from '../models';
44

55
interface Cohort {
66
cohort: number | null;
@@ -21,6 +21,7 @@ interface TraineeSummary {
2121
LearningStatus: string;
2222
JobPath: string;
2323
strikes: number;
24+
averageTestScore: number | null;
2425
}
2526

2627
export interface CohortsControllerType {
@@ -47,14 +48,14 @@ export class CohortsController implements CohortsControllerType {
4748

4849
// Sort trainees in each group
4950
Object.values(cohortDictionary).forEach((trainees) => {
50-
trainees?.sort(this.compareTraineeInCohort);
51+
trainees?.sort(this.compareTraineeInCohort.bind(this));
5152
});
5253
// Convert dictionary to array of cohorts
5354
const result: Cohort[] = Object.entries(cohortDictionary).map(([cohortNumber, trainees]) => {
5455
const cohortNumberInt = Number.parseInt(cohortNumber);
5556
return {
5657
cohort: isNaN(cohortNumberInt) ? null : cohortNumberInt,
57-
trainees: (trainees ?? []).map(this.getTraineeSummary),
58+
trainees: (trainees ?? []).map(this.getTraineeSummary.bind(this)),
5859
};
5960
});
6061
res.status(200).json(result);
@@ -75,6 +76,7 @@ export class CohortsController implements CohortsControllerType {
7576
LearningStatus: trainee.educationInfo.learningStatus,
7677
JobPath: trainee.employmentInfo.jobPath,
7778
strikes: trainee.educationInfo.strikes.length,
79+
averageTestScore: calculateAverageTestScore(trainee.educationInfo.tests),
7880
};
7981
}
8082

server/src/models/Test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface Test {
44
readonly id: string;
55
date: Date;
66
type: TestType;
7-
score?: number;
7+
score: number | null;
88
result: TestResult;
99
comments?: string;
1010
}
@@ -20,3 +20,33 @@ export const validateTest = (test: Test): void => {
2020
throw new Error(`Unknown test type [${Object.values(TestType)}]`);
2121
}
2222
};
23+
24+
// Calculate average of all test scores, taking only the highest score for each test type
25+
export const calculateAverageTestScore = (tests: Test[]): number | null => {
26+
// Group by test type
27+
const testsByType = Object.groupBy(tests, (test) => test.type);
28+
29+
// Select highest test for each type
30+
const scores = Object.values(testsByType)
31+
.map((testGroup) => {
32+
return getTestWithMaxScore(testGroup)?.score;
33+
})
34+
.filter((score) => score !== null && score !== undefined);
35+
36+
// No scores, no average
37+
if (scores.length === 0) {
38+
return null;
39+
}
40+
41+
// Calculate average
42+
const sum = scores.reduce((acc, score) => acc + score, 0);
43+
return sum / scores.length;
44+
};
45+
46+
// Helper function to get the best test (highest score) from a list of tests
47+
const getTestWithMaxScore = (tests: Test[]): Test | null => {
48+
const sortedByScore = tests
49+
.filter((test: Test) => Number.isFinite(test.score))
50+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
51+
return sortedByScore[0] ?? null;
52+
};

server/src/repositories/TraineesRepository.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ export class MongooseTraineesRepository implements TraineesRepository {
7070
'personalInfo.hasWorkPermit',
7171
'educationInfo.learningStatus',
7272
'educationInfo.strikes.id',
73+
'educationInfo.tests.score',
74+
'educationInfo.tests.type',
7375
'educationInfo.currentCohort',
7476
'employmentInfo.jobPath',
7577
])

0 commit comments

Comments
 (0)