getAcademicPerformance method

Future<List<SemesterScoreDto>> getAcademicPerformance()

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

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*學期');
  final semesterButtons = document.querySelectorAll("input[type='submit']");
  final semesterMatches = semesterButtons
      .map((btn) => semesterPattern.firstMatch(btn.attributes['value'] ?? ''))
      .whereType<RegExpMatch>()
      .toList();

  final tables = document.querySelectorAll('table');

  final results = <SemesterScoreDto>[];
  for (var i = 0; i < tables.length && i < semesterMatches.length; i++) {
    final match = semesterMatches[i];
    final semester = (
      year: int.parse(match.group(1)!),
      term: int.parse(match.group(2)!),
    );

    final rows = tables[i].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]),
          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: semester,
      scores: scores,
      average: average,
      conduct: conduct,
      totalCredits: totalCredits,
      creditsPassed: creditsPassed,
      note: note,
    ));
  }

  return results;
}