getAcademicPerformance method

  1. @override
Future<List<SemesterScoreDto>> getAcademicPerformance()
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;
}