TIF_E41221079/lib/features/academic/academic_screen.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]),
);
},
),
),
);
}
}