// ignore_for_file: no_leading_underscores_for_local_identifiers import 'dart:math'; import 'package:animations/animations.dart'; import 'package:collection/collection.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:refilc/api/providers/update_provider.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc/theme/colors/utils.dart'; import 'package:refilc/ui/date_widget.dart'; import 'package:refilc_kreta_api/models/absence.dart'; import 'package:refilc_kreta_api/models/lesson.dart'; import 'package:refilc_kreta_api/models/subject.dart'; import 'package:refilc_kreta_api/models/week.dart'; import 'package:refilc_kreta_api/providers/absence_provider.dart'; import 'package:refilc_kreta_api/providers/note_provider.dart'; import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/theme/colors/colors.dart'; import 'package:refilc_kreta_api/providers/timetable_provider.dart'; import 'package:refilc_mobile_ui/common/action_button.dart'; import 'package:refilc_mobile_ui/common/empty.dart'; import 'package:refilc_mobile_ui/common/filter_bar.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/splitted_panel/splitted_panel.dart'; import 'package:refilc_mobile_ui/common/widgets/absence/absence_subject_tile.dart'; import 'package:refilc_mobile_ui/common/widgets/absence/absence_viewable.dart'; import 'package:refilc_mobile_ui/common/widgets/miss_tile.dart'; import 'package:refilc_mobile_ui/pages/absences/absence_subject_view.dart'; import 'package:refilc/ui/filter/sort.dart'; import 'package:flutter/material.dart'; import 'package:flutter_feather_icons/flutter_feather_icons.dart'; import 'package:provider/provider.dart'; import 'absences_page.i18n.dart'; enum AbsenceFilter { absences, delays, misses } class SubjectAbsence { GradeSubject subject; List absences; double percentage; SubjectAbsence( {required this.subject, this.absences = const [], this.percentage = 0.0}); } class AbsencesPage extends StatefulWidget { const AbsencesPage({super.key}); @override AbsencesPageState createState() => AbsencesPageState(); } class AbsencesPageState extends State with TickerProviderStateMixin { late UserProvider user; late AbsenceProvider absenceProvider; late TimetableProvider timetableProvider; late NoteProvider noteProvider; late UpdateProvider updateProvider; late String firstName; late TabController _tabController; late List absences = []; final Map _lessonCount = {}; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); timetableProvider = Provider.of(context, listen: false); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { for (final lesson in timetableProvider.getWeek(Week.current()) ?? []) { if (!lesson.isEmpty && lesson.subject.id != '' && lesson.lessonYearIndex != null) { _lessonCount.update( lesson.subject, (value) { if (lesson.lessonYearIndex! > value.lessonYearIndex!) { return lesson; } else { return value; } }, ifAbsent: () => lesson, ); } } setState(() {}); }); } void buildSubjectAbsences() { Map _absences = {}; for (final absence in absenceProvider.absences) { if (absence.delay != 0) continue; if (!_absences.containsKey(absence.subject)) { _absences[absence.subject] = SubjectAbsence(subject: absence.subject, absences: [absence]); } else { _absences[absence.subject]?.absences.add(absence); } } _absences.forEach((subject, absence) { final absentLessonsOfSubject = absenceProvider.absences .where((e) => e.subject == subject && e.delay == 0) .length; final totalLessonsOfSubject = _lessonCount[subject]?.lessonYearIndex ?? 0; double absentLessonsOfSubjectPercentage; if (absentLessonsOfSubject <= totalLessonsOfSubject) { absentLessonsOfSubjectPercentage = absentLessonsOfSubject / totalLessonsOfSubject * 100; } else { absentLessonsOfSubjectPercentage = -1; } _absences[subject]?.percentage = absentLessonsOfSubjectPercentage.clamp(-1, 100.0); }); absences = _absences.values.toList(); absences.sort((a, b) => -a.percentage.compareTo(b.percentage)); } @override Widget build(BuildContext context) { user = Provider.of(context); absenceProvider = Provider.of(context); noteProvider = Provider.of(context); updateProvider = Provider.of(context); timetableProvider = Provider.of(context); List nameParts = user.displayName?.split(" ") ?? ["?"]; firstName = nameParts.length > 1 ? nameParts[1] : nameParts[0]; buildSubjectAbsences(); return Scaffold( body: Padding( padding: const EdgeInsets.only(top: 12.0), child: NestedScrollView( physics: const BouncingScrollPhysics( parent: AlwaysScrollableScrollPhysics()), headerSliverBuilder: (context, _) => [ SliverAppBar( pinned: true, floating: false, snap: false, centerTitle: false, surfaceTintColor: Theme.of(context).scaffoldBackgroundColor, actions: [ // Profile Icon Padding( padding: const EdgeInsets.only(right: 24.0), child: ProfileButton( child: ProfileImage( heroTag: "profile", name: firstName, backgroundColor: Theme.of(context) .colorScheme .tertiary, //ColorUtils.stringToColor(user.displayName ?? "?"), badge: updateProvider.available, role: user.role, profilePictureString: user.picture, gradeStreak: (user.gradeStreak ?? 0) > 1, ), ), ), ], automaticallyImplyLeading: false, shadowColor: Theme.of(context).shadowColor, title: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( "Absences".i18n, style: Provider.of(context).fontFamily != '' && Provider.of(context).titleOnlyFont ? GoogleFonts.getFont( Provider.of(context).fontFamily, textStyle: TextStyle( color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold, ), ) : TextStyle( color: AppColors.of(context).text, fontSize: 32.0, fontWeight: FontWeight.bold, ), ), ), bottom: FilterBar( items: [ Tab(text: "Absences".i18n), Tab(text: "Delays".i18n), Tab(text: "Misses".i18n), ], controller: _tabController, disableFading: true, ), ), ], body: TabBarView( physics: const BouncingScrollPhysics(), controller: _tabController, children: List.generate( 3, (index) => filterViewBuilder(context, index))), ), ), ); } List getFilterWidgets(AbsenceFilter activeData) { List items = []; switch (activeData) { case AbsenceFilter.absences: for (var a in absences) { items.add(DateWidget( date: DateTime.fromMillisecondsSinceEpoch(0), widget: AbsenceSubjectTile( a.subject, percentage: a.percentage, excused: a.absences .where((a) => a.state == Justification.excused) .length, unexcused: a.absences .where((a) => a.state == Justification.unexcused) .length, pending: a.absences .where((a) => a.state == Justification.pending) .length, onTap: () => AbsenceSubjectView.show(a.subject, a.absences, context: context), ), )); } break; case AbsenceFilter.delays: for (var absence in absenceProvider.absences) { if (absence.delay != 0) { items.add(DateWidget( date: absence.date, widget: AbsenceViewable(absence, padding: EdgeInsets.zero), )); } } break; case AbsenceFilter.misses: for (var note in noteProvider.notes) { if (note.type?.name == "HaziFeladatHiany" || note.type?.name == "Felszereleshiany") { items.add(DateWidget( date: note.date, widget: MissTile(note), )); } } break; } return items; } Widget filterViewBuilder(context, int activeData) { List filterWidgets = []; var absWidgets = getFilterWidgets(AbsenceFilter.values[activeData]) .map((e) => e.widget) .cast() .toList(); if (activeData > 0) { filterWidgets = sortDateWidgets( context, dateWidgets: getFilterWidgets(AbsenceFilter.values[activeData]), padding: EdgeInsets.zero, hasShadow: true, ); } else if (absWidgets.isNotEmpty) { filterWidgets = [ Padding( padding: const EdgeInsets.only(bottom: 24.0), child: Panel( padding: EdgeInsets.zero, isTransparent: true, hasShadow: false, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text("Subjects".i18n), Padding( padding: const EdgeInsets.only(right: 4.0), child: IconButton( onPressed: () { showDialog( context: context, builder: (context) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0)), title: Text("attention".i18n), content: Text("attention_body".i18n), actions: [ ActionButton( label: "Ok", onTap: () => Navigator.of(context).pop(), ), ], ), ); }, padding: EdgeInsets.zero, splashRadius: 24.0, visualDensity: VisualDensity.compact, constraints: BoxConstraints.tight(const Size(42.0, 42.0)), icon: const Icon(FeatherIcons.info), ), ), ], ), child: PageTransitionSwitcher( transitionBuilder: ( Widget child, Animation primaryAnimation, Animation secondaryAnimation, ) { return FadeThroughTransition( animation: primaryAnimation, secondaryAnimation: secondaryAnimation, fillColor: Colors.transparent, child: child, ); }, child: SplittedPanel( padding: EdgeInsets.zero, isSeparated: true, isTransparent: true, children: absWidgets, ), ), ), ) ]; } return Padding( padding: const EdgeInsets.only(top: 12.0), child: RefreshIndicator( color: Theme.of(context).colorScheme.secondary, onRefresh: () async { await absenceProvider.fetch(); await noteProvider.fetch(); }, child: ListView.builder( padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), itemCount: max(filterWidgets.length + (activeData <= 1 ? 1 : 0), 1), itemBuilder: (context, index) { if (filterWidgets.isNotEmpty) { if ((index == 0 && activeData == 1) || (index == 0 && activeData == 0)) { int value1 = 0; int value2 = 0; int value3 = 0; String title1 = ""; String title2 = ""; String suffix = ""; List unexcused = []; List excused = []; List pending = []; List absencePositions = []; List finalChartColors = []; if (activeData == AbsenceFilter.absences.index) { unexcused = absenceProvider.absences .where((e) => e.delay == 0 && e.state == Justification.unexcused) .toList(); excused = absenceProvider.absences .where((e) => e.delay == 0 && e.state == Justification.excused) .toList(); pending = absenceProvider.absences .where((e) => e.delay == 0 && e.state == Justification.pending) .toList(); value1 = excused.length; value2 = unexcused.length; value3 = pending.length; title1 = "stat_1".i18n; title2 = "stat_2".i18n; suffix = " ${"hr".i18n}"; } else if (activeData == AbsenceFilter.delays.index) { unexcused = absenceProvider.absences .where((e) => e.delay != 0 && e.state == Justification.unexcused) .toList(); excused = absenceProvider.absences .where((e) => e.delay != 0 && e.state == Justification.excused) .toList(); pending = absenceProvider.absences .where((e) => e.delay != 0 && e.state == Justification.pending) .toList(); value1 = excused.map((e) => e.delay).fold(0, (a, b) => a + b); value2 = unexcused.map((e) => e.delay).fold(0, (a, b) => a + b); value3 = pending.map((e) => e.delay).fold(0, (a, b) => a + b); title1 = "stat_3".i18n; title2 = "stat_4".i18n; suffix = " ${"min".i18n}"; } // bar chart magic List absenceChartData = []; int yr = DateTime.now().month < 9 ? DateTime.now().year - 1 : DateTime.now().year; int barTotal = DateTime.now().difference(DateTime(yr, 09, 01)).inDays; [...unexcused, ...excused, ...pending].forEachIndexed((i, a) { int abs = DateTime.now().difference(a.date).inDays; double startPos = (barTotal - abs) / barTotal; double endPos = startPos + (barTotal / 100 / barTotal); if (absenceChartData.isEmpty) { absenceChartData.add(AbsenceChartData( start: 0.0, end: startPos, color: Theme.of(context).scaffoldBackgroundColor, )); } absenceChartData.add(AbsenceChartData( start: startPos, end: endPos, color: a.state == Justification.excused ? Colors.green : a.state == Justification.unexcused ? Colors.red : Colors.orange, )); if ([...unexcused, ...excused, ...pending].length > i + 1) { int nextAbs = DateTime.now() .difference( [...unexcused, ...excused, ...pending][i + 1].date) .inDays; double nextStartPos = (barTotal - nextAbs) / barTotal; // double nextEndPos = startPos + (barTotal / 100 / barTotal); absenceChartData.add(AbsenceChartData( start: endPos, end: nextStartPos < 0.999 ? nextStartPos : 1.0, color: Theme.of(context).scaffoldBackgroundColor, )); } // print(value2.toString() + '-total'); // print(absenceChartData.length.toString() + '-chartdata'); if ((i + 1 == [...unexcused, ...excused, ...pending].length) && endPos < 0.999) { absenceChartData.add(AbsenceChartData( start: endPos, end: 1.0, color: Theme.of(context).scaffoldBackgroundColor, )); } }); for (var aP in absenceChartData) { absencePositions.addAll([aP.start, aP.end]); } for (var aC in absenceChartData) { finalChartColors.addAll([aC.color, aC.color]); } return Padding( padding: const EdgeInsets.only( bottom: 20.0, left: 24.0, right: 24.0), child: Row(children: [ Expanded( child: SplittedPanel( padding: EdgeInsets.zero, cardPadding: const EdgeInsets.symmetric( vertical: 16.0, horizontal: 18.0, ), spacing: 8.0, children: [ Column( children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( value1.toString() + suffix, style: TextStyle( fontSize: 18.0, fontWeight: FontWeight.w700, color: value1 > 0 ? AppColors.of(context).green : AppColors.of(context).text, ), ), Text( title1, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, height: 1.1, color: value1 > 0 ? ColorsUtils().fade( context, AppColors.of(context).green, darkenAmount: 0.5, lightenAmount: 0.4, ) : AppColors.of(context).text, ), ), ], ), const SizedBox( width: 18.0, ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( value2.toString() + suffix, style: TextStyle( fontSize: 18.0, fontWeight: FontWeight.w700, color: value2 > 0 ? AppColors.of(context).red : AppColors.of(context).text, ), ), Text( title2, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, height: 1.1, color: value2 > 0 ? ColorsUtils().fade( context, AppColors.of(context).red, darkenAmount: 0.4, lightenAmount: 0.2, ) : AppColors.of(context).text, ), ), ], ) ], ), const SizedBox( height: 18.0, ), Column( children: [ Container( height: 9.11, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.0), color: Theme.of(context) .scaffoldBackgroundColor, gradient: LinearGradient( colors: finalChartColors, stops: absencePositions, begin: Alignment.centerLeft, end: Alignment.centerRight, ), ), ), const SizedBox( height: 3.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "sept".i18n, ), Text("now".i18n), ], ), ], ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.av_timer_rounded, color: value3 > 0 ? Colors.orange : null, ), const SizedBox( width: 10.0, ), Text( "pending".i18n, style: const TextStyle( fontSize: 16.0, fontWeight: FontWeight.w600, ), ), ], ), Text( value3.toString() + suffix, style: TextStyle( fontSize: 16.0, fontWeight: FontWeight.w500, color: AppColors.of(context) .text .withOpacity(0.8), ), ), ], ), ], ), ), // Expanded( // child: StatisticsTile( // title: AutoSizeText( // title1, // textAlign: TextAlign.center, // maxLines: 2, // overflow: TextOverflow.ellipsis, // ), // valueSuffix: suffix, // value: value1.toDouble(), // decimal: false, // showZero: true, // color: AppColors.of(context).green, // ), // ), // const SizedBox(width: 24.0), // Expanded( // child: StatisticsTile( // title: AutoSizeText( // title2, // textAlign: TextAlign.center, // maxLines: 2, // overflow: TextOverflow.ellipsis, // ), // valueSuffix: suffix, // value: value2.toDouble(), // decimal: false, // showZero: true, // color: AppColors.of(context).red, // ), // ), ]), ); } return Padding( padding: const EdgeInsets.only( left: 24.0, right: 24.0, bottom: 12.0), child: filterWidgets[index - (activeData <= 1 ? 1 : 0)], ); } else { return activeData == 1 ? Empty(subtitle: "emptyDelays".i18n) : Empty(subtitle: "emptyMisses".i18n); } }, ), ), ); } }