237 lines
7.6 KiB
Dart
237 lines
7.6 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:smpn1_lengkong/core/models/student_model.dart';
|
|
import 'package:smpn1_lengkong/core/constants/app_colors.dart';
|
|
import 'package:smpn1_lengkong/core/models/academic_score_model.dart';
|
|
import 'package:smpn1_lengkong/features/academic/widgets/academic_subject_card.dart';
|
|
|
|
class AcademicScreen extends StatefulWidget {
|
|
const AcademicScreen({super.key});
|
|
|
|
@override
|
|
State<AcademicScreen> createState() => _AcademicScreenState();
|
|
}
|
|
|
|
class _AcademicScreenState extends State<AcademicScreen> {
|
|
late Future<List<AcademicScore>> _academicDataFuture;
|
|
bool _isInitialized = false;
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (!_isInitialized) {
|
|
_initFetch();
|
|
_isInitialized = true;
|
|
}
|
|
}
|
|
|
|
/// Fungsi bantuan untuk inisialisasi data awal
|
|
void _initFetch() {
|
|
final student = ModalRoute.of(context)!.settings.arguments as StudentModel;
|
|
_academicDataFuture = _fetchAcademicData(
|
|
student.id,
|
|
student.className,
|
|
_determineAcademicYear(student),
|
|
);
|
|
}
|
|
|
|
/// Menentukan tahun ajaran aktif dari data siswa
|
|
String _determineAcademicYear(StudentModel student) {
|
|
try {
|
|
return student.history
|
|
.firstWhere((h) => h.className == student.className)
|
|
.academicYear;
|
|
} catch (e) {
|
|
return student.history.isNotEmpty
|
|
? student.history.last.academicYear
|
|
: "-";
|
|
}
|
|
}
|
|
|
|
/// Fungsi untuk menangani Pull-to-Refresh
|
|
Future<void> _handleRefresh() async {
|
|
final student = ModalRoute.of(context)!.settings.arguments as StudentModel;
|
|
|
|
setState(() {
|
|
_academicDataFuture = _fetchAcademicData(
|
|
student.id,
|
|
student.className,
|
|
_determineAcademicYear(student),
|
|
);
|
|
});
|
|
|
|
// Tunggu hingga future selesai agar indikator refresh berhenti tepat waktu
|
|
await _academicDataFuture;
|
|
}
|
|
|
|
Future<List<AcademicScore>> _fetchAcademicData(
|
|
String studentId,
|
|
String className,
|
|
String academicYear,
|
|
) async {
|
|
final firestore = FirebaseFirestore.instance;
|
|
|
|
// 1. Ambil semua daftar tugas/ujian yang terdaftar untuk kelas tersebut
|
|
final tugasQuery = await firestore
|
|
.collection('tugas')
|
|
.where('kelas', isEqualTo: className)
|
|
.where('tahun_ajaran', isEqualTo: academicYear)
|
|
.get();
|
|
|
|
// 2. Ambil semua hasil penilaian milik siswa tersebut
|
|
final penilaianQuery = await firestore
|
|
.collection('penilaian')
|
|
.where('siswa_id', isEqualTo: studentId)
|
|
.where('kelas', isEqualTo: className)
|
|
.where('tahun_ajaran', isEqualTo: academicYear)
|
|
.get();
|
|
|
|
// Mapping nilai siswa berdasarkan id_tugas dari koleksi penilaian
|
|
Map<String, double> nilaiSiswaMap = {};
|
|
for (var doc in penilaianQuery.docs) {
|
|
final data = doc.data();
|
|
String idTugas = (data['id_tugas'] ?? "").toString();
|
|
if (idTugas.isNotEmpty) {
|
|
nilaiSiswaMap[idTugas] = (data['nilai'] as num?)?.toDouble() ?? 0.0;
|
|
}
|
|
}
|
|
|
|
Map<String, List<Assignment>> assignmentsMap = {};
|
|
Map<String, double> utsMap = {};
|
|
Map<String, double> uasMap = {};
|
|
|
|
// 3. Proses penggabungan data
|
|
for (var doc in tugasQuery.docs) {
|
|
final String docIdTugas = doc.id;
|
|
final data = doc.data();
|
|
String subject = (data['mapel'] ?? "Lainnya").toString().trim();
|
|
String title = (data['judul'] ?? "Tugas").toString().trim();
|
|
String tipe = data['tipe_ujian'] ?? "Tugas";
|
|
|
|
// Cek apakah tugas/ujian ini sudah ada record-nya di koleksi penilaian
|
|
bool isGraded = nilaiSiswaMap.containsKey(docIdTugas);
|
|
double skor = nilaiSiswaMap[docIdTugas] ?? 0.0;
|
|
|
|
if (tipe == 'UTS') {
|
|
// Hanya masukkan jika sudah dinilai, agar tidak merusak rata-rata jika data belum ada
|
|
if (isGraded) utsMap[subject] = skor;
|
|
} else if (tipe == 'UAS') {
|
|
// Perbaikan Bug: UAS terbaca meski nilai 0 selama datanya ada di koleksi penilaian
|
|
if (isGraded) uasMap[subject] = skor;
|
|
} else {
|
|
assignmentsMap.putIfAbsent(subject, () => []);
|
|
assignmentsMap[subject]!.add(
|
|
Assignment(
|
|
name: title,
|
|
score: skor,
|
|
status: isGraded ? "Selesai" : "Belum Dinilai",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// 4. Transformasi ke Model AcademicScore
|
|
List<AcademicScore> academicScores = [];
|
|
Set<String> allSubjects = {
|
|
...assignmentsMap.keys,
|
|
...utsMap.keys,
|
|
...uasMap.keys,
|
|
};
|
|
|
|
for (var subject in allSubjects) {
|
|
academicScores.add(
|
|
AcademicScore(
|
|
subject: subject,
|
|
assignments: assignmentsMap[subject] ?? [],
|
|
midExamScore: utsMap[subject], // Akan bernilai null jika belum dinilai
|
|
finalExamScore: uasMap[subject], // Akan bernilai null jika belum dinilai
|
|
),
|
|
);
|
|
}
|
|
|
|
academicScores.sort((a, b) => a.subject.compareTo(b.subject));
|
|
return academicScores;
|
|
}
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final student = ModalRoute.of(context)!.settings.arguments as StudentModel;
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppColors.backgroundWhite,
|
|
appBar: AppBar(
|
|
title: Text(
|
|
"Nilai Akademik - Kelas ${student.className}",
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
color: AppColors.primaryBlue,
|
|
child: FutureBuilder<List<AcademicScore>>(
|
|
future: _academicDataFuture,
|
|
builder: (context, snapshot) {
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
|
|
// Jika terjadi error, bungkus dalam ListView agar bisa pull-to-refresh
|
|
if (snapshot.hasError) {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
SizedBox(height: 100.h),
|
|
Center(child: Text("Terjadi kesalahan: ${snapshot.error}")),
|
|
],
|
|
);
|
|
}
|
|
|
|
final scores = snapshot.data ?? [];
|
|
|
|
// Jika data kosong, bungkus dalam ListView agar bisa pull-to-refresh
|
|
if (scores.isEmpty) {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
SizedBox(height: 100.h),
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons
|
|
.menu_book_rounded, // Ikon buku sesuai konteks akademik
|
|
size: 60.sp,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
SizedBox(height: 16.h),
|
|
Text(
|
|
"Belum Ada Data Nilai",
|
|
style: TextStyle(color: Colors.grey.shade500),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
// Physics ini penting agar list selalu bisa ditarik walau item sedikit
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
padding: EdgeInsets.all(20.w),
|
|
itemCount: scores.length,
|
|
itemBuilder:
|
|
(context, index) =>
|
|
AcademicSubjectCard(scoreData: scores[index]),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|