diff --git a/lib/i18n/en-US.i18n.yaml b/lib/i18n/en-US.i18n.yaml index 0f6192c8..64e60926 100644 --- a/lib/i18n/en-US.i18n.yaml +++ b/lib/i18n/en-US.i18n.yaml @@ -48,6 +48,27 @@ nav: courseTable: Courses scores: Scores profile: Me +score: + loadFailed: Failed to load scores + refreshSuccess: Scores updated + refreshFailed: Failed to refresh scores + noRecords: No score records found + noScoresThisSemester: No scores for this semester + courseNumber: "No: ${number} Code: ${code}" + none: N/A + summary: + cumulativeGpa: Cumulative GPA + conduct: Conduct + semesterAverage: Semester Avg + creditsPassed: Credits Passed + totalCredits: Total Credits + status: + notEntered: Not entered + withdraw: Withdrawn + undelivered: Not submitted + pass: Pass + fail: Fail + creditTransfer: Credit transfer courseTable: notFound: Course table not found dayOfWeek(map): diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 5b7c3547..dc2e6f88 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 202 (101 per locale) +/// Strings: 238 (119 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en_US.g.dart b/lib/i18n/strings_en_US.g.dart index 030e9534..ded83327 100644 --- a/lib/i18n/strings_en_US.g.dart +++ b/lib/i18n/strings_en_US.g.dart @@ -44,6 +44,7 @@ class TranslationsEnUs extends Translations with BaseTranslations 'Me'; } +// Path: score +class _TranslationsScoreEnUs extends TranslationsScoreZhTw { + _TranslationsScoreEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root); + + final TranslationsEnUs _root; // ignore: unused_field + + // Translations + @override String get loadFailed => 'Failed to load scores'; + @override String get refreshSuccess => 'Scores updated'; + @override String get refreshFailed => 'Failed to refresh scores'; + @override String get noRecords => 'No score records found'; + @override String get noScoresThisSemester => 'No scores for this semester'; + @override String courseNumber({required Object number, required Object code}) => 'No: ${number} Code: ${code}'; + @override String get none => 'N/A'; + @override late final _TranslationsScoreSummaryEnUs summary = _TranslationsScoreSummaryEnUs._(_root); + @override late final _TranslationsScoreStatusEnUs status = _TranslationsScoreStatusEnUs._(_root); +} + // Path: courseTable class _TranslationsCourseTableEnUs extends TranslationsCourseTableZhTw { _TranslationsCourseTableEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root); @@ -223,6 +242,35 @@ class _TranslationsLoginErrorsEnUs extends TranslationsLoginErrorsZhTw { @override String get mobileVerificationRequired => 'Mobile phone verification is required. Please complete it on the NTUT portal.'; } +// Path: score.summary +class _TranslationsScoreSummaryEnUs extends TranslationsScoreSummaryZhTw { + _TranslationsScoreSummaryEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root); + + final TranslationsEnUs _root; // ignore: unused_field + + // Translations + @override String get cumulativeGpa => 'Cumulative GPA'; + @override String get conduct => 'Conduct'; + @override String get semesterAverage => 'Semester Avg'; + @override String get creditsPassed => 'Credits Passed'; + @override String get totalCredits => 'Total Credits'; +} + +// Path: score.status +class _TranslationsScoreStatusEnUs extends TranslationsScoreStatusZhTw { + _TranslationsScoreStatusEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root); + + final TranslationsEnUs _root; // ignore: unused_field + + // Translations + @override String get notEntered => 'Not entered'; + @override String get withdraw => 'Withdrawn'; + @override String get undelivered => 'Not submitted'; + @override String get pass => 'Pass'; + @override String get fail => 'Fail'; + @override String get creditTransfer => 'Credit transfer'; +} + // Path: profile.sections class _TranslationsProfileSectionsEnUs extends TranslationsProfileSectionsZhTw { _TranslationsProfileSectionsEnUs._(TranslationsEnUs root) : this._root = root, super.internal(root); @@ -392,6 +440,24 @@ extension on TranslationsEnUs { 'nav.courseTable' => 'Courses', 'nav.scores' => 'Scores', 'nav.profile' => 'Me', + 'score.loadFailed' => 'Failed to load scores', + 'score.refreshSuccess' => 'Scores updated', + 'score.refreshFailed' => 'Failed to refresh scores', + 'score.noRecords' => 'No score records found', + 'score.noScoresThisSemester' => 'No scores for this semester', + 'score.courseNumber' => ({required Object number, required Object code}) => 'No: ${number} Code: ${code}', + 'score.none' => 'N/A', + 'score.summary.cumulativeGpa' => 'Cumulative GPA', + 'score.summary.conduct' => 'Conduct', + 'score.summary.semesterAverage' => 'Semester Avg', + 'score.summary.creditsPassed' => 'Credits Passed', + 'score.summary.totalCredits' => 'Total Credits', + 'score.status.notEntered' => 'Not entered', + 'score.status.withdraw' => 'Withdrawn', + 'score.status.undelivered' => 'Not submitted', + 'score.status.pass' => 'Pass', + 'score.status.fail' => 'Fail', + 'score.status.creditTransfer' => 'Credit transfer', 'courseTable.notFound' => 'Course table not found', 'courseTable.dayOfWeek.sunday' => 'Sun', 'courseTable.dayOfWeek.monday' => 'Mon', diff --git a/lib/i18n/strings_zh_TW.g.dart b/lib/i18n/strings_zh_TW.g.dart index b2a7abab..a1cfd823 100644 --- a/lib/i18n/strings_zh_TW.g.dart +++ b/lib/i18n/strings_zh_TW.g.dart @@ -45,6 +45,7 @@ class Translations with BaseTranslations { late final TranslationsIntroZhTw intro = TranslationsIntroZhTw.internal(_root); late final TranslationsLoginZhTw login = TranslationsLoginZhTw.internal(_root); late final TranslationsNavZhTw nav = TranslationsNavZhTw.internal(_root); + late final TranslationsScoreZhTw score = TranslationsScoreZhTw.internal(_root); late final TranslationsCourseTableZhTw courseTable = TranslationsCourseTableZhTw.internal(_root); late final TranslationsProfileZhTw profile = TranslationsProfileZhTw.internal(_root); late final TranslationsEnrollmentStatusZhTw enrollmentStatus = TranslationsEnrollmentStatusZhTw.internal(_root); @@ -187,6 +188,39 @@ class TranslationsNavZhTw { String get profile => '我'; } +// Path: score +class TranslationsScoreZhTw { + TranslationsScoreZhTw.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// zh-TW: '成績載入失敗' + String get loadFailed => '成績載入失敗'; + + /// zh-TW: '成績資料已更新' + String get refreshSuccess => '成績資料已更新'; + + /// zh-TW: '成績更新失敗' + String get refreshFailed => '成績更新失敗'; + + /// zh-TW: '目前沒有任何成績紀錄' + String get noRecords => '目前沒有任何成績紀錄'; + + /// zh-TW: '本學期尚無成績' + String get noScoresThisSemester => '本學期尚無成績'; + + /// zh-TW: '課號: ${number} 編碼: ${code}' + String courseNumber({required Object number, required Object code}) => '課號: ${number} 編碼: ${code}'; + + /// zh-TW: '無' + String get none => '無'; + + late final TranslationsScoreSummaryZhTw summary = TranslationsScoreSummaryZhTw.internal(_root); + late final TranslationsScoreStatusZhTw status = TranslationsScoreStatusZhTw.internal(_root); +} + // Path: courseTable class TranslationsCourseTableZhTw { TranslationsCourseTableZhTw.internal(this._root); @@ -322,6 +356,57 @@ class TranslationsLoginErrorsZhTw { String get mobileVerificationRequired => '需要進行手機驗證,請至校園入口網站完成驗證'; } +// Path: score.summary +class TranslationsScoreSummaryZhTw { + TranslationsScoreSummaryZhTw.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// zh-TW: '歷年GPA' + String get cumulativeGpa => '歷年GPA'; + + /// zh-TW: '操行成績' + String get conduct => '操行成績'; + + /// zh-TW: '學期平均' + String get semesterAverage => '學期平均'; + + /// zh-TW: '實得學分' + String get creditsPassed => '實得學分'; + + /// zh-TW: '修課總學分' + String get totalCredits => '修課總學分'; +} + +// Path: score.status +class TranslationsScoreStatusZhTw { + TranslationsScoreStatusZhTw.internal(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// zh-TW: '未輸入' + String get notEntered => '未輸入'; + + /// zh-TW: '撤選' + String get withdraw => '撤選'; + + /// zh-TW: '未送成績' + String get undelivered => '未送成績'; + + /// zh-TW: '通過' + String get pass => '通過'; + + /// zh-TW: '不通過' + String get fail => '不通過'; + + /// zh-TW: '抵免' + String get creditTransfer => '抵免'; +} + // Path: profile.sections class TranslationsProfileSectionsZhTw { TranslationsProfileSectionsZhTw.internal(this._root); @@ -571,6 +656,24 @@ extension on Translations { 'nav.courseTable' => '課表', 'nav.scores' => '成績', 'nav.profile' => '我', + 'score.loadFailed' => '成績載入失敗', + 'score.refreshSuccess' => '成績資料已更新', + 'score.refreshFailed' => '成績更新失敗', + 'score.noRecords' => '目前沒有任何成績紀錄', + 'score.noScoresThisSemester' => '本學期尚無成績', + 'score.courseNumber' => ({required Object number, required Object code}) => '課號: ${number} 編碼: ${code}', + 'score.none' => '無', + 'score.summary.cumulativeGpa' => '歷年GPA', + 'score.summary.conduct' => '操行成績', + 'score.summary.semesterAverage' => '學期平均', + 'score.summary.creditsPassed' => '實得學分', + 'score.summary.totalCredits' => '修課總學分', + 'score.status.notEntered' => '未輸入', + 'score.status.withdraw' => '撤選', + 'score.status.undelivered' => '未送成績', + 'score.status.pass' => '通過', + 'score.status.fail' => '不通過', + 'score.status.creditTransfer' => '抵免', 'courseTable.notFound' => '找不到課表', 'courseTable.dayOfWeek.sunday' => '日', 'courseTable.dayOfWeek.monday' => '一', diff --git a/lib/i18n/zh-TW.i18n.yaml b/lib/i18n/zh-TW.i18n.yaml index d1993192..ebcdf8a8 100644 --- a/lib/i18n/zh-TW.i18n.yaml +++ b/lib/i18n/zh-TW.i18n.yaml @@ -48,6 +48,27 @@ nav: courseTable: 課表 scores: 成績 profile: 我 +score: + loadFailed: 成績載入失敗 + refreshSuccess: 成績資料已更新 + refreshFailed: 成績更新失敗 + noRecords: 目前沒有任何成績紀錄 + noScoresThisSemester: 本學期尚無成績 + courseNumber: "課號: ${number} 編碼: ${code}" + none: 無 + summary: + cumulativeGpa: 歷年GPA + conduct: 操行成績 + semesterAverage: 學期平均 + creditsPassed: 實得學分 + totalCredits: 修課總學分 + status: + notEntered: 未輸入 + withdraw: 撤選 + undelivered: 未送成績 + pass: 通過 + fail: 不通過 + creditTransfer: 抵免 courseTable: notFound: 找不到課表 dayOfWeek(map): diff --git a/lib/screens/main/score/score_providers.dart b/lib/screens/main/score/score_providers.dart new file mode 100644 index 00000000..ae31a691 --- /dev/null +++ b/lib/screens/main/score/score_providers.dart @@ -0,0 +1,13 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tattoo/repositories/auth_repository.dart'; +import 'package:tattoo/repositories/student_repository.dart'; + +/// Provides semester records (scores, GPA, rankings) for the score screen. +final semesterRecordsProvider = + FutureProvider.autoDispose>((ref) async { + try { + return await ref.watch(studentRepositoryProvider).getSemesterRecords(); + } on NotLoggedInException { + return []; + } + }); diff --git a/lib/screens/main/score/score_screen.dart b/lib/screens/main/score/score_screen.dart index 8eb00129..ec9b0cf3 100644 --- a/lib/screens/main/score/score_screen.dart +++ b/lib/screens/main/score/score_screen.dart @@ -1,14 +1,389 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tattoo/components/chip_tab_switcher.dart'; +import 'package:tattoo/database/database.dart'; import 'package:tattoo/i18n/strings.g.dart'; +import 'package:tattoo/repositories/student_repository.dart'; +import 'package:tattoo/screens/main/score/score_providers.dart'; +import 'package:tattoo/screens/main/score/score_screen_actions.dart'; +import 'package:tattoo/screens/main/score/score_view_helpers.dart'; -class ScoreScreen extends StatelessWidget { +class ScoreScreen extends ConsumerStatefulWidget { const ScoreScreen({super.key}); + @override + ConsumerState createState() => _ScoreScreenState(); +} + +class _ScoreScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + int _selectedIndex = 0; + String? _selectedSemesterKey; + TabController? _semesterTabController; + int _semesterTabLength = 0; + + @override + void dispose() { + _semesterTabController?.removeListener(_handleSemesterTabChanged); + _semesterTabController?.dispose(); + super.dispose(); + } + + void _dismissRefreshSnackBar() { + if (!mounted) return; + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + } + + bool _handleScrollNotification(ScrollNotification notification) { + if (notification is ScrollStartNotification || + notification is UserScrollNotification) { + _dismissRefreshSnackBar(); + } + return false; + } + + int _findPreferredSemesterIndex(List records) { + if (_selectedSemesterKey == null) return -1; + return records.indexWhere( + (record) => semesterKey(record) == _selectedSemesterKey, + ); + } + + int _findDefaultSemesterIndex(List records) { + final index = records.indexWhere((record) => record.scores.isNotEmpty); + return index >= 0 ? index : 0; + } + + void _handleSemesterTabChanged() { + final controller = _semesterTabController; + if (controller == null || controller.indexIsChanging || !mounted) return; + + if (_selectedIndex == controller.index) return; + + setState(() { + _dismissRefreshSnackBar(); + _selectedIndex = controller.index; + }); + } + + void _syncSemesterTabController(List records) { + if (records.isEmpty) { + _semesterTabController?.removeListener(_handleSemesterTabChanged); + _semesterTabController?.dispose(); + _semesterTabController = null; + _semesterTabLength = 0; + _selectedIndex = 0; + _selectedSemesterKey = null; + return; + } + + final preferredIndex = _findPreferredSemesterIndex(records); + final initialIndex = preferredIndex >= 0 + ? preferredIndex + : _findDefaultSemesterIndex(records); + + if (_semesterTabController == null || + _semesterTabLength != records.length) { + _semesterTabController?.removeListener(_handleSemesterTabChanged); + _semesterTabController?.dispose(); + _semesterTabController = TabController( + length: records.length, + initialIndex: initialIndex, + vsync: this, + )..addListener(_handleSemesterTabChanged); + _semesterTabLength = records.length; + _selectedIndex = initialIndex; + return; + } + + final targetIndex = _selectedIndex >= records.length + ? initialIndex + : _selectedIndex; + if (_semesterTabController!.index != targetIndex) { + _semesterTabController!.index = targetIndex; + } + _selectedIndex = targetIndex; + } + + Future _reloadScores() async { + try { + await refreshSemesterRecords(ref); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.score.refreshSuccess)), + ); + } catch (_) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.score.refreshFailed)), + ); + } + } + @override Widget build(BuildContext context) { + final recordsAsync = ref.watch(semesterRecordsProvider); + return Scaffold( - appBar: AppBar(title: Text(t.nav.scores)), - body: Center(child: Text(t.nav.scores)), + body: recordsAsync.when( + loading: () => CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(t.nav.scores), + centerTitle: true, + ), + const SliverFillRemaining( + hasScrollBody: false, + child: Center(child: CircularProgressIndicator()), + ), + ], + ), + error: (err, stack) => CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(t.nav.scores), + centerTitle: true, + ), + SliverFillRemaining( + hasScrollBody: false, + child: Center(child: Text('${t.score.loadFailed}\n$err')), + ), + ], + ), + data: (records) { + _syncSemesterTabController(records); + + final hasRecords = records.isNotEmpty; + if (hasRecords) { + final activeIndex = _semesterTabController?.index ?? _selectedIndex; + _selectedIndex = activeIndex >= records.length ? 0 : activeIndex; + _selectedSemesterKey = semesterKey(records[_selectedIndex]); + } + + return NotificationListener( + onNotification: _handleScrollNotification, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverAppBar( + pinned: true, + title: Text(t.nav.scores), + centerTitle: true, + bottom: _semesterTabController != null && records.isNotEmpty + ? PreferredSize( + preferredSize: const Size.fromHeight(52), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: SizedBox( + height: 40, + child: ChipTabSwitcher( + tabs: [ + for (final record in records) + '${record.summary.year}-${record.summary.term}', + ], + controller: _semesterTabController, + padding: EdgeInsets.zero, + spacing: 6, + ), + ), + ), + ) + : null, + ), + if (!hasRecords) + SliverFillRemaining( + hasScrollBody: false, + child: Center(child: Text(t.score.noRecords)), + ) + else ...[ + SliverFillRemaining( + child: TabBarView( + controller: _semesterTabController, + children: [ + for (final record in records) + _SemesterScoreList( + record: record, + onRefresh: _reloadScores, + ), + ], + ), + ), + ], + ], + ), + ); + }, + ), + ); + } +} + +class _SemesterScoreList extends StatelessWidget { + final SemesterRecordData record; + final Future Function() onRefresh; + + const _SemesterScoreList({ + required this.record, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + return RefreshIndicator( + onRefresh: onRefresh, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(top: 8, bottom: 12), + itemCount: record.scores.length + 2, + itemBuilder: (context, index) { + if (index == 0) { + return _SemesterSummaryCard(summary: record.summary); + } + if (index == 1) { + if (record.scores.isNotEmpty) return const SizedBox(height: 8); + return Padding( + padding: const EdgeInsets.only(top: 12), + child: Center(child: Text(t.score.noScoresThisSemester)), + ); + } + + final scoreIndex = index - 2; + final score = record.scores[scoreIndex]; + return Column( + children: [ + _ScoreTile(score: score), + if (scoreIndex != record.scores.length - 1) + const Divider(height: 1, indent: 16), + ], + ); + }, + ), + ); + } +} + +class _SemesterSummaryCard extends StatelessWidget { + final UserAcademicSummary summary; + + const _SemesterSummaryCard({required this.summary}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 0), + child: DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + primary: false, + physics: const ClampingScrollPhysics(), + child: Row( + children: [ + _buildStat( + context, + t.score.summary.cumulativeGpa, + _formatDouble(summary.gpa), + ), + const SizedBox(width: 24), + _buildStat( + context, + t.score.summary.conduct, + summary.conduct?.toString() ?? '-', + ), + const SizedBox(width: 24), + _buildStat( + context, + t.score.summary.semesterAverage, + summary.average?.toString() ?? '-', + ), + const SizedBox(width: 24), + _buildStat( + context, + t.score.summary.creditsPassed, + summary.creditsPassed?.toString() ?? '-', + ), + const SizedBox(width: 24), + _buildStat( + context, + t.score.summary.totalCredits, + summary.totalCredits?.toString() ?? '-', + ), + ], + ), + ), + ), + ), + ); + } + + String _formatDouble(double? value) { + if (value == null) return '-'; + return value.toStringAsFixed(2); + } + + Widget _buildStat(BuildContext context, String label, String value) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + value, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + Text(label, style: Theme.of(context).textTheme.labelMedium), + ], + ); + } +} + +class _ScoreTile extends StatelessWidget { + final ScoreDetail score; + + const _ScoreTile({required this.score}); + + @override + Widget build(BuildContext context) { + final scoreColor = getScoreColor(context, score); + + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4), + title: Text( + score.nameZh, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + subtitle: Text( + t.score.courseNumber( + number: score.number ?? t.score.none, + code: score.code, + ), + ), + trailing: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: scoreColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + score.score?.toString() ?? getScoreStatusText(score.status), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), + ), ); } } diff --git a/lib/screens/main/score/score_screen_actions.dart b/lib/screens/main/score/score_screen_actions.dart new file mode 100644 index 00000000..7b9714bb --- /dev/null +++ b/lib/screens/main/score/score_screen_actions.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:tattoo/repositories/student_repository.dart'; +import 'package:tattoo/screens/main/score/score_providers.dart'; + +Future> refreshSemesterRecords(WidgetRef ref) async { + final records = await ref + .read(studentRepositoryProvider) + .getSemesterRecords(refresh: true); + ref.invalidate(semesterRecordsProvider); + return records; +} diff --git a/lib/screens/main/score/score_view_helpers.dart b/lib/screens/main/score/score_view_helpers.dart new file mode 100644 index 00000000..cbb28534 --- /dev/null +++ b/lib/screens/main/score/score_view_helpers.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:tattoo/database/database.dart'; +import 'package:tattoo/i18n/strings.g.dart'; +import 'package:tattoo/models/score.dart'; +import 'package:tattoo/repositories/student_repository.dart'; + +String semesterKey(SemesterRecordData record) { + return '${record.summary.year}-${record.summary.term}'; +} + +String formatLastUpdated(DateTime dateTime) { + String twoDigits(int value) => value.toString().padLeft(2, '0'); + final year = dateTime.year; + final month = twoDigits(dateTime.month); + final day = twoDigits(dateTime.day); + final hour = twoDigits(dateTime.hour); + final minute = twoDigits(dateTime.minute); + return '$year/$month/$day $hour:$minute'; +} + +Color getScoreColor(BuildContext context, ScoreDetail score) { + if (score.score != null) { + return score.score! >= 60 + ? Colors.green.shade600 + : Theme.of(context).colorScheme.error; + } + if (score.status == ScoreStatus.pass || + score.status == ScoreStatus.creditTransfer) { + return Colors.green.shade600; + } + return Theme.of(context).colorScheme.onSurfaceVariant; +} + +String getScoreStatusText(ScoreStatus? status) { + return switch (status) { + ScoreStatus.notEntered => t.score.status.notEntered, + ScoreStatus.withdraw => t.score.status.withdraw, + ScoreStatus.undelivered => t.score.status.undelivered, + ScoreStatus.pass => t.score.status.pass, + ScoreStatus.fail => t.score.status.fail, + ScoreStatus.creditTransfer => t.score.status.creditTransfer, + _ => '-', + }; +}