forked from firka/firka
ref: grade utils
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
bool _isPercentageGrade(Grade grade) {
|
||||
final name = grade.valueType.name?.toLowerCase() ?? '';
|
||||
return name.contains('szazalek') || name.contains('percent');
|
||||
}
|
||||
|
||||
bool shouldIgnoreInAverage(Grade grade) {
|
||||
if (_isPercentageGrade(grade)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final typeName = grade.type.name?.toLowerCase() ?? '';
|
||||
if (typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
double calculateAverage(List<Grade> sortedGrades, {bool applyIgnoreFilter = true}) {
|
||||
double totalWeight = 0.0;
|
||||
double weightedSum = 0.0;
|
||||
|
||||
if (applyIgnoreFilter &&
|
||||
sortedGrades.isNotEmpty &&
|
||||
sortedGrades.where((g) => !shouldIgnoreInAverage(g)).isEmpty) {
|
||||
final grades = sortedGrades.where(
|
||||
(g) => g.numericValue != null && g.numericValue! > 0,
|
||||
);
|
||||
|
||||
if (grades.isNotEmpty) {
|
||||
return grades.last.numericValue!.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
for (final grade in sortedGrades) {
|
||||
if (applyIgnoreFilter && shouldIgnoreInAverage(grade)) continue;
|
||||
|
||||
final value = grade.numericValue;
|
||||
final weight = grade.weightPercentage;
|
||||
|
||||
if (value != null && weight != null) {
|
||||
weightedSum += value * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight == 0) {
|
||||
return double.parse(0.0.toStringAsFixed(2));
|
||||
}
|
||||
|
||||
final avg = weightedSum / totalWeight;
|
||||
return double.parse(avg.toStringAsFixed(2));
|
||||
}
|
||||
@@ -24,6 +24,16 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
GoRouter createAppRouter() {
|
||||
final subjectRoute = GoRoute(
|
||||
path: 'subject',
|
||||
builder: (context, state) {
|
||||
return DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: HomeGradesSubjectScreen(state.extra as Subject, initData),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return GoRouter(
|
||||
navigatorKey: navigatorKey,
|
||||
initialLocation: _initialLocation,
|
||||
@@ -124,19 +134,7 @@ GoRouter createAppRouter() {
|
||||
child: HomeMainScreen(initData),
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'subject/:uid',
|
||||
builder: (context, state) {
|
||||
final uid = state.pathParameters['uid'] ?? '';
|
||||
activeSubjectUid = uid;
|
||||
return DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: HomeGradesSubjectScreen(initData),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
routes: [subjectRoute],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -151,19 +149,7 @@ GoRouter createAppRouter() {
|
||||
child: HomeGradesScreen(initData),
|
||||
),
|
||||
),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'subject/:uid',
|
||||
builder: (context, state) {
|
||||
final uid = state.pathParameters['uid'] ?? '';
|
||||
activeSubjectUid = uid;
|
||||
return DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: HomeGradesSubjectScreen(initData),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
routes: [subjectRoute],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -186,17 +172,7 @@ GoRouter createAppRouter() {
|
||||
child: HomeTimetableMonthlyScreen(initData),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'subject/:uid',
|
||||
builder: (context, state) {
|
||||
final uid = state.pathParameters['uid'] ?? '';
|
||||
activeSubjectUid = uid;
|
||||
return DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: HomeGradesSubjectScreen(initData),
|
||||
);
|
||||
},
|
||||
),
|
||||
subjectRoute,
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -128,10 +128,8 @@ Future<void> showLessonBottomSheet(
|
||||
FilledCircle(
|
||||
diameter: 40,
|
||||
color: bgColor,
|
||||
child: ClassIconWidget(
|
||||
uid: lesson.uid,
|
||||
className: lesson.name,
|
||||
category: subjectName,
|
||||
child: ClassIconWidget.subject(
|
||||
subject: lesson.subject!,
|
||||
color: accent,
|
||||
size: 26,
|
||||
),
|
||||
@@ -218,13 +216,7 @@ Future<void> showLessonBottomSheet(
|
||||
color: appStyle.colors.buttonSecondaryFill,
|
||||
),
|
||||
onTap: () {
|
||||
activeSubjectUid = lesson.subject!.uid;
|
||||
subjectName = lesson.subject!.name;
|
||||
subjectId = lesson.subject!.uid;
|
||||
subjectCategory = lesson.subject!.category.name;
|
||||
subjectInfo = [];
|
||||
Navigator.pop(context);
|
||||
context.push('/timetable/subject/${lesson.subject!.uid}');
|
||||
context.go('/timetable/subject', extra: lesson.subject);
|
||||
},
|
||||
),
|
||||
]);
|
||||
@@ -392,13 +384,8 @@ Future<void> showGradeBottomSheet(
|
||||
color: appStyle.colors.buttonSecondaryFill,
|
||||
),
|
||||
onTap: () {
|
||||
activeSubjectUid = grade.subject.uid;
|
||||
subjectName = grade.subject.name;
|
||||
subjectId = grade.subject.uid;
|
||||
subjectCategory = grade.subject.category.name;
|
||||
subjectInfo = [];
|
||||
Navigator.pop(context);
|
||||
context.go('/grades/subject/${grade.subject.uid}');
|
||||
context.go('/grades/subject', extra: grade.subject);
|
||||
},
|
||||
),
|
||||
]);
|
||||
@@ -490,13 +477,7 @@ Future<void> showHomeworkBottomSheet(
|
||||
color: appStyle.colors.buttonSecondaryFill,
|
||||
),
|
||||
onTap: () {
|
||||
activeSubjectUid = homework.subject.uid;
|
||||
subjectName = homework.subjectName;
|
||||
subjectId = homework.subject.uid;
|
||||
subjectCategory = "";
|
||||
subjectInfo = [];
|
||||
Navigator.pop(context);
|
||||
context.push('/home/subject/${homework.subject.uid}');
|
||||
context.go('/home/subject', extra: homework.subject);
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:firka/core/extensions.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/core/average_helper.dart';
|
||||
import 'package:firka/ui/components/firka_card.dart';
|
||||
import 'package:firka/ui/components/grade_helpers.dart';
|
||||
import 'package:firka/ui/phone/widgets/grade_chart.dart';
|
||||
@@ -27,12 +29,6 @@ class HomeGradesScreen extends StatefulWidget {
|
||||
State<StatefulWidget> createState() => _HomeGradesScreen();
|
||||
}
|
||||
|
||||
String activeSubjectUid = "";
|
||||
String subjectName = "";
|
||||
String subjectId = "";
|
||||
String subjectCategory = "";
|
||||
List<Subject> subjectInfo = [];
|
||||
|
||||
class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
||||
ApiResponse<List<Grade>>? grades;
|
||||
ApiResponse<List<Lesson>>? week;
|
||||
@@ -105,165 +101,32 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
var subjectAvg = 0.00;
|
||||
var subjectCount = 0;
|
||||
var subjectAvgRounded = 0.00;
|
||||
final allGrades = grades!.response!;
|
||||
final bySubject = <String, List<Grade>>{};
|
||||
for (final g in allGrades) {
|
||||
bySubject.putIfAbsent(g.subject.uid, () => []).add(g);
|
||||
}
|
||||
final gradesForCalculation = <Grade>[];
|
||||
for (final subjectGrades in bySubject.values) {
|
||||
final feleviOrEvvegi = subjectGrades.where((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles';
|
||||
}).toList();
|
||||
final hasOtherType = subjectGrades.any((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName != 'felevi_jegy_ertekeles' &&
|
||||
typeName != 'evvegi_jegy_ertekeles';
|
||||
});
|
||||
if (!hasOtherType && feleviOrEvvegi.isNotEmpty) {
|
||||
final withValue = feleviOrEvvegi
|
||||
.where((g) => g.numericValue != null && g.numericValue! > 0)
|
||||
.toList();
|
||||
if (withValue.isNotEmpty) {
|
||||
withValue.sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
gradesForCalculation.add(withValue.last);
|
||||
}
|
||||
} else {
|
||||
gradesForCalculation.addAll(
|
||||
subjectGrades.where((g) => !shouldIgnoreInAverage(g)),
|
||||
);
|
||||
}
|
||||
}
|
||||
final allLessons = lessons!.response!;
|
||||
|
||||
final summaryAvg2 = calculateAverage(
|
||||
gradesForCalculation,
|
||||
applyIgnoreFilter: false,
|
||||
final subjectAverage = allGrades.getSubjectAverage();
|
||||
|
||||
final Set<Subject> subjects = HashSet(
|
||||
hashCode: (s) => s.uid.hashCode,
|
||||
equals: (s, s2) => s.uid == s2.uid,
|
||||
);
|
||||
final List<Subject> subjects = List<Subject>.empty(growable: true);
|
||||
final List<Widget> gradeCards = [];
|
||||
|
||||
for (var grade in allGrades) {
|
||||
if (subjects.where((s) => s.uid == grade.subject.uid).isEmpty) {
|
||||
subjects.add(grade.subject);
|
||||
}
|
||||
allGrades.map((g) => g.subject).forEach(subjects.add);
|
||||
allLessons.map((l) => l.subject).forEach(subjects.add);
|
||||
|
||||
for (var subject
|
||||
in subjects.toList()..sort((s1, s2) => s1.name.compareTo(s2.name))) {
|
||||
gradeCards.add(
|
||||
GestureDetector(
|
||||
child: GradeSmallCard(allGrades, subject),
|
||||
onTap: () {
|
||||
context.go('/grades/subject', extra: subject);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (lessons != null && lessons!.response != null) {
|
||||
for (var lesson in lessons!.response!) {
|
||||
if (subjects.where((s) => s.uid == lesson.uid).isEmpty) {
|
||||
subjects.add(lesson.subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subjects.sort((s1, s2) => s1.name.compareTo(s2.name));
|
||||
|
||||
for (var subject in subjects) {
|
||||
final subjectGrades = allGrades
|
||||
.where((g) => g.subject.uid == subject.uid)
|
||||
.toList();
|
||||
|
||||
double avg = double.nan;
|
||||
if (subjectGrades.isNotEmpty) {
|
||||
final feleviOrEvvegi = subjectGrades.where((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles';
|
||||
}).toList();
|
||||
final hasOtherType = subjectGrades.any((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName != 'felevi_jegy_ertekeles' &&
|
||||
typeName != 'evvegi_jegy_ertekeles';
|
||||
});
|
||||
if (!hasOtherType && feleviOrEvvegi.isNotEmpty) {
|
||||
final withValue = feleviOrEvvegi
|
||||
.where((g) => g.numericValue != null && g.numericValue! > 0)
|
||||
.toList();
|
||||
if (withValue.isNotEmpty) {
|
||||
withValue.sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
avg = withValue.last.numericValue!.toDouble();
|
||||
}
|
||||
} else {
|
||||
avg = subjectGrades.getAverageBySubject(subject);
|
||||
}
|
||||
}
|
||||
|
||||
if (avg.isNaN) {
|
||||
gradeCards.add(
|
||||
GestureDetector(
|
||||
child: GradeSmallCard(allGrades, subject),
|
||||
onTap: () {
|
||||
activeSubjectUid = subject.uid;
|
||||
subjectName = subject.name;
|
||||
subjectId = subject.uid;
|
||||
subjectCategory = subject.category.name!;
|
||||
subjectInfo = subjects
|
||||
.where((s) => s.uid == subject.uid)
|
||||
.toList();
|
||||
context.go('/grades/subject/${subject.uid}');
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
gradeCards.add(
|
||||
GestureDetector(
|
||||
child: GradeSmallCard(allGrades, subject),
|
||||
onTap: () {
|
||||
activeSubjectUid = subject.uid;
|
||||
subjectName = subject.name;
|
||||
subjectId = subject.uid;
|
||||
subjectCategory = subject.category.name!;
|
||||
subjectInfo = subjects
|
||||
.where((s) => s.uid == subject.uid)
|
||||
.toList();
|
||||
context.go('/grades/subject/${subject.uid}');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!avg.isNaN && avg > 0) {
|
||||
subjectCount++;
|
||||
subjectAvg += avg;
|
||||
final rounding = widget.data.settings
|
||||
.group("settings")
|
||||
.subGroup("application")
|
||||
.subGroup("rounding");
|
||||
subjectAvgRounded += roundGrade(
|
||||
avg,
|
||||
t1: rounding.dbl("1"),
|
||||
t2: rounding.dbl("2"),
|
||||
t3: rounding.dbl("3"),
|
||||
t4: rounding.dbl("4"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
subjectAvg /= subjectCount;
|
||||
subjectAvgRounded /= subjectCount;
|
||||
|
||||
if (subjectCount == 0) {
|
||||
subjectAvg = 0.00;
|
||||
subjectAvgRounded = 0.00;
|
||||
}
|
||||
|
||||
final rounding = widget.data.settings
|
||||
.group("settings")
|
||||
.subGroup("application")
|
||||
.subGroup("rounding");
|
||||
var subjectAvgColor = getGradeColor(
|
||||
subjectAvg,
|
||||
t1: rounding.dbl("1"),
|
||||
t2: rounding.dbl("2"),
|
||||
t3: rounding.dbl("3"),
|
||||
t4: rounding.dbl("4"),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0, right: 20.0, top: 12.0),
|
||||
child: Column(
|
||||
@@ -279,12 +142,9 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
GradeChartWithInteraction(grades: gradesForCalculation),
|
||||
GradeChartWithInteraction(grades: allGrades),
|
||||
SizedBox(height: 2),
|
||||
GradeSummaryBar(
|
||||
grades: gradesForCalculation,
|
||||
l10n: widget.data.l10n,
|
||||
),
|
||||
GradeSummaryBar(grades: allGrades, l10n: widget.data.l10n),
|
||||
SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
@@ -319,84 +179,25 @@ class _HomeGradesScreen extends FirkaState<HomeGradesScreen> {
|
||||
),
|
||||
],
|
||||
right: [
|
||||
Card(
|
||||
shadowColor: Colors.transparent,
|
||||
color: subjectAvgColor.withAlpha(38),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
if (subjectAverage != null)
|
||||
Container(
|
||||
width: 48,
|
||||
height: 26,
|
||||
decoration: ShapeDecoration(
|
||||
color: getGradeColor(subjectAverage).withAlpha(38),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
subjectAvg.toStringAsFixed(2),
|
||||
style: appStyle.fonts.B_16SB.apply(
|
||||
color: subjectAvgColor,
|
||||
child: Center(
|
||||
child: Text(
|
||||
subjectAverage.toStringAsFixed(2),
|
||||
style: appStyle.fonts.B_16R.apply(
|
||||
color: getGradeColor(subjectAverage),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FirkaCard(
|
||||
left: [
|
||||
Text(
|
||||
widget.data.l10n.subject_avg_rounded,
|
||||
style: appStyle.fonts.B_16SB.apply(
|
||||
color: appStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
right: [
|
||||
Card(
|
||||
shadowColor: Colors.transparent,
|
||||
color: subjectAvgColor.withAlpha(38),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
child: Text(
|
||||
subjectAvgRounded.toStringAsFixed(2),
|
||||
style: appStyle.fonts.B_16SB.apply(
|
||||
color: subjectAvgColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FirkaCard(
|
||||
left: [
|
||||
Text(
|
||||
widget.data.l10n.overall_avg,
|
||||
style: appStyle.fonts.B_16SB.apply(
|
||||
color: appStyle.colors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
right: [
|
||||
Card(
|
||||
shadowColor: Colors.transparent,
|
||||
color: subjectAvgColor.withAlpha(38),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
),
|
||||
child: Text(
|
||||
summaryAvg2.toStringAsFixed(2),
|
||||
style: appStyle.fonts.B_16SB.apply(
|
||||
color: subjectAvgColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
FirkaCard(
|
||||
|
||||
@@ -23,8 +23,9 @@ import 'package:firka/ui/theme/style.dart';
|
||||
|
||||
class HomeGradesSubjectScreen extends StatefulWidget {
|
||||
final AppInitialization data;
|
||||
final Subject subject;
|
||||
|
||||
const HomeGradesSubjectScreen(this.data, {super.key});
|
||||
const HomeGradesSubjectScreen(this.subject, this.data, {super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _HomeGradesSubjectScreen();
|
||||
@@ -36,9 +37,9 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
||||
|
||||
void _onRefreshRequested(BuildContext context) async {
|
||||
final cubit = context.read<HomeRefreshCubit>();
|
||||
grades = (await widget.data.client.getGrades(forceCache: false)).response!
|
||||
.where((grade) => grade.subject.uid == activeSubjectUid)
|
||||
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
|
||||
grades = (await widget.data.client.getGrades(
|
||||
forceCache: false,
|
||||
)).response!.where((grade) => grade.subject.uid == widget.subject.uid);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
@@ -51,9 +52,9 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
||||
super.initState();
|
||||
|
||||
(() async {
|
||||
grades = (await widget.data.client.getGrades()).response!
|
||||
.where((grade) => grade.subject.uid == activeSubjectUid)
|
||||
.where((grade) => grade.type.name != "felevi_jegy_ertekeles");
|
||||
grades = (await widget.data.client.getGrades()).response!.where(
|
||||
(grade) => grade.subject.uid == widget.subject.uid,
|
||||
);
|
||||
|
||||
if (mounted) setState(() {});
|
||||
})();
|
||||
@@ -107,7 +108,7 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
if (grades == null || grades!.isEmpty || activeSubjectUid == "") {
|
||||
if (grades == null || grades!.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
@@ -152,17 +153,15 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsetsGeometry.all(6),
|
||||
child: ClassIconWidget(
|
||||
uid: subjectId,
|
||||
className: subjectName,
|
||||
category: subjectCategory,
|
||||
child: ClassIconWidget.subject(
|
||||
subject: widget.subject,
|
||||
color: appStyle.colors.accent,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
subjectName,
|
||||
widget.subject.name,
|
||||
style: appStyle.fonts.H_H2.apply(
|
||||
color: appStyle.colors.textPrimary,
|
||||
),
|
||||
@@ -289,10 +288,8 @@ class _HomeGradesSubjectScreen extends FirkaState<HomeGradesSubjectScreen> {
|
||||
FilledCircle(
|
||||
diameter: 36,
|
||||
color: appStyle.colors.a15p,
|
||||
child: ClassIconWidget(
|
||||
uid: aGrade.subject.uid,
|
||||
className: aGrade.subject.name,
|
||||
category: aGrade.subject.category.name!,
|
||||
child: ClassIconWidget.subject(
|
||||
subject: aGrade.subject,
|
||||
color: appStyle.colors.accent,
|
||||
size: 24,
|
||||
),
|
||||
|
||||
@@ -5,8 +5,6 @@ import 'package:firka_common/firka_common.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/routing/chart_interaction_scope.dart';
|
||||
import 'package:firka/ui/components/grade_helpers.dart';
|
||||
import 'package:firka/ui/theme/style.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -31,46 +29,6 @@ class _GradeChartState extends State<GradeChart> {
|
||||
|
||||
late List<DateSpot> spots;
|
||||
|
||||
double? _subjectAverageInList(List<Grade> grades, String subjectUid) {
|
||||
double weightedSum = 0;
|
||||
double totalWeight = 0;
|
||||
for (final g in grades) {
|
||||
if (g.subject.uid != subjectUid) continue;
|
||||
final name = g.valueType.name?.toLowerCase() ?? '';
|
||||
final isPercentage =
|
||||
name.contains('szazalek') || name.contains('percent');
|
||||
if (isPercentage) continue;
|
||||
final v = g.numericValue;
|
||||
final w = g.weightPercentage;
|
||||
if (v != null && w != null) {
|
||||
final effectiveValue = g.valueType.name == "Szazalekos"
|
||||
? percentageToGrade(v).toDouble()
|
||||
: v.toDouble();
|
||||
weightedSum += effectiveValue * w;
|
||||
totalWeight += w;
|
||||
}
|
||||
}
|
||||
return totalWeight > 0 ? weightedSum / totalWeight : null;
|
||||
}
|
||||
|
||||
double _runningSubjectAverage(List<Grade> sortedGrades, int upToInclusive) {
|
||||
final sublist = sortedGrades.sublist(
|
||||
0,
|
||||
(upToInclusive + 1).clamp(0, sortedGrades.length),
|
||||
);
|
||||
final subjectUids = sublist.map((g) => g.subject.uid).toSet();
|
||||
double sum = 0;
|
||||
int count = 0;
|
||||
for (final uid in subjectUids) {
|
||||
final avg = _subjectAverageInList(sublist, uid);
|
||||
if (avg != null) {
|
||||
sum += avg;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count > 0 ? sum / count : 0;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -79,12 +37,7 @@ class _GradeChartState extends State<GradeChart> {
|
||||
|
||||
void _computeSpots() {
|
||||
final sortedGrades =
|
||||
widget.grades
|
||||
.where(
|
||||
(grade) =>
|
||||
grade.numericValue != null && grade.weightPercentage != null,
|
||||
)
|
||||
.toList()
|
||||
widget.grades.where((grade) => grade.shouldIncludeInAverage()).toList()
|
||||
..sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
|
||||
if (sortedGrades.isEmpty) {
|
||||
@@ -98,8 +51,10 @@ class _GradeChartState extends State<GradeChart> {
|
||||
|
||||
spots = [];
|
||||
for (var i = 0; i < sortedGrades.length; i++) {
|
||||
final partialAvg = _runningSubjectAverage(sortedGrades, i);
|
||||
spots.add(DateSpot(i.toDouble(), partialAvg, sortedGrades[i].recordDate));
|
||||
final partialAvg = sortedGrades.take(i + 1).getSubjectAverage();
|
||||
spots.add(
|
||||
DateSpot(i.toDouble(), partialAvg!, sortedGrades[i].recordDate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:firka/core/average_helper.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/ui/components/grade.dart';
|
||||
import 'package:firka/ui/components/grade_helpers.dart';
|
||||
@@ -30,17 +29,9 @@ class _GradeSummaryBarState extends State<GradeSummaryBar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final (total, countsByGrade) = getGradeDistribution(widget.grades);
|
||||
final gradeColors = [
|
||||
appStyle.colors.grade1,
|
||||
appStyle.colors.grade2,
|
||||
appStyle.colors.grade3,
|
||||
appStyle.colors.grade4,
|
||||
appStyle.colors.grade5,
|
||||
];
|
||||
final totalCounted = countsByGrade.reduce((a, b) => a + b);
|
||||
final (total, countsByGrade) = widget.grades.getGradeDistribution();
|
||||
final averageText = widget.showAverage
|
||||
? calculateAverage(widget.grades).toStringAsFixed(2)
|
||||
? (widget.grades.getAverage() ?? 0).toStringAsFixed(2)
|
||||
: '';
|
||||
|
||||
return Card(
|
||||
@@ -72,10 +63,13 @@ class _GradeSummaryBarState extends State<GradeSummaryBar> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Row(
|
||||
children: List.generate(5, (i) {
|
||||
final flex = totalCounted > 0 ? countsByGrade[i] : 1;
|
||||
final flex = total > 0 ? countsByGrade[i] : 1;
|
||||
return Expanded(
|
||||
flex: flex,
|
||||
child: Container(height: 10, color: gradeColors[i]),
|
||||
child: Container(
|
||||
height: 10,
|
||||
color: getGradeColor(i + 1),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -38,13 +38,7 @@ class InfoCard extends StatelessWidget {
|
||||
return FilledCircle(
|
||||
diameter: 32,
|
||||
color: color.withAlpha(38),
|
||||
child: ClassIconWidget(
|
||||
uid: subject.uid,
|
||||
className: subject.name,
|
||||
category: subject.category.name!,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
child: ClassIconWidget.subject(subject: subject, color: color, size: 20),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class GradeWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (g.valueType.name == 'Szazalekos') {
|
||||
final gradeColor = getGradeColor(percentageToGrade(g.numericValue!));
|
||||
final gradeColor = appStyle.colors.accent;
|
||||
return FilledCircle(
|
||||
diameter: size,
|
||||
color: gradeColor.withAlpha(38),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:firka_common/ui/theme/style.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
int roundGrade(
|
||||
num grade, {
|
||||
@@ -26,23 +27,6 @@ int roundGrade(
|
||||
return 5;
|
||||
}
|
||||
|
||||
int percentageToGrade(int grade) {
|
||||
if (grade < 50) {
|
||||
return 1;
|
||||
}
|
||||
if (grade < 60) {
|
||||
return 2;
|
||||
}
|
||||
if (grade < 70) {
|
||||
return 3;
|
||||
}
|
||||
if (grade < 80) {
|
||||
return 4;
|
||||
}
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
Color getGradeColor(
|
||||
num grade, {
|
||||
double t1 = 1,
|
||||
@@ -64,77 +48,80 @@ Color getGradeColor(
|
||||
}
|
||||
}
|
||||
|
||||
(int total, List<int> countsByGrade) getGradeDistribution(List<Grade> grades) {
|
||||
final filtered = grades.where((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
extension GradeListExtension on Iterable<Grade> {
|
||||
(int total, List<int> countsByGrade) getGradeDistribution() {
|
||||
final filtered = where((g) => g.shouldIncludeInAverage());
|
||||
final counts = [0, 0, 0, 0, 0];
|
||||
for (final g in filtered) {
|
||||
counts[g.numericValue! - 1]++;
|
||||
}
|
||||
return (filtered.length, counts);
|
||||
}
|
||||
|
||||
double? getAverageBySubject(Subject subject) {
|
||||
return where(
|
||||
(g) => g.subject.uid == subject.uid && g.shouldIncludeInAverage(),
|
||||
).getAverage();
|
||||
}
|
||||
|
||||
double? getSubjectAverage() {
|
||||
final averages = map(
|
||||
(g) => g.subject,
|
||||
).toSet().map((subject) => getAverageBySubject(subject)).nonNulls;
|
||||
|
||||
if (averages.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return averages.reduce((sum, avg) => sum + avg) / averages.length;
|
||||
}
|
||||
|
||||
double? getAverage() {
|
||||
if (isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double weightTotal = 0;
|
||||
double sum = 0;
|
||||
for (Grade grade in this) {
|
||||
if (!grade.shouldIncludeInAverage()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double weight = (grade.weightPercentage ?? 100) / 100.0;
|
||||
weightTotal += weight;
|
||||
sum += grade.numericValue! * weight;
|
||||
}
|
||||
|
||||
if (sum == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sum / weightTotal;
|
||||
}
|
||||
}
|
||||
|
||||
extension GradeExtension on Grade {
|
||||
bool isInPercentage() {
|
||||
final name = valueType.name.toLowerCase();
|
||||
return name.contains('szazalek') || name.contains('percent');
|
||||
}
|
||||
|
||||
bool shouldIncludeInAverage() {
|
||||
if (isInPercentage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final typeName = type.name.toLowerCase();
|
||||
if (typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles') {
|
||||
return false;
|
||||
}
|
||||
|
||||
final valueTypeName = g.valueType.name?.toLowerCase() ?? '';
|
||||
final isPercentage =
|
||||
valueTypeName.contains('szazalek') || valueTypeName.contains('percent');
|
||||
if (isPercentage) {
|
||||
if (numericValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
final counts = [0, 0, 0, 0, 0];
|
||||
for (final g in filtered) {
|
||||
if (g.numericValue == null) continue;
|
||||
final value = g.valueType.name == "Szazalekos"
|
||||
? percentageToGrade(g.numericValue!.round())
|
||||
: g.numericValue!.round().clamp(1, 5);
|
||||
counts[value - 1]++;
|
||||
}
|
||||
return (filtered.length, counts);
|
||||
}
|
||||
|
||||
extension GradeListExtension on List<Grade> {
|
||||
double getAverageBySubject(Subject subject) {
|
||||
final subjectGrades = where((g) => g.subject.uid == subject.uid).toList();
|
||||
if (subjectGrades.isEmpty) return double.nan;
|
||||
|
||||
final feleviOrEvvegi = subjectGrades.where((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles';
|
||||
}).toList();
|
||||
final hasOtherType = subjectGrades.any((g) {
|
||||
final typeName = g.type.name?.toLowerCase() ?? '';
|
||||
return typeName != 'felevi_jegy_ertekeles' &&
|
||||
typeName != 'evvegi_jegy_ertekeles';
|
||||
});
|
||||
|
||||
if (!hasOtherType && feleviOrEvvegi.isNotEmpty) {
|
||||
final withValue = feleviOrEvvegi
|
||||
.where((g) => g.numericValue != null && g.numericValue! > 0)
|
||||
.toList();
|
||||
if (withValue.isEmpty) return double.nan;
|
||||
withValue.sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
return withValue.last.numericValue!.toDouble();
|
||||
}
|
||||
|
||||
var weightTotal = 0.00;
|
||||
var sum = 0.00;
|
||||
for (var grade in subjectGrades) {
|
||||
final valueTypeName = grade.valueType.name?.toLowerCase() ?? '';
|
||||
final isPercentage =
|
||||
valueTypeName.contains('szazalek') ||
|
||||
valueTypeName.contains('percent');
|
||||
final typeName = grade.type.name?.toLowerCase() ?? '';
|
||||
final isHalfYear = typeName == 'felevi_jegy_ertekeles';
|
||||
final isEndYear = typeName == 'evvegi_jegy_ertekeles';
|
||||
if (isPercentage || isHalfYear || isEndYear) continue;
|
||||
if (grade.numericValue != null) {
|
||||
var weight = (grade.weightPercentage ?? 100) / 100.0;
|
||||
weightTotal += weight;
|
||||
sum += grade.numericValue! * weight;
|
||||
}
|
||||
}
|
||||
if (weightTotal == 0) return double.nan;
|
||||
return sum / weightTotal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:firka_common/core/icon_helper.dart';
|
||||
import 'package:firka_common/ui/shared/firka_icon.dart';
|
||||
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
class ClassIconWidget extends StatelessWidget {
|
||||
final String _uid;
|
||||
final String _className;
|
||||
@@ -10,6 +12,15 @@ class ClassIconWidget extends StatelessWidget {
|
||||
final Color color;
|
||||
final double? size;
|
||||
|
||||
ClassIconWidget.subject({
|
||||
super.key,
|
||||
required Subject subject,
|
||||
this.color = Colors.white,
|
||||
this.size,
|
||||
}) : _className = subject.name,
|
||||
_uid = subject.uid,
|
||||
_category = subject.category.name;
|
||||
|
||||
const ClassIconWidget({
|
||||
super.key,
|
||||
required String uid,
|
||||
|
||||
@@ -6,13 +6,12 @@ import 'package:firka_common/ui/components/grade_helpers.dart';
|
||||
import 'package:firka_common/ui/shared/class_icon.dart';
|
||||
import 'package:firka_common/ui/theme/style.dart';
|
||||
|
||||
import '../../firka_common.dart';
|
||||
|
||||
class GradeSmallCard extends StatelessWidget {
|
||||
final List<Grade> grades;
|
||||
final double? average;
|
||||
final Subject subject;
|
||||
|
||||
GradeSmallCard(this.grades, this.subject, {super.key});
|
||||
GradeSmallCard(List<Grade> grades, this.subject, {super.key})
|
||||
: average = grades.getAverageBySubject(subject);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -39,26 +38,22 @@ class GradeSmallCard extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
grades.getAverageBySubject(subject).isNaN
|
||||
average == null
|
||||
? const SizedBox()
|
||||
: Container(
|
||||
width: 48,
|
||||
height: 26,
|
||||
decoration: ShapeDecoration(
|
||||
color: getGradeColor(
|
||||
grades.getAverageBySubject(subject),
|
||||
).withAlpha(38),
|
||||
color: getGradeColor(average!).withAlpha(38),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grades.getAverageBySubject(subject).toStringAsFixed(2),
|
||||
average!.toStringAsFixed(2),
|
||||
style: appStyle.fonts.B_16R.apply(
|
||||
color: getGradeColor(
|
||||
grades.getAverageBySubject(subject),
|
||||
),
|
||||
color: getGradeColor(average!),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user