getCourseTable method

Future<List<ScheduleDto>> getCourseTable({
  1. required String username,
  2. required SemesterDto semester,
})

Fetches the course schedule table for a specific student and semester.

Returns a list of course offerings enrolled by the student, including:

  • Course details (name, credits, hours)
  • Schedule information (days, periods, classroom)
  • Teacher and class information
  • Enrollment status and remarks

The username should be a student ID, and semester should be obtained from getCourseSemesterList.

Throws an Exception if no courses are found for the given semester.

Implementation

Future<List<ScheduleDto>> getCourseTable({
  required String username,
  required SemesterDto semester,
}) async {
  final response = await _courseDio.get(
    'Select.jsp',
    queryParameters: {
      'format': '-2',
      'code': username,
      'year': semester.year,
      'sem': semester.term,
    },
  );

  final document = parse(response.data);
  final tables = document.querySelectorAll('table');
  if (tables.length < 2) {
    throw Exception('Expected timetable grid and course list tables.');
  }

  // Parse the timetable grid (table[0]) for per-timeslot schedule+classroom
  // Structure: header row has day labels (一–日), data rows have period
  // labels in column 0 and course cells with <a> links for the rest.
  final timetableGrid = tables[0];
  final timetableRows = timetableGrid.querySelectorAll('tr');
  if (timetableRows.length < 3) {
    throw Exception('Timetable grid has no data rows.');
  }

  // Build column -> DayOfWeek map from header row
  const dayCharToEnum = {
    '一': DayOfWeek.monday,
    '二': DayOfWeek.tuesday,
    '三': DayOfWeek.wednesday,
    '四': DayOfWeek.thursday,
    '五': DayOfWeek.friday,
    '六': DayOfWeek.saturday,
    '日': DayOfWeek.sunday,
  };
  final headerCells = timetableRows[1].children;
  final colToDayMap = <int, DayOfWeek>{};
  for (var i = 1; i < headerCells.length; i++) {
    final text = headerCells[i].text.trim();
    final day = dayCharToEnum.entries
        .firstWhereOrNull((e) => text.contains(e.key))
        ?.value;
    if (day != null) colToDayMap[i] = day;
  }

  // Build schedule map keyed by course ID from the grid
  final periodRegex = RegExp(r'第 (\S) 節');
  final scheduleMap = <String, List<(DayOfWeek, Period, ReferenceDto?)>>{};

  for (var rowIndex = 2; rowIndex < timetableRows.length; rowIndex++) {
    final cells = timetableRows[rowIndex].children;
    if (cells.isEmpty) continue;

    final periodMatch = periodRegex.firstMatch(cells[0].text);
    if (periodMatch == null) continue;
    final period = Period.values.firstWhereOrNull(
      (p) => p.code == periodMatch.group(1),
    );
    if (period == null) continue;

    for (var colIndex = 1; colIndex < cells.length; colIndex++) {
      final day = colToDayMap[colIndex];
      if (day == null) continue;

      final anchors = cells[colIndex].querySelectorAll('a');
      if (anchors.isEmpty) continue;

      final courseRef = _parseAnchorRef(anchors[0]);
      final courseId = courseRef.id;
      if (courseId == null) continue;

      final classroomRef = anchors.length >= 3
          ? _parseAnchorRef(anchors[2])
          : null;

      scheduleMap.putIfAbsent(courseId, () => []);
      scheduleMap[courseId]!.add((day, period, classroomRef));
    }
  }

  // Parse the course list (table[1]) for metadata
  final courseListTable = tables[1];
  final tableRows = courseListTable.querySelectorAll('tr');
  final trimmedTableRows = tableRows.sublist(2, tableRows.length - 1);
  if (trimmedTableRows.isEmpty) {
    throw Exception('No courses found in the selection table.');
  }

  return trimmedTableRows.map((row) {
    final cells = row.children;

    final number = _parseCellText(cells[0]);
    final course = _parseCellRef(cells[1]);
    final phase = int.tryParse(cells[2].text.trim());
    final credits = double.tryParse(cells[3].text.trim());
    final hours = int.tryParse(cells[4].text.trim());
    final type = _parseCellText(cells[5]);
    final teacher = _parseCellRef(cells[6]);
    final classes = _parseCellRefs(cells[7]);

    // Look up schedule+classroom from the timetable grid by course ID
    final schedule = course.id != null ? scheduleMap[course.id!] : null;

    final status = _parseCellText(cells[16]);
    final language = _parseCellText(cells[17]);
    final syllabusId = _parseCellRef(cells[18]).id;
    final remarks = _parseCellText(cells[19]);

    return (
      number: number,
      course: course,
      phase: phase,
      credits: credits,
      hours: hours,
      type: type,
      teacher: teacher,
      classes: classes,
      schedule: schedule,
      status: status,
      language: language,
      syllabusId: syllabusId,
      remarks: remarks,
    );
  }).toList();
}