// ignore_for_file: no_leading_underscores_for_local_identifiers import 'dart:math'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:collection/collection.dart'; import 'package:refilc/api/providers/update_provider.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/ui/widgets/grade/grade_tile.dart'; import 'package:refilc_kreta_api/models/exam.dart'; import 'package:refilc_kreta_api/providers/exam_provider.dart'; // import 'package:refilc_kreta_api/client/api.dart'; // import 'package:refilc_kreta_api/client/client.dart'; import 'package:refilc_kreta_api/providers/grade_provider.dart'; import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_kreta_api/models/grade.dart'; import 'package:refilc_kreta_api/models/subject.dart'; import 'package:refilc_kreta_api/models/group_average.dart'; import 'package:refilc_kreta_api/providers/homework_provider.dart'; import 'package:refilc_mobile_ui/common/average_display.dart'; import 'package:refilc_mobile_ui/common/bottom_sheet_menu/rounded_bottom_sheet.dart'; import 'package:refilc_mobile_ui/common/empty.dart'; import 'package:refilc_mobile_ui/common/panel/panel.dart'; import 'package:refilc_mobile_ui/common/profile_image/profile_button.dart'; import 'package:refilc_mobile_ui/common/profile_image/profile_image.dart'; import 'package:refilc_mobile_ui/common/widgets/exam/exam_viewable.dart'; import 'package:refilc_mobile_ui/common/widgets/statistics_tile.dart'; import 'package:refilc_mobile_ui/common/widgets/grade/grade_subject_tile.dart'; import 'package:refilc_mobile_ui/common/trend_display.dart'; import 'package:refilc_mobile_ui/pages/grades/fail_warning.dart'; import 'package:refilc_mobile_ui/pages/grades/grades_count.dart'; import 'package:refilc_mobile_ui/pages/grades/graph.dart'; import 'package:refilc_mobile_ui/pages/grades/grade_subject_view.dart'; import 'package:refilc_plus/models/premium_scopes.dart'; import 'package:refilc_plus/providers/plus_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'package:refilc/helpers/average_helper.dart'; import 'package:refilc_plus/ui/mobile/plus/upsell.dart'; import 'average_selector.dart'; import 'package:refilc_plus/ui/mobile/plus/premium_inline.dart'; import 'calculator/grade_calculator.dart'; import 'calculator/grade_calculator_provider.dart'; import 'grades_page.i18n.dart'; class GradesPage extends StatefulWidget { const GradesPage({super.key}); @override GradesPageState createState() => GradesPageState(); } class GradesPageState extends State { final GlobalKey _scaffoldKey = GlobalKey(); PersistentBottomSheetController? _sheetController; late UserProvider user; late GradeProvider gradeProvider; late UpdateProvider updateProvider; late GradeCalculatorProvider calculatorProvider; late HomeworkProvider homeworkProvider; late ExamProvider examProvider; late String firstName; late Widget yearlyGraph; late Widget gradesCount; List subjectTiles = []; int avgDropValue = 0; bool gradeCalcMode = false; List getSubjectGrades(GradeSubject subject, {int days = 0}) => !gradeCalcMode ? gradeProvider .grades .where((e) => e .subject == subject && e.type == GradeType.midYear && (days == 0 || e.date.isBefore( DateTime.now().subtract(Duration(days: days))))) .toList() : calculatorProvider.grades .where((e) => e.subject == subject) .toList(); void generateTiles() { List subjects = gradeProvider.grades .map((e) => GradeSubject( category: e.subject.category, id: e.subject.id, name: e.subject.name, renamedTo: e.subject.renamedTo, customRounding: e.subject.customRounding, teacher: e.teacher, )) .toSet() .toList() ..sort((a, b) => a.name.compareTo(b.name)); List tiles = []; Map subjectAvgs = {}; if (!gradeCalcMode) { var i = 0; tiles.addAll(subjects.map((subject) { List subjectGrades = getSubjectGrades(subject); double avg = AverageHelper.averageEvals(subjectGrades); double averageBefore = 0.0; if (avgDropValue != 0) { List gradesBefore = getSubjectGrades(subject, days: avgDropValue); averageBefore = avgDropValue == 0 ? 0.0 : AverageHelper.averageEvals(gradesBefore); } var nullavg = GroupAverage(average: 0.0, subject: subject, uid: "0"); double groupAverage = gradeProvider.groupAverages .firstWhere((e) => e.subject == subject, orElse: () => nullavg) .average; if (avg != 0) subjectAvgs[subject] = avg; i++; int homeworkCount = homeworkProvider.homework .where((e) => e.subject.id == subject.id && e.deadline.isAfter(DateTime.now())) .length; bool hasHomework = homeworkCount > 0; List allExams = examProvider.exams; allExams.sort((a, b) => a.date.compareTo(b.date)); Exam? nearestExam = allExams.firstWhereOrNull((e) => e.subject.id == subject.id && e.writeDate.isAfter(DateTime.now())); bool hasUnder = hasHomework || nearestExam != null; return Padding( padding: i > 1 ? const EdgeInsets.only(top: 9.0) : EdgeInsets.zero, child: Column( children: [ Container( decoration: BoxDecoration( boxShadow: [ if (Provider.of(context, listen: false) .shadowEffect) BoxShadow( offset: const Offset(0, 21), blurRadius: 23.0, color: Theme.of(context).shadowColor, ) ], borderRadius: BorderRadius.only( topLeft: const Radius.circular(16.0), topRight: const Radius.circular(16.0), bottomLeft: hasUnder ? const Radius.circular(8.0) : const Radius.circular(16.0), bottomRight: hasUnder ? const Radius.circular(8.0) : const Radius.circular(16.0), ), color: Theme.of(context).colorScheme.background, ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8.0, horizontal: 6.0), child: Theme( data: Theme.of(context).copyWith( highlightColor: Colors.transparent, splashColor: Colors.transparent, ), child: GradeSubjectTile( subject, averageBefore: averageBefore, average: avg, groupAverage: avgDropValue == 0 ? groupAverage : 0.0, onTap: () { GradeSubjectView(subject, groupAverage: groupAverage) .push(context, root: true); }, ), ), ), ), if (hasUnder) const SizedBox( height: 6.0, ), if (hasHomework) Container( decoration: BoxDecoration( boxShadow: [ if (Provider.of(context, listen: false) .shadowEffect) BoxShadow( offset: const Offset(0, 21), blurRadius: 23.0, color: Theme.of(context).shadowColor, ) ], borderRadius: BorderRadius.only( topLeft: const Radius.circular(8.0), topRight: const Radius.circular(8.0), bottomLeft: nearestExam != null ? const Radius.circular(8.0) : const Radius.circular(16.0), bottomRight: nearestExam != null ? const Radius.circular(8.0) : const Radius.circular(16.0), ), color: Theme.of(context).colorScheme.background, ), child: Padding( padding: const EdgeInsets.only( top: 8.0, bottom: 8.0, left: 15.0, right: 8.0, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'you_have_hw'.i18n.fill([homeworkCount]), style: const TextStyle( fontSize: 15.0, fontWeight: FontWeight.w500), ), // const Icon( // FeatherIcons.chevronRight, // grade: 0.5, // size: 20.0, // ) ], ), ), ), if (nearestExam != null) Container( decoration: BoxDecoration( boxShadow: [ if (Provider.of(context, listen: false) .shadowEffect) BoxShadow( offset: const Offset(0, 21), blurRadius: 23.0, color: Theme.of(context).shadowColor, ) ], borderRadius: const BorderRadius.only( topLeft: Radius.circular(8.0), topRight: Radius.circular(8.0), bottomLeft: Radius.circular(16.0), bottomRight: Radius.circular(16.0), ), color: Theme.of(context).colorScheme.background, ), child: ExamViewable( nearestExam, showSubject: false, tilePadding: const EdgeInsets.symmetric(horizontal: 6.0), ), ), ], ), ); })); } else { tiles.clear(); List ghostGrades = calculatorProvider.ghosts; ghostGrades.sort((a, b) => -a.date.compareTo(b.date)); List _gradeTiles = []; for (Grade grade in ghostGrades) { _gradeTiles.add(GradeTile( grade, viewOverride: true, )); } tiles.add( _gradeTiles.isNotEmpty ? Panel( key: ValueKey(gradeCalcMode), title: Text( "Ghost Grades".i18n, ), child: Column( children: _gradeTiles, ), ) : const SizedBox(), ); } if (tiles.isNotEmpty || gradeCalcMode) { tiles.insert(0, yearlyGraph); tiles.insert(1, gradesCount); if (!gradeCalcMode) { tiles.insert(2, FailWarning(subjectAvgs: subjectAvgs)); tiles.insert( 3, PanelTitle( title: Text( avgDropValue == 0 ? "Subjects".i18n : "Subjects_changes".i18n, )), ); // tiles.insert(4, const PanelHeader(padding: EdgeInsets.only(top: 12.0))); // tiles.add(const PanelFooter(padding: EdgeInsets.only(bottom: 12.0))); } tiles.add(Padding( padding: EdgeInsets.only(bottom: !gradeCalcMode ? 24.0 : 250.0), )); } else { tiles.insert( 0, Padding( padding: const EdgeInsets.only(top: 24.0), child: Empty(subtitle: "empty".i18n), ), ); } double subjectAvg = subjectAvgs.isNotEmpty ? subjectAvgs.values.fold(0.0, (double a, double b) => a + b) / subjectAvgs.length : 0.0; final double classAvg = gradeProvider.groupAverages.isNotEmpty ? gradeProvider.groupAverages .map((e) => e.average) .fold(0.0, (double a, double b) => a + b) / gradeProvider.groupAverages.length : 0.0; if (subjectAvg > 0 && !gradeCalcMode) { tiles.add( PanelTitle(title: Text("data".i18n)), ); tiles.add(Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: StatisticsTile( fill: true, title: AutoSizeText( "subjectavg".i18n, textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), value: subjectAvg, ), ), const SizedBox(width: 24.0), Expanded( child: StatisticsTile( outline: true, title: AutoSizeText( "classavg".i18n, textAlign: TextAlign.center, maxLines: 1, wrapWords: false, overflow: TextOverflow.ellipsis, ), value: classAvg, ), ), ], )); } tiles.add(Provider.of(context, listen: false).hasPremium ? const SizedBox() : const Padding( padding: EdgeInsets.only(top: 24.0), child: PremiumInline(features: [ PremiumInlineFeature.goal, PremiumInlineFeature.stats, ]), )); // padding tiles.add(const SizedBox(height: 32.0)); subjectTiles = List.castFrom(tiles); } @override Widget build(BuildContext context) { user = Provider.of(context); gradeProvider = Provider.of(context); updateProvider = Provider.of(context); calculatorProvider = Provider.of(context); homeworkProvider = Provider.of(context); examProvider = Provider.of(context); context.watch(); List nameParts = user.displayName?.split(" ") ?? ["?"]; firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0]; final double totalClassAvg = gradeProvider.groupAverages.isEmpty ? 0.0 : gradeProvider.groupAverages .map((e) => e.average) .fold(0.0, (double a, double b) => a + b) / gradeProvider.groupAverages.length; final now = gradeProvider.grades.isNotEmpty ? gradeProvider.grades .reduce((v, e) => e.date.isAfter(v.date) ? e : v) .date : DateTime.now(); final currentStudentAvg = AverageHelper.averageEvals(!gradeCalcMode ? gradeProvider.grades .where((e) => e.type == GradeType.midYear) .toList() : calculatorProvider.grades); final prevStudentAvg = AverageHelper.averageEvals(gradeProvider.grades .where((e) => e.type == GradeType.midYear) .where((e) => e.date.isBefore(now.subtract(const Duration(days: 30)))) .toList()); List graphGrades = !gradeCalcMode ? gradeProvider.grades .where((e) => e.type == GradeType.midYear && (avgDropValue == 0 || e.date.isAfter( DateTime.now().subtract(Duration(days: avgDropValue))))) .toList() : calculatorProvider.grades .where(((e) => avgDropValue == 0 || e.date.isAfter( DateTime.now().subtract(Duration(days: avgDropValue))))) .toList(); yearlyGraph = Padding( padding: const EdgeInsets.only(top: 12.0, bottom: 8.0), child: Panel( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ AverageSelector( value: avgDropValue, onChanged: (value) { setState(() { avgDropValue = value!; }); }, ), Row( children: [ // if (totalClassAvg >= 1.0) AverageDisplay(average: totalClassAvg, border: true), // const SizedBox(width: 4.0), TrendDisplay( previous: prevStudentAvg, current: currentStudentAvg), if (gradeProvider.grades .where((e) => e.type == GradeType.midYear) .isNotEmpty) AverageDisplay(average: currentStudentAvg), ], ) ], ), child: Container( padding: const EdgeInsets.only(top: 12.0, right: 12.0), child: GradeGraph(graphGrades, dayThreshold: 2, classAvg: totalClassAvg), ), ), ); gradesCount = Padding( padding: const EdgeInsets.only(bottom: 24.0), child: Panel(child: GradesCount(grades: graphGrades)), ); generateTiles(); return Scaffold( key: _scaffoldKey, body: Padding( padding: const EdgeInsets.only(top: 9.0), child: NestedScrollView( physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), headerSliverBuilder: (context, _) => [ SliverAppBar( centerTitle: false, pinned: true, floating: false, snap: false, surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, actions: [ if (!gradeCalcMode) Padding( padding: const EdgeInsets.symmetric( horizontal: 5.0, vertical: 0.0), child: IconButton( splashRadius: 24.0, onPressed: () { if (!Provider.of(context, listen: false) .hasScope(PremiumScopes.totalGradeCalculator)) { PlusLockedFeaturePopup.show( context: context, feature: PremiumFeature.gradeCalculation); return; } // SoonAlert.show(context: context); gradeCalcTotal(context); }, icon: Icon( FeatherIcons.plus, color: AppColors.of(context).text, ), ), ), // profile Icon Padding( padding: const EdgeInsets.only(right: 24.0), child: ProfileButton( child: ProfileImage( heroTag: "profile", name: firstName, backgroundColor: Theme.of(context) .colorScheme .secondary, //ColorUtils.stringToColor(user.displayName ?? "?"), badge: updateProvider.available, role: user.role, profilePictureString: user.picture, ), ), ), ], automaticallyImplyLeading: false, title: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( "Grades".i18n, style: TextStyle( color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold), ), ), shadowColor: Theme.of(context).shadowColor, ), ], body: RefreshIndicator( onRefresh: () => gradeProvider.fetch(), color: Theme.of(context).colorScheme.secondary, child: ListView.builder( padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), itemCount: max(subjectTiles.length, 1), itemBuilder: (context, index) { if (subjectTiles.isNotEmpty) { EdgeInsetsGeometry panelPadding = const EdgeInsets.symmetric(horizontal: 24.0); if (subjectTiles[index].runtimeType == GradeSubjectTile) { return Padding( padding: panelPadding, child: PanelBody( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: subjectTiles[index], )); } else { return Padding( padding: panelPadding, child: subjectTiles[index]); } } else { return Container(); } }, ), ), ), ), ); } void gradeCalcTotal(BuildContext context) { calculatorProvider.clear(); calculatorProvider.addAllGrades(gradeProvider.grades); _sheetController = _scaffoldKey.currentState?.showBottomSheet( (context) => const RoundedBottomSheet( borderRadius: 14.0, child: GradeCalculator(null)), backgroundColor: const Color(0x00000000), elevation: 12.0, ); // Hide the fab and grades setState(() { gradeCalcMode = true; }); _sheetController!.closed.then((value) { // Show fab and grades if (mounted) { setState(() { gradeCalcMode = false; }); } }); } }