diff --git a/packages/uni_app/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart b/packages/uni_app/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart index 402db34a1..8a5cf944a 100644 --- a/packages/uni_app/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart +++ b/packages/uni_app/lib/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart @@ -4,14 +4,18 @@ import 'package:uni/controller/parsers/schedule/new_api/parser.dart'; import 'package:uni/model/entities/lecture.dart'; import 'package:uni/session/flows/base/session.dart'; -/// Class for fetching the user's lectures from the schedule's HTML page. -class ScheduleFetcherNewApi extends ScheduleFetcher { +/// Abstract base class for fetching lectures from the schedule's HTML page. +abstract class ScheduleFetcherNewApiBase extends ScheduleFetcher { + String getEndpointView(); + + Map getQueryParams(Session session); + @override List getEndpoints(Session session) { final urls = NetworkRouter.getBaseUrlsFromSession( session, - ).map((url) => '${url}hor_geral.estudantes_view').toList(); + ).map((url) => '${url}hor_geral.${getEndpointView()}').toList(); return urls; } @@ -22,7 +26,7 @@ class ScheduleFetcherNewApi extends ScheduleFetcher { final lectiveYear = getLectiveYear(DateTime.now()); final scheduleResponse = await NetworkRouter.getWithCookies(url, { - 'pv_num_unico': session.username, + ...getQueryParams(session), 'pv_ano_lectivo': lectiveYear.toString(), 'pv_periodos': '1', }, session); @@ -47,3 +51,28 @@ class ScheduleFetcherNewApi extends ScheduleFetcher { return lectures; } } + +/// Class for fetching student lectures from the schedule's HTML page. +class ScheduleFetcherNewApi extends ScheduleFetcherNewApiBase { + @override + String getEndpointView() => 'estudantes_view'; + + @override + Map getQueryParams(Session session) => { + 'pv_num_unico': session.username, + }; +} + +/// Class for fetching professor lectures from the schedule's HTML page. +class ScheduleFetcherNewApiProfessor extends ScheduleFetcherNewApiBase { + ScheduleFetcherNewApiProfessor({required this.professorCode}); + final String professorCode; + + @override + String getEndpointView() => 'docentes_view'; + + @override + Map getQueryParams(Session session) => { + 'pv_doc_codigo': professorCode, + }; +} diff --git a/packages/uni_app/lib/controller/parsers/schedule/new_api/parser.dart b/packages/uni_app/lib/controller/parsers/schedule/new_api/parser.dart index 4133a1802..a2fabb5b0 100644 --- a/packages/uni_app/lib/controller/parsers/schedule/new_api/parser.dart +++ b/packages/uni_app/lib/controller/parsers/schedule/new_api/parser.dart @@ -35,9 +35,7 @@ List getLecturesFromApiResponse(http.Response response) { lecture.persons.map((person) => person.acronym).join('+'), _filterTeacherName(lecture.persons.first.name), _filterTeacherCode(lecture.persons.first.name), - lecture.classes.length > 1 - ? '${lecture.classes.first.acronym} + ${lecture.classes.length - 1}' - : lecture.classes.first.acronym, + lecture.classes.first.acronym, lecture.units.first.sigarraId, ), ) diff --git a/packages/uni_app/lib/generated/intl/messages_en.dart b/packages/uni_app/lib/generated/intl/messages_en.dart index 6fb90730a..725abab63 100644 --- a/packages/uni_app/lib/generated/intl/messages_en.dart +++ b/packages/uni_app/lib/generated/intl/messages_en.dart @@ -113,6 +113,7 @@ class MessageLookup extends MessageLookupByLibrary { "check_internet": MessageLookupByLibrary.simpleMessage( "Check your internet connection", ), + "classProfessor": MessageLookupByLibrary.simpleMessage("Class Professor"), "class_registration": MessageLookupByLibrary.simpleMessage( "Class Registration", ), diff --git a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart index 0d32594cb..f9bd8240e 100644 --- a/packages/uni_app/lib/generated/intl/messages_pt_PT.dart +++ b/packages/uni_app/lib/generated/intl/messages_pt_PT.dart @@ -119,6 +119,9 @@ class MessageLookup extends MessageLookupByLibrary { "check_internet": MessageLookupByLibrary.simpleMessage( "Verifica a tua ligação à internet", ), + "classProfessor": MessageLookupByLibrary.simpleMessage( + "Professor da Turma", + ), "class_registration": MessageLookupByLibrary.simpleMessage( "Inscrição de Turmas", ), diff --git a/packages/uni_app/lib/generated/l10n.dart b/packages/uni_app/lib/generated/l10n.dart index d713a336e..9ece8570d 100644 --- a/packages/uni_app/lib/generated/l10n.dart +++ b/packages/uni_app/lib/generated/l10n.dart @@ -1688,6 +1688,16 @@ class S { return Intl.message('Instructor', name: 'instructor', desc: '', args: []); } + /// `Class Professor` + String get classProfessor { + return Intl.message( + 'Class Professor', + name: 'classProfessor', + desc: '', + args: [], + ); + } + /// `Lectures` String get lectures { return Intl.message('Lectures', name: 'lectures', desc: '', args: []); diff --git a/packages/uni_app/lib/l10n/intl_en.arb b/packages/uni_app/lib/l10n/intl_en.arb index e7e9a27a7..0cbb8976a 100644 --- a/packages/uni_app/lib/l10n/intl_en.arb +++ b/packages/uni_app/lib/l10n/intl_en.arb @@ -398,6 +398,8 @@ "@courseRegent": {}, "instructor": "Instructor", "@instructor": {}, + "classProfessor": "Class Professor", + "@classProfessor": {}, "lectures": "Lectures", "@lectures": {}, "exams": "Exams", diff --git a/packages/uni_app/lib/l10n/intl_pt_PT.arb b/packages/uni_app/lib/l10n/intl_pt_PT.arb index 10587cafd..dfc0b852b 100644 --- a/packages/uni_app/lib/l10n/intl_pt_PT.arb +++ b/packages/uni_app/lib/l10n/intl_pt_PT.arb @@ -398,6 +398,8 @@ "@courseRegent": {}, "instructor": "Docente", "@instructor": {}, + "classProfessor": "Professor da Turma", + "@classProfessor": {}, "lectures": "Aulas", "@lectures": {}, "exams": "Exames", diff --git a/packages/uni_app/lib/model/providers/riverpod/course_units_info_provider.dart b/packages/uni_app/lib/model/providers/riverpod/course_units_info_provider.dart index e6ad0f4ea..9985fc066 100644 --- a/packages/uni_app/lib/model/providers/riverpod/course_units_info_provider.dart +++ b/packages/uni_app/lib/model/providers/riverpod/course_units_info_provider.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uni/controller/fetchers/course_units_fetcher/course_units_info_fetcher.dart'; +import 'package:uni/controller/fetchers/schedule_fetcher/schedule_fetcher_new_api.dart'; import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; import 'package:uni/model/entities/course_units/course_unit_directory.dart'; @@ -12,7 +13,9 @@ import 'package:uni/model/providers/riverpod/session_provider.dart'; typedef SheetsMap = Map; typedef ClassesMap = Map>; typedef FilesMap = Map>; -typedef CourseUnitsInfoState = (SheetsMap, ClassesMap, FilesMap); +typedef ClassProfessorsMap = Map>>; +typedef CourseUnitsInfoState = + (SheetsMap, ClassesMap, FilesMap, ClassProfessorsMap); final courseUnitsInfoProvider = AsyncNotifierProvider( @@ -45,12 +48,21 @@ class CourseUnitsInfoNotifier ); } + UnmodifiableMapView>> + get courseUnitsClassProfessors { + final currentState = state.value; + return UnmodifiableMapView( + currentState?.$4 ?? >>{}, + ); + } + @override Future loadFromStorage() async { return ( {}, >{}, >{}, + >>{}, ); } @@ -60,6 +72,7 @@ class CourseUnitsInfoNotifier {}, >{}, >{}, + >>{}, ); } @@ -82,11 +95,17 @@ class CourseUnitsInfoNotifier {}, >{}, >{}, + >>{}, ); final updatedSheetsMap = Map.from(currentState.$1); updatedSheetsMap[courseUnit] = sheet; - updateState((updatedSheetsMap, currentState.$2, currentState.$3)); + updateState(( + updatedSheetsMap, + currentState.$2, + currentState.$3, + currentState.$4, + )); } Future fetchCourseUnitClasses(CourseUnit courseUnit) async { @@ -111,13 +130,19 @@ class CourseUnitsInfoNotifier {}, >{}, >{}, + >>{}, ); final updatedClassesMap = Map>.from( currentState.$2, ); updatedClassesMap[courseUnit] = classes; - updateState((currentState.$1, updatedClassesMap, currentState.$3)); + updateState(( + currentState.$1, + updatedClassesMap, + currentState.$3, + currentState.$4, + )); } Future fetchCourseUnitFiles(CourseUnit courseUnit) async { @@ -142,12 +167,73 @@ class CourseUnitsInfoNotifier {}, >{}, >{}, + >>{}, ); final updatedFilesMap = Map>.from( currentState.$3, ); updatedFilesMap[courseUnit] = files; - updateState((currentState.$1, currentState.$2, updatedFilesMap)); + updateState(( + currentState.$1, + currentState.$2, + updatedFilesMap, + currentState.$4, + )); + } + + Future fetchClassProfessors(CourseUnit courseUnit) async { + final session = await ref.read(sessionProvider.future); + if (session == null) { + return; + } + + final sheet = courseUnitsSheets[courseUnit]; + if (sheet == null) { + return; + } + + final professors = sheet.professors; + final Map> classProfessors = {}; + final courseAcronym = courseUnit.abbreviation; + + for (final professor in professors) { + final fetcher = ScheduleFetcherNewApiProfessor( + professorCode: professor.code, + ); + final lectures = await fetcher.getLectures(session); + + for (final lecture in lectures) { + if (lecture.classNumber.isNotEmpty && + lecture.acronym == courseAcronym && + lecture.typeClass != 'T') { + if (!classProfessors.containsKey(lecture.classNumber)) { + classProfessors[lecture.classNumber] = []; + } + if (!classProfessors[lecture.classNumber]!.contains(professor)) { + classProfessors[lecture.classNumber]!.add(professor); + } + } + } + } + + final currentState = + state.value ?? + ( + {}, + >{}, + >{}, + >>{}, + ); + + final updatedClassProfessorsMap = + Map>>.from(currentState.$4); + updatedClassProfessorsMap[courseUnit] = classProfessors; + updateState(( + currentState.$1, + currentState.$2, + currentState.$3, + updatedClassProfessorsMap, + )); } } diff --git a/packages/uni_app/lib/view/course_unit_info/course_unit_info.dart b/packages/uni_app/lib/view/course_unit_info/course_unit_info.dart index abdef7134..394cff8c7 100644 --- a/packages/uni_app/lib/view/course_unit_info/course_unit_info.dart +++ b/packages/uni_app/lib/view/course_unit_info/course_unit_info.dart @@ -36,6 +36,13 @@ class CourseUnitDetailPageViewState void initState() { super.initState(); tabController = TabController(vsync: this, length: 3); + tabController.addListener(_onTabChanged); + } + + void _onTabChanged() { + if (tabController.index == 1) { + loadClasses(force: false); + } } Future loadInfo({required bool force}) async { @@ -52,17 +59,30 @@ class CourseUnitDetailPageViewState if (courseUnitFiles == null || force) { await courseUnitsProvider.fetchCourseUnitFiles(widget.courseUnit); } + } + + Future loadClasses({required bool force}) async { + final courseUnitsProvider = ref.read(courseUnitsInfoProvider.notifier); final courseUnitClasses = courseUnitsProvider.courseUnitsClasses[widget.courseUnit]; if (courseUnitClasses == null || force) { await courseUnitsProvider.fetchCourseUnitClasses(widget.courseUnit); } + + final courseUnitClassProfessors = + courseUnitsProvider.courseUnitsClassProfessors[widget.courseUnit]; + if (courseUnitClassProfessors == null || force) { + await courseUnitsProvider.fetchClassProfessors(widget.courseUnit); + } } @override Future onRefresh() async { await loadInfo(force: true); + if (tabController.index == 1) { + await loadClasses(force: true); + } } @override @@ -151,17 +171,38 @@ class CourseUnitDetailPageViewState } Widget _courseUnitClassesView(BuildContext context) { - final classes = - ref.read(courseUnitsInfoProvider.notifier).courseUnitsClasses[widget - .courseUnit]; + return Consumer( + builder: (context, ref, _) { + ref.watch(courseUnitsInfoProvider); + final provider = ref.read(courseUnitsInfoProvider.notifier); - if (classes == null || classes.isEmpty) { - return Center( - child: Text(S.of(context).no_class, textAlign: TextAlign.center), - ); - } + final classes = provider.courseUnitsClasses[widget.courseUnit]; + final sheet = provider.courseUnitsSheets[widget.courseUnit]; + final classProfessors = + provider.courseUnitsClassProfessors[widget.courseUnit]; + + if (classes == null) { + return const Center(child: CircularProgressIndicator()); + } + + if (classes.isEmpty) { + return Center( + child: Text(S.of(context).no_class, textAlign: TextAlign.center), + ); + } + + if (classProfessors == null) { + return const Center(child: CircularProgressIndicator()); + } - return CourseUnitClassesView(classes); + return CourseUnitClassesView( + classes, + sheet?.professors ?? [], + widget.courseUnit, + classProfessors: classProfessors, + ); + }, + ); } @override diff --git a/packages/uni_app/lib/view/course_unit_info/widgets/course_unit_classes.dart b/packages/uni_app/lib/view/course_unit_info/widgets/course_unit_classes.dart index 96cc3e101..37419787d 100644 --- a/packages/uni_app/lib/view/course_unit_info/widgets/course_unit_classes.dart +++ b/packages/uni_app/lib/view/course_unit_info/widgets/course_unit_classes.dart @@ -1,15 +1,32 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:uni/generated/l10n.dart'; +import 'package:uni/model/entities/course_units/course_unit.dart'; import 'package:uni/model/entities/course_units/course_unit_class.dart'; +import 'package:uni/model/entities/course_units/sheet.dart'; +import 'package:uni/model/providers/riverpod/profile_provider.dart'; import 'package:uni/model/providers/riverpod/session_provider.dart'; import 'package:uni/session/flows/base/session.dart'; import 'package:uni/utils/student_number_getter.dart'; import 'package:uni/view/course_unit_info/widgets/course_unit_student_tile.dart'; +import 'package:uni/view/course_unit_info/widgets/modal_professor_info.dart'; +import 'package:uni_ui/cards/instructor_card.dart'; class CourseUnitClassesView extends ConsumerStatefulWidget { - const CourseUnitClassesView(this.classes, {super.key}); + const CourseUnitClassesView( + this.classes, + this.professors, + this.courseUnit, { + this.classProfessors = const >{}, + super.key, + }); final List classes; + final List professors; + final CourseUnit courseUnit; + final Map> classProfessors; @override ConsumerState createState() => @@ -96,6 +113,7 @@ class _CourseUnitClassesViewState extends ConsumerState { child: Column( children: [ _buildClassSelector(studentNumber), + _buildClassProfessor(), _buildStudentList(session), ], ), @@ -181,6 +199,73 @@ class _CourseUnitClassesViewState extends ConsumerState { ); } + Widget _buildClassProfessor() { + final currentClass = widget.classes[selectedIndex!]; + final professors = widget.classProfessors[currentClass.className]; + + if (professors == null || professors.isEmpty) { + return const SizedBox.shrink(); + } + + final session = ref.read(sessionProvider).value!; + + return Padding( + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: + professors.map((professor) { + return GestureDetector( + onTap: () { + showDialog( + context: context, + builder: (context) => ProfessorInfoModal(professor), + ); + }, + + child: Container( + decoration: ShapeDecoration( + color: Theme.of(context).colorScheme.secondary, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + shadows: [ + BoxShadow( + color: Theme.of( + context, + ).colorScheme.shadow.withAlpha(0x25), + blurRadius: 2, + ), + ], + ), + child: FutureBuilder( + future: ProfileNotifier.fetchOrGetCachedProfilePicture( + session, + studentNumber: int.parse(professor.code), + ), + builder: (context, snapshot) { + final profileImage = + snapshot.hasData && snapshot.data != null + ? FileImage(snapshot.data!) + : null; + + return InstructorCard( + name: professor.name, + isRegent: professor.isRegent, + instructorLabel: S.of(context).classProfessor, + regentLabel: S.of(context).classProfessor, + profileImage: profileImage, + ); + }, + ), + ), + ); + }).toList(), + ), + ); + } + Widget _buildStudentList(Session session) { final currentClass = widget.classes[selectedIndex!];