getAcademicPerformance method
override
Fetches academic performance (scores) for all semesters.
Returns a list of SemesterScoreDto ordered from most recent to oldest, each containing individual course scores and semester summary statistics.
Implementation
@override
Future<List<SemesterScoreDto>> getAcademicPerformance() async {
final response = await _studentQueryDio.get(
'QryScore.jsp',
queryParameters: {'format': '-2'},
);
final document = parse(response.data);
// Semester labels are in submit button values: "114 學年度 第 1 學期 (2025 - Fall)"
final semesterPattern = RegExp(r'(\d+)\s*學年度\s*第\s*(\d+)\s*學期');
// Walk buttons and tables in document order, pairing each semester button
// with the next table. Other submits (print/reset) leave pending intact.
final results = <SemesterScoreDto>[];
SemesterDto? pendingSemester;
final nodes = document.querySelectorAll("input[type='submit'], table");
for (final node in nodes) {
if (node.localName == 'input') {
if (semesterPattern.firstMatch(node.attributes['value'] ?? '')
case final match?) {
pendingSemester = (
year: int.parse(match.group(1)!),
term: int.parse(match.group(2)!),
);
}
continue;
}
if (pendingSemester == null) continue;
final rows = node.querySelectorAll('tr');
final scores = <ScoreDto>[];
double? average;
double? conduct;
double? totalCredits;
double? creditsPassed;
String? note;
// Skip header row; data rows have 9+ cells, summary rows have 1-2
for (final row in rows.skip(1)) {
final cells = row.querySelectorAll('th, td');
if (cells.length >= 9) {
final scoreText = _parseCellText(cells[7]);
final (scoreValue, status) = _parseScore(scoreText);
scores.add((
number: _parseCellText(cells[0]),
courseNameZh: _parseCellText(cells[2]),
courseNameEn: _parseCellText(cells[3]),
courseCode: _parseCellText(cells[4]),
score: scoreValue,
status: status,
));
} else if (cells.length == 2) {
final label = cells[0].text;
final value = _parseCellText(cells[1]);
if (label.contains('Average')) {
average = double.tryParse(value ?? '');
} else if (label.contains('Conduct')) {
conduct = double.tryParse(value ?? '');
} else if (label.contains('Total Credits')) {
totalCredits = double.tryParse(value ?? '');
} else if (label.contains('Credits Passed')) {
creditsPassed = double.tryParse(value ?? '');
} else if (label.contains('Note')) {
note = value;
}
}
}
results.add((
semester: pendingSemester,
scores: scores,
average: average,
conduct: conduct,
totalCredits: totalCredits,
creditsPassed: creditsPassed,
note: note,
));
pendingSemester = null;
}
return results;
}