import 'package:flutter/material.dart'; import 'package:forward_chaining_man_app/app/controllers/developer_controller.dart'; import 'package:forward_chaining_man_app/app/views/developer/page/page_developer_viewer.dart'; import 'package:forward_chaining_man_app/app/views/page_intro.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:fl_chart/fl_chart.dart'; class TeacherDashboardController extends GetxController { // ========== INSTANCE & DEPENDENCIES ========== final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance; // ========== STATE VARIABLES ========== // Loading state var isLoading = true.obs; // Developer mode final RxBool isDeveloperMode = developerMode.obs; // User data var teacherName = ''.obs; // School data var schoolId = ''.obs; // Results data var studentResults = [].obs; var filteredResults = [].obs; // Filter states var selectedCategoryFilter = 'all'.obs; var selectedDateFilter = 'all_time'.obs; var selectedFilter = 'all'.obs; var selectedClass = 'Semua Kelas'.obs; var availableClasses = ['Semua Kelas'].obs; // Custom date filter var selectedStartDate = DateTime.now().obs; var selectedEndDate = DateTime.now().obs; var isCustomDateSelected = false.obs; // Statistics var stats = { 'totalStudents': 0, 'careerRecommendations': 0, 'studyRecommendations': 0, }.obs; // Variabel untuk pagination var currentPage = 0.obs; var itemsPerPage = 10.obs; // Jumlah item per halaman var totalPages = 0.obs; var paginatedResults = [].obs; // Fungsi untuk mengatur jumlah item per halaman void setItemsPerPage(int count) { itemsPerPage.value = count; updatePagination(); } // Fungsi untuk pindah ke halaman tertentu void goToPage(int page) { if (page >= 0 && page < totalPages.value) { currentPage.value = page; updatePagination(); } } // Fungsi untuk halaman selanjutnya void nextPage() { if (currentPage.value < totalPages.value - 1) { currentPage.value++; updatePagination(); } } // Fungsi untuk halaman sebelumnya void previousPage() { if (currentPage.value > 0) { currentPage.value--; updatePagination(); } } void updatePagination() { if (filteredResults.isEmpty) { paginatedResults.clear(); totalPages.value = 0; return; } totalPages.value = (filteredResults.length / itemsPerPage.value).ceil(); // Pastikan halaman saat ini valid if (currentPage.value >= totalPages.value) { currentPage.value = totalPages.value - 1; } int startIndex = currentPage.value * itemsPerPage.value; int endIndex = startIndex + itemsPerPage.value; // Pastikan endIndex tidak melebihi panjang array if (endIndex > filteredResults.length) { endIndex = filteredResults.length; } // Update data yang akan ditampilkan paginatedResults.value = filteredResults.sublist(startIndex, endIndex); } // Cache untuk student class mapping - user ID to class mapping final Map _studentClassCache = {}; // Cache untuk student school mapping - user ID to school ID mapping final Map _studentSchoolCache = {}; // ========== LIFECYCLE METHODS ========== @override void onInit() { super.onInit(); loadTeacherData(); loadStudentResults(); loadAvailableClasses(); } // ========== DATA LOADING METHODS ========== Future loadTeacherData() async { try { final User? currentUser = _auth.currentUser; if (currentUser != null) { // Tetap menggunakan koleksi teachers seperti yang diminta final teacherDoc = await _firestore.collection('teachers').doc(currentUser.uid).get(); if (teacherDoc.exists) { teacherName.value = teacherDoc.data()?['name'] ?? 'Guru'; // Ambil schoolId dari data guru jika ada schoolId.value = teacherDoc.data()?['schoolId'] ?? ''; } } } catch (e) { print('Error loading teacher data: $e'); } } Future loadStudentResults() async { try { isLoading.value = true; // Get the teacher's school ID final prefs = await SharedPreferences.getInstance(); final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; if (teacherSchoolId.isEmpty) { // If no school ID, handle the error Get.snackbar( 'Error', 'Tidak dapat menentukan sekolah Anda', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, snackPosition: SnackPosition.BOTTOM, ); isLoading.value = false; return; } // Preload student class cache await _preloadStudentClassCache(); // Jika filter kelas sudah dipilih, gunakan query where QuerySnapshot querySnapshot; if (selectedClass.value != 'Semua Kelas') { // Dapatkan semua ID siswa dengan kelas yang dipilih List studentIds = await _getStudentIdsByClass(selectedClass.value); if (studentIds.isEmpty) { // Jika tidak ada siswa dengan kelas ini studentResults.value = []; filteredResults.value = []; stats['totalStudents'] = 0; stats['careerRecommendations'] = 0; stats['studyRecommendations'] = 0; isLoading.value = false; return; } // Batasi ukuran chunk karena Firestore hanya mendukung hingga 10 where-in dalam satu query List allResults = []; for (int i = 0; i < studentIds.length; i += 10) { int end = (i + 10 < studentIds.length) ? i + 10 : studentIds.length; List chunk = studentIds.sublist(i, end); // Query from the school-specific recommendation_history subcollection QuerySnapshot chunkResult = await _firestore .collection('schools') .doc(teacherSchoolId) .collection('recommendation_history') .where('userId', whereIn: chunk) .orderBy('timestamp', descending: true) .get(); allResults.addAll(chunkResult.docs); } // Sort the combined results by timestamp allResults.sort((a, b) { Timestamp aTimestamp = (a.data() as Map)['timestamp'] as Timestamp; Timestamp bTimestamp = (b.data() as Map)['timestamp'] as Timestamp; return bTimestamp.compareTo(aTimestamp); }); studentResults.value = allResults; } else { // Terapkan pagination jika perlu - query from school-specific collection querySnapshot = await _firestore .collection('schools') .doc(teacherSchoolId) .collection('recommendation_history') .orderBy('timestamp', descending: true) .limit(100) // Batasi ke 100 hasil terakhir untuk mencegah lag .get(); studentResults.value = querySnapshot.docs; } // Apply category and date filters _applyFiltersWithoutClass(); // Update stats stats['totalStudents'] = studentResults.length; stats['careerRecommendations'] = studentResults.where((doc) { final data = doc.data() as Map; return data['isKerja'] == true; }).length; stats['studyRecommendations'] = studentResults.where((doc) { final data = doc.data() as Map; return data['isKerja'] == false; }).length; } catch (e) { print('Error loading student results: $e'); Get.snackbar( 'Error', 'Gagal memuat data siswa: ${e.toString()}', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, snackPosition: SnackPosition.BOTTOM, ); } finally { isLoading.value = false; } } // Metode baru untuk memuat class cache - diperbarui untuk struktur firestore baru Future _preloadStudentClassCache() async { try { if (_studentClassCache.isEmpty) { // Dapatkan shared preferences untuk school ID final prefs = await SharedPreferences.getInstance(); final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; if (teacherSchoolId.isEmpty) { // Jika tidak ada schoolId, coba ambil dari semua sekolah final QuerySnapshot schoolsSnapshot = await _firestore.collection('schools').get(); for (var schoolDoc in schoolsSnapshot.docs) { // Ambil semua siswa dari setiap sekolah final QuerySnapshot studentsSnapshot = await schoolDoc.reference.collection('students').get(); for (var doc in studentsSnapshot.docs) { final data = doc.data() as Map; if (data.containsKey('class') && data['class'] != null) { _studentClassCache[doc.id] = data['class'].toString(); _studentSchoolCache[doc.id] = schoolDoc.id; } } } } else { // Jika ada schoolId, ambil hanya dari sekolah tersebut final QuerySnapshot studentsSnapshot = await _firestore .collection('schools') .doc(teacherSchoolId) .collection('students') .get(); for (var doc in studentsSnapshot.docs) { final data = doc.data() as Map; if (data.containsKey('class') && data['class'] != null) { _studentClassCache[doc.id] = data['class'].toString(); _studentSchoolCache[doc.id] = teacherSchoolId; } } } } } catch (e) { print('Error preloading student class cache: $e'); } } // Metode untuk mendapatkan student IDs berdasarkan kelas - diperbarui untuk struktur firestore baru Future> _getStudentIdsByClass(String className) async { List studentIds = []; try { // Gunakan cache yang sudah kita buat if (_studentClassCache.isNotEmpty) { _studentClassCache.forEach((userId, userClass) { if (userClass == className) { studentIds.add(userId); } }); } else { // Jika cache kosong, kita perlu query database // Dapatkan shared preferences untuk school ID final prefs = await SharedPreferences.getInstance(); final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; if (teacherSchoolId.isEmpty) { // Jika tidak ada schoolId, cari di semua sekolah final QuerySnapshot schoolsSnapshot = await _firestore.collection('schools').get(); for (var schoolDoc in schoolsSnapshot.docs) { final QuerySnapshot studentsSnapshot = await schoolDoc.reference .collection('students') .where('class', isEqualTo: className) .get(); for (var doc in studentsSnapshot.docs) { studentIds.add(doc.id); // Update cache _studentClassCache[doc.id] = className; _studentSchoolCache[doc.id] = schoolDoc.id; } } } else { // Jika ada schoolId, ambil hanya dari sekolah tersebut final QuerySnapshot studentsSnapshot = await _firestore .collection('schools') .doc(teacherSchoolId) .collection('students') .where('class', isEqualTo: className) .get(); studentIds = studentsSnapshot.docs.map((doc) => doc.id).toList(); // Update cache for (var doc in studentsSnapshot.docs) { _studentClassCache[doc.id] = className; _studentSchoolCache[doc.id] = teacherSchoolId; } } } } catch (e) { print('Error getting student IDs by class: $e'); } return studentIds; } Future loadAvailableClasses() async { try { if (_studentClassCache.isNotEmpty) { // Gunakan data dari cache yang sudah ada final Set classes = {'Semua Kelas'}; classes.addAll(_studentClassCache.values); availableClasses.value = classes.toList()..sort(); } else { // Dapatkan shared preferences untuk school ID final prefs = await SharedPreferences.getInstance(); final teacherSchoolId = prefs.getString('school_id') ?? schoolId.value; // Extract unique class values final Set classes = {'Semua Kelas'}; if (teacherSchoolId.isEmpty) { // Jika tidak ada schoolId, ambil dari semua sekolah final QuerySnapshot schoolsSnapshot = await _firestore.collection('schools').get(); for (var schoolDoc in schoolsSnapshot.docs) { final QuerySnapshot studentsSnapshot = await schoolDoc.reference.collection('students').get(); for (var doc in studentsSnapshot.docs) { final data = doc.data() as Map; if (data.containsKey('class') && data['class'] != null) { classes.add(data['class'].toString()); // Update cache sambil kita ambil data _studentClassCache[doc.id] = data['class'].toString(); _studentSchoolCache[doc.id] = schoolDoc.id; } } } } else { // Jika ada schoolId, ambil hanya dari sekolah tersebut final QuerySnapshot studentsSnapshot = await _firestore .collection('schools') .doc(teacherSchoolId) .collection('students') .get(); for (var doc in studentsSnapshot.docs) { final data = doc.data() as Map; if (data.containsKey('class') && data['class'] != null) { classes.add(data['class'].toString()); // Update cache sambil kita ambil data _studentClassCache[doc.id] = data['class'].toString(); _studentSchoolCache[doc.id] = teacherSchoolId; } } } availableClasses.value = classes.toList()..sort(); } } catch (e) { print('Error loading available classes: $e'); } } // ========== FILTERING METHODS ========== // Metode dioptimalkan untuk filter tanpa perlu loading ulang data kelas void _applyFiltersWithoutClass() { List result = studentResults; // Apply category filter if (selectedCategoryFilter.value != 'all') { bool isKerja = selectedCategoryFilter.value == 'career'; result = result.where((doc) { final data = doc.data() as Map; return data['isKerja'] == isKerja; }).toList(); } // Apply date filter final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final firstDayOfMonth = DateTime(now.year, now.month, 1); if (isCustomDateSelected.value) { final start = DateTime(selectedStartDate.value.year, selectedStartDate.value.month, selectedStartDate.value.day); final end = DateTime(selectedEndDate.value.year, selectedEndDate.value.month, selectedEndDate.value.day, 23, 59, 59); result = result.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date.isAfter(start.subtract(const Duration(seconds: 1))) && date.isBefore(end.add(const Duration(seconds: 1))); }).toList(); } else if (selectedDateFilter.value == 'today') { result = result.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date.isAfter(today.subtract(const Duration(seconds: 1))); }).toList(); } else if (selectedDateFilter.value == 'this_month') { result = result.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date .isAfter(firstDayOfMonth.subtract(const Duration(seconds: 1))); }).toList(); } filteredResults.value = result; updatePagination(); } // Master filter application - sekarang hanya memicu reload jika filter kelas berubah Future applyAllFilters() async { // Jika filter kelas berubah, kita perlu memuat ulang data if (selectedClass.value != 'Semua Kelas') { await loadStudentResults(); } else { _applyFiltersWithoutClass(); } } // Filter triggers void filterByCategory(String category) { selectedCategoryFilter.value = category; _applyFiltersWithoutClass(); } // Filter by date (all_time, today, this_month) void filterByDate(String dateFilter) { selectedDateFilter.value = dateFilter; _applyFiltersWithoutClass(); } // Filter by class - ini memerlukan reload karena mengubah query dasar void filterByClass(String classFilter) { selectedClass.value = classFilter; loadStudentResults(); // Reload data dengan query baru } // Custom date filter methods void setCustomDateRange(DateTime start, DateTime end) { selectedStartDate.value = start; selectedEndDate.value = end; isCustomDateSelected.value = true; selectedDateFilter.value = 'custom'; _applyFiltersWithoutClass(); } void resetCustomDateFilter() { isCustomDateSelected.value = false; } // Legacy filter - tetap dipertahankan untuk kompatibilitas void filterResults(String filter) { selectedFilter.value = filter; switch (filter) { case 'career': filteredResults.value = studentResults.where((doc) => doc['isKerja'] == true).toList(); break; case 'study': filteredResults.value = studentResults.where((doc) => doc['isKerja'] == false).toList(); break; case 'all': default: filteredResults.value = studentResults; break; } updatePagination(); } // ========== LEGACY FILTER METHODS - DIPERTAHANKAN UNTUK KOMPATIBILITAS ========== List _applyCategoryFilter( List docs, String filter) { switch (filter) { case 'career': return docs.where((doc) { final data = doc.data() as Map; return data['isKerja'] == true; }).toList(); case 'study': return docs.where((doc) { final data = doc.data() as Map; return data['isKerja'] == false; }).toList(); case 'all': default: return docs; } } List _applyDateFilter( List docs, String filter) { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final firstDayOfMonth = DateTime(now.year, now.month, 1); switch (filter) { case 'today': return docs.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date.isAfter(today.subtract(const Duration(seconds: 1))); }).toList(); case 'this_month': return docs.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date .isAfter(firstDayOfMonth.subtract(const Duration(seconds: 1))); }).toList(); case 'all_time': default: return docs; } } List _applyCustomDateFilter( List docs, DateTime startDate, DateTime endDate) { final start = DateTime(startDate.year, startDate.month, startDate.day); final end = DateTime(endDate.year, endDate.month, endDate.day, 23, 59, 59); return docs.where((doc) { final data = doc.data() as Map; if (data['timestamp'] == null) return false; final timestamp = data['timestamp'] as Timestamp; final date = timestamp.toDate(); return date.isAfter(start.subtract(const Duration(seconds: 1))) && date.isBefore(end.add(const Duration(seconds: 1))); }).toList(); } // Optimasi: pakai cache untuk mencegah query ke database untuk setiap dokumen - diperbarui untuk struktur baru Future> _applyClassFilter( List docs, String filter) async { if (filter == 'Semua Kelas') { return docs; } List filteredDocs = []; for (var doc in docs) { final data = doc.data() as Map; final userId = data['userId']; if (userId == null) continue; // Gunakan cache terlebih dahulu if (_studentClassCache.containsKey(userId)) { if (_studentClassCache[userId] == filter) { filteredDocs.add(doc); } continue; } // Jika tidak ada di cache, coba query database try { // Dapatkan schoolId dari cache atau shared preferences String? studentSchoolId = _studentSchoolCache[userId]; if (studentSchoolId == null) { // Jika tidak ada di cache, coba cari di semua sekolah bool found = false; final schoolsSnapshot = await _firestore.collection('schools').get(); for (var schoolDoc in schoolsSnapshot.docs) { final studentDoc = await schoolDoc.reference .collection('students') .doc(userId) .get(); if (studentDoc.exists) { final studentData = studentDoc.data() as Map; final studentClass = studentData['class']; // Update cache _studentClassCache[userId] = studentClass ?? ''; _studentSchoolCache[userId] = schoolDoc.id; if (studentClass == filter) { filteredDocs.add(doc); } found = true; break; } } if (!found) { // Siswa tidak ditemukan di database continue; } } else { // Jika ada schoolId di cache, gunakan untuk query langsung final studentDoc = await _firestore .collection('schools') .doc(studentSchoolId) .collection('students') .doc(userId) .get(); if (!studentDoc.exists) continue; final studentData = studentDoc.data() as Map; final studentClass = studentData['class']; // Update cache _studentClassCache[userId] = studentClass ?? ''; if (studentClass == filter) { filteredDocs.add(doc); } } } catch (e) { print('Error fetching student class: $e'); } } return filteredDocs; } // ========== UTILITY METHODS ========== String formatTimestamp(dynamic timestamp) { if (timestamp == null) return 'Tanggal tidak tersedia'; try { DateTime dateTime = (timestamp as Timestamp).toDate(); return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}'; } catch (e) { return 'Format tanggal error'; } } void toggleDeveloperMode(bool value) { isDeveloperMode.value = value; developerMode = value; } // ========== CHART DATA METHODS ========== List getPieChartData() { return [ PieChartSectionData( value: stats['careerRecommendations']!.toDouble(), title: 'Karir', color: Colors.deepPurple.shade700, radius: 60, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), PieChartSectionData( value: stats['studyRecommendations']!.toDouble(), title: 'Kuliah', color: Colors.indigo.shade900, radius: 60, titleStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), ]; } // ========== NAVIGATION METHODS ========== void viewStudentDetail(DocumentSnapshot doc) { Get.to(() => StudentResultDetailPage(document: doc)); } Future signOut() async { try { await _auth.signOut(); // Clear shared preferences final prefs = await SharedPreferences.getInstance(); await prefs.clear(); // Navigate to login selection page Get.off(IntroPage()); } catch (e) { print('Error signing out: $e'); Get.snackbar( 'Error', 'Gagal keluar: ${e.toString()}', backgroundColor: Colors.red.shade100, colorText: Colors.red.shade800, snackPosition: SnackPosition.BOTTOM, ); } } } class TeacherDashboardPage extends StatelessWidget { const TeacherDashboardPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final controller = Get.put(TeacherDashboardController()); final theme = Theme.of(context); return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.blue.shade800, Colors.indigo.shade900, ], ), ), child: SafeArea( child: Column( children: [ _buildAppBar(controller), // Developer Mode Card with improved visual cues Expanded( child: Container( margin: const EdgeInsets.only(top: 16), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: const BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, -5), ), ], ), child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), child: Obx(() => controller.isLoading.value ? _buildLoadingView() : _buildDashboardContent(controller, theme)), ), ), ), ], ), ), ), ); } Widget _buildAppBar(TeacherDashboardController controller) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), child: Row( children: [ Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), padding: const EdgeInsets.all(8), child: Icon( Icons.psychology, size: 30, color: Colors.blue.shade700, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'EduGuide', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white, ), ), Obx(() => Text( 'Selamat datang, ${controller.teacherName.value}', style: const TextStyle( fontSize: 14, color: Colors.white70, ), )), ], ), ), Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: IconButton( onPressed: () { // Refresh data controller.loadStudentResults(); }, icon: const Icon(Icons.refresh, color: Colors.white), tooltip: 'Refresh Data', ), ), const SizedBox(width: 8), Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: IconButton( onPressed: () { // Show sign out confirmation Get.dialog( AlertDialog( title: const Text('Konfirmasi'), content: const Text('Apakah Anda yakin ingin keluar?'), actions: [ TextButton( onPressed: () => Get.back(), child: Text( 'Batal', style: TextStyle(color: Colors.grey.shade700), ), ), ElevatedButton( onPressed: () { Get.back(); controller.signOut(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade700, ), child: const Text('Keluar', style: TextStyle(color: Colors.white)), ), ], ), ); }, icon: const Icon(Icons.logout, color: Colors.white), tooltip: 'Keluar', ), ), ], ), ); } Widget _buildLoadingView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.blue.shade700), ), const SizedBox(height: 16), Text( 'Memuat data...', style: TextStyle( fontSize: 16, color: Colors.blue.shade700, ), ), ], ), ); } Widget _buildDashboardContent( TeacherDashboardController controller, ThemeData theme) { return RefreshIndicator( onRefresh: controller.loadStudentResults, color: Colors.blue.shade700, child: ListView( padding: const EdgeInsets.all(16), children: [ Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( children: [ Icon(Icons.analytics, size: 18, color: Colors.blue.shade800), const SizedBox(width: 8), Text( 'Akses Data Aplikasi', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), ], ), ), const SizedBox(height: 12), AnimatedContainer( duration: const Duration(milliseconds: 300), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(20), border: Border.all( color: Colors.grey.shade300, width: 1, ), boxShadow: [ BoxShadow( color: Colors.indigo.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.orange.shade100, borderRadius: BorderRadius.circular(10), ), child: Icon( Icons.code, size: 20, color: Colors.orange.shade800, ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Mode Developer', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.indigo.shade800, ), ), const SizedBox(height: 2), const Text( 'Akses data & model AI', style: TextStyle( fontSize: 12, color: Colors.black54, ), ), ], ), ), ], ), const SizedBox(height: 12), const Text( 'Aktifkan mode pengembang untuk melihat data dan validasi model forward chaining yang digunakan dalam sistem rekomendasi.', style: TextStyle( fontSize: 13, color: Colors.black54, height: 1.4, ), ), const SizedBox(height: 12), Obx(() => Container( decoration: BoxDecoration( color: controller.isDeveloperMode.value ? Colors.indigo.shade50 : Colors.grey.shade100, borderRadius: BorderRadius.circular(12), ), child: SwitchListTile( title: Text( controller.isDeveloperMode.value ? 'Mode Pengembang Aktif' : 'Mode Pengembang Nonaktif', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: controller.isDeveloperMode.value ? Colors.indigo.shade800 : Colors.black54, ), ), value: controller.isDeveloperMode.value, onChanged: (value) => controller.toggleDeveloperMode(value), activeColor: Colors.indigo, activeTrackColor: Colors.indigo.shade300, inactiveThumbColor: Colors.grey.shade400, inactiveTrackColor: Colors.grey.shade300, secondary: Icon( controller.isDeveloperMode.value ? Icons.visibility : Icons.visibility_off, color: controller.isDeveloperMode.value ? Colors.indigo : Colors.grey.shade500, size: 20, ), dense: true, ), )), const SizedBox(height: 12), // Developer mode button with attention-grabbing styling Obx(() => controller.isDeveloperMode.value ? Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.orange.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: ElevatedButton( onPressed: () => Get.to(() => const DevDataViewerPage()), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade600, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16), minimumSize: const Size(double.infinity, 56), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), elevation: 0, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.data_array, size: 22), SizedBox(width: 12), Text( 'Data Viewer', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ], ), ), ) : const SizedBox.shrink()), ], ), ), ), ], ), const SizedBox(height: 12), _buildStatisticsCards(controller), const SizedBox(height: 24), _buildFilterButtons(controller), const SizedBox(height: 24), _buildStudentList(controller), ], ), ); } Widget _buildStatisticsCards(TeacherDashboardController controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan padding yang konsisten Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( children: [ Icon(Icons.analytics, size: 18, color: Colors.blue.shade800), const SizedBox(width: 8), Text( 'Statistik Hasil Kuisioner', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), ], ), ), const SizedBox(height: 12), // Row untuk Chart dan Stat Cards LayoutBuilder( builder: (context, constraints) { // Tentukan ukuran chart optimal berdasarkan lebar yang tersedia final isWideScreen = constraints.maxWidth > 600; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Chart Container - flex yang lebih besar pada layar lebar Expanded( flex: isWideScreen ? 3 : 2, child: Container( height: 210, // Sedikit ditambah untuk menampung legend padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Chart title Row( children: [ Icon(Icons.pie_chart, size: 14, color: Colors.blue.shade800), const SizedBox(width: 6), Expanded( child: Text( 'Distribusi Jenis Rekomendasi', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), // Chart area Expanded( child: Obx(() { final hasData = controller.stats['totalStudents']! > 0; return hasData ? PieChartWidget(controller: controller) : const EmptyChartMessage(); }), ), // Legend - hanya ditampilkan jika ada data Obx(() { final hasData = controller.stats['totalStudents']! > 0; return hasData ? Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildLegendItem( 'Karir', Colors.blue.shade700), const SizedBox(width: 16), _buildLegendItem( 'Kuliah', Colors.indigo.shade800), ], ), ) : const SizedBox.shrink(); }), ], ), ), ), const SizedBox(width: 12), // Stat Cards Container Expanded( flex: isWideScreen ? 2 : 3, // Sesuaikan flex berdasarkan layar child: Container( height: 210, // Tinggi yang sama dengan chart untuk konsistensi child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Stat card untuk Total Siswa Expanded( child: _buildStatCard( title: 'Total Siswa', value: controller.stats['totalStudents'].toString(), icon: Icons.people, color: Colors.blue.shade800, ), ), const SizedBox(height: 8), // Stat card untuk Rekomendasi Karir Expanded( child: _buildStatCard( title: 'Rekomendasi Karir', value: controller.stats['careerRecommendations'] .toString(), icon: Icons.work, color: Colors.blue.shade700, ), ), const SizedBox(height: 8), // Stat card untuk Rekomendasi Kuliah Expanded( child: _buildStatCard( title: 'Rekomendasi Kuliah', value: controller.stats['studyRecommendations'] .toString(), icon: Icons.school, color: Colors.indigo.shade800, ), ), ], ), ), ), ], ); }, ), ], ); } Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 10, height: 10, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 11, color: Colors.grey.shade700, ), ), ], ); } Widget _buildStatCard({ required String title, required String value, required IconData icon, required Color color, }) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 3), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Icon( icon, color: color, size: 16, ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( title, style: TextStyle( fontSize: 11, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( value, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: color, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ); } Widget _buildFilterButtons(TeacherDashboardController controller) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.filter_list, size: 20, color: Colors.blue.shade800), const SizedBox(width: 8), Text( 'Daftar Hasil Kuisioner Siswa', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), ], ), const SizedBox(height: 16), // Category Filters Text( 'Filter Kategori:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), const SizedBox(height: 8), Row( children: [ _buildFilterButton( label: 'Semua', value: 'all', controller: controller, icon: Icons.list_alt, filterType: 'category', ), const SizedBox(width: 12), _buildFilterButton( label: 'Karir', value: 'career', controller: controller, icon: Icons.work, filterType: 'category', ), const SizedBox(width: 12), _buildFilterButton( label: 'Kuliah', value: 'study', controller: controller, icon: Icons.school, filterType: 'category', ), ], ), const SizedBox(height: 20), // Date Filters Text( 'Filter Waktu:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), const SizedBox(height: 8), Row( children: [ _buildFilterButton( label: 'Semua Waktu', value: 'all_time', controller: controller, icon: Icons.av_timer, filterType: 'date', ), const SizedBox(width: 12), _buildFilterButton( label: 'Hari Ini', value: 'today', controller: controller, icon: Icons.today, filterType: 'date', ), const SizedBox(width: 12), _buildFilterButton( label: 'Bulan Ini', value: 'this_month', controller: controller, icon: Icons.date_range, filterType: 'date', ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: _buildDatePickerButton(controller), ), ], ), Obx(() => controller.isCustomDateSelected.value ? Padding( padding: const EdgeInsets.only(top: 8), child: Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.blue.shade200), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.date_range, size: 16, color: Colors.blue), const SizedBox(width: 8), Text( '${controller.selectedStartDate.value.day}/${controller.selectedStartDate.value.month}/${controller.selectedStartDate.value.year} - ' '${controller.selectedEndDate.value.day}/${controller.selectedEndDate.value.month}/${controller.selectedEndDate.value.year}', style: TextStyle( fontSize: 12, color: Colors.blue.shade800, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 8), InkWell( onTap: () { controller.resetCustomDateFilter(); controller.filterByDate('all_time'); }, child: Icon(Icons.close, size: 16, color: Colors.red.shade700), ), ], ), ), ) : const SizedBox.shrink()), const SizedBox(height: 20), // Class Filter DropDown Text( 'Filter Kelas:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.blue.shade200, width: 1), ), child: Obx(() => DropdownButtonHideUnderline( child: DropdownButton( value: controller.selectedClass.value, isExpanded: true, icon: Icon(Icons.keyboard_arrow_down, color: Colors.blue.shade700), elevation: 16, style: TextStyle(color: Colors.blue.shade700), onChanged: (String? newValue) { controller.filterByClass(newValue!); }, items: controller.availableClasses .map>((String value) { return DropdownMenuItem( value: value, child: Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ); }).toList(), ), )), ), ], ); } Widget _buildDatePickerButton(TeacherDashboardController controller) { return ElevatedButton( onPressed: () async { // Tampilkan date range picker final DateTimeRange? picked = await showDateRangePicker( context: Get.context!, firstDate: DateTime(2020), lastDate: DateTime.now(), initialDateRange: DateTimeRange( start: controller.selectedStartDate.value, end: controller.selectedEndDate.value, ), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: ColorScheme.light( primary: Colors.blue.shade700, onPrimary: Colors.white, onSurface: Colors.black, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: Colors.blue.shade700, ), ), ), child: child!, ); }, ); if (picked != null) { controller.setCustomDateRange(picked.start, picked.end); } }, style: ElevatedButton.styleFrom( backgroundColor: controller.isCustomDateSelected.value ? Colors.blue.shade700 : Colors.white, foregroundColor: controller.isCustomDateSelected.value ? Colors.white : Colors.blue.shade700, elevation: controller.isCustomDateSelected.value ? 4 : 1, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), side: BorderSide( color: controller.isCustomDateSelected.value ? Colors.transparent : Colors.blue.shade200, width: 1, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.calendar_month, size: 16), const SizedBox(width: 8), const Text( 'Pilih Tanggal', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), ), ], ), ); } Widget _buildFilterButton({ required String label, required String value, required TeacherDashboardController controller, required IconData icon, required String filterType, }) { return Obx(() { final isSelected = filterType == 'category' ? controller.selectedCategoryFilter.value == value : controller.selectedDateFilter.value == value; // Jika filter tanggal kustom aktif, nonaktifkan tombol filter tanggal lainnya final isDateDisabled = filterType == 'date' && controller.isCustomDateSelected.value && value != 'custom'; return Expanded( child: ElevatedButton( onPressed: isDateDisabled ? null : () { if (filterType == 'category') { controller.filterByCategory(value); } else { controller.resetCustomDateFilter(); controller.filterByDate(value); } }, style: ElevatedButton.styleFrom( backgroundColor: isSelected ? Colors.blue.shade700 : Colors.white, foregroundColor: isSelected ? Colors.white : Colors.blue.shade700, disabledBackgroundColor: Colors.grey.shade200, disabledForegroundColor: Colors.grey.shade600, elevation: isSelected ? 4 : 1, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), side: BorderSide( color: isSelected || isDateDisabled ? Colors.transparent : Colors.blue.shade200, width: 1, ), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 16), const SizedBox(width: 8), Flexible( child: Text( label, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); }); } Widget _buildStudentList(TeacherDashboardController controller) { return Obx(() { if (controller.filteredResults.isEmpty) { return Container( height: 200, alignment: Alignment.center, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.search_off, size: 48, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( 'Belum ada data hasil kuisioner', style: TextStyle( fontSize: 16, color: Colors.grey.shade600, ), ), ], ), ); } // Panggil updatePagination jika belum dipanggil if (controller.paginatedResults.isEmpty) { controller.updatePagination(); } return Column( children: [ // Informasi pagination Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Menampilkan ${controller.paginatedResults.length} dari ${controller.filteredResults.length} hasil', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), // Dropdown untuk mengatur jumlah item per halaman Row( children: [ Text( 'Tampilkan: ', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(4), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: controller.itemsPerPage.value, items: [5, 10, 20, 50].map((int value) { return DropdownMenuItem( value: value, child: Text('$value'), ); }).toList(), onChanged: (int? newValue) { if (newValue != null) { controller.setItemsPerPage(newValue); } }, ), ), ), ], ), ], ), ), // Daftar siswa ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: controller.paginatedResults.length, itemBuilder: (context, index) { final doc = controller.paginatedResults[index]; final data = doc.data() as Map; final isKerja = data['isKerja'] ?? false; final recommendations = List>.from( data['recommendations'] ?? []); // Get top recommendation String topRecommendation = 'Tidak ada'; if (recommendations.isNotEmpty) { topRecommendation = recommendations[0]['title'] ?? 'Tidak ada'; } return Card( margin: const EdgeInsets.only(bottom: 16), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: InkWell( onTap: () => controller.viewStudentDetail(doc), borderRadius: BorderRadius.circular(15), child: Container( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: isKerja ? Colors.blue.shade50 : Colors.indigo.shade50, borderRadius: BorderRadius.circular(12), ), child: Icon( isKerja ? Icons.work : Icons.school, color: isKerja ? Colors.blue.shade400 : Colors.indigo.shade400, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data['userName'] ?? data['userEmail'] ?? 'Siswa', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Text( controller .formatTimestamp(data['timestamp']), style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: isKerja ? Colors.blue.shade100 : Colors.indigo.shade100, borderRadius: BorderRadius.circular(20), ), child: Text( isKerja ? 'Karir' : 'Kuliah', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: isKerja ? Colors.blue.shade700 : Colors.indigo.shade700, ), ), ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 8), Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Rekomendasi Utama', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), const SizedBox(height: 4), Text( topRecommendation, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), const SizedBox(width: 16), Container( decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: IconButton( onPressed: () => controller.viewStudentDetail(doc), icon: const Icon(Icons.visibility), color: Colors.blue.shade700, tooltip: 'Lihat Detail', ), ), ], ), ], ), ), ), ); }, ), // Kontrol pagination yang diperbaiki if (controller.totalPages.value > 1) Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Tombol Previous IconButton( onPressed: controller.currentPage.value > 0 ? () => controller.previousPage() : null, icon: const Icon(Icons.chevron_left), color: Colors.blue.shade700, disabledColor: Colors.grey.shade400, ), // Indikator halaman dengan smart pagination Expanded( child: Center( child: _buildSmartPagination(controller), ), ), // Tombol Next IconButton( onPressed: controller.currentPage.value < controller.totalPages.value - 1 ? () => controller.nextPage() : null, icon: const Icon(Icons.chevron_right), color: Colors.blue.shade700, disabledColor: Colors.grey.shade400, ), ], ), ), ], ); }); } // Fungsi untuk membuat tombol halaman dengan pendekatan smart pagination Widget _buildSmartPagination(TeacherDashboardController controller) { // Variabel yang dibutuhkan final int currentPage = controller.currentPage.value; final int totalPages = controller.totalPages.value; // Fungsi untuk membuat tombol halaman Widget pageButton(int index) { return Container( margin: const EdgeInsets.symmetric(horizontal: 2), child: ElevatedButton( onPressed: () => controller.goToPage(index), style: ElevatedButton.styleFrom( backgroundColor: currentPage == index ? Colors.blue.shade700 : Colors.white, foregroundColor: currentPage == index ? Colors.white : Colors.blue.shade700, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide( color: currentPage == index ? Colors.transparent : Colors.blue.shade200, ), ), padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 0, ), minimumSize: const Size(32, 32), ), child: Text('${index + 1}'), ), ); } // Widget untuk ellipsis Widget ellipsis() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Text( '...', style: TextStyle( color: Colors.grey.shade600, fontWeight: FontWeight.bold, ), ), ); } // Buat list untuk tombol-tombol yang akan ditampilkan List paginationItems = []; // Jika total halaman <= 7, tampilkan semua halaman if (totalPages <= 7) { for (int i = 0; i < totalPages; i++) { paginationItems.add(pageButton(i)); } } // Jika halaman lebih dari 7, gunakan smart pagination else { // Selalu tampilkan halaman pertama paginationItems.add(pageButton(0)); // Logika untuk menentukan halaman mana yang ditampilkan if (currentPage < 3) { // Dekat dengan awal: tampilkan halaman 0-4 kemudian ellipsis dan halaman terakhir for (int i = 1; i <= 3; i++) { paginationItems.add(pageButton(i)); } paginationItems.add(ellipsis()); paginationItems.add(pageButton(totalPages - 1)); } else if (currentPage >= totalPages - 3) { // Dekat dengan akhir: tampilkan halaman pertama, ellipsis, dan 4 halaman terakhir paginationItems.add(ellipsis()); for (int i = totalPages - 4; i < totalPages; i++) { paginationItems.add(pageButton(i)); } } else { // Di tengah: tampilkan halaman pertama, ellipsis, halaman saat ini dan tetangganya, ellipsis, halaman terakhir paginationItems.add(ellipsis()); for (int i = currentPage - 1; i <= currentPage + 1; i++) { paginationItems.add(pageButton(i)); } paginationItems.add(ellipsis()); paginationItems.add(pageButton(totalPages - 1)); } } // Tampilkan dalam SingleChildScrollView untuk mencegah overflow return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: paginationItems, ), ); } } class PieChartWidget extends StatelessWidget { final TeacherDashboardController controller; const PieChartWidget({ Key? key, required this.controller, }) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { // Hitung ukuran chart berdasarkan constraints final size = constraints.maxWidth < constraints.maxHeight ? constraints.maxWidth : constraints.maxHeight; return Center( child: SizedBox( width: size * 0.9, // Sedikit lebih kecil dari ruang yang tersedia height: size * 0.9, child: PieChart( PieChartData( centerSpaceRadius: size * 0.15, sectionsSpace: 1, borderData: FlBorderData(show: false), centerSpaceColor: Colors.transparent, sections: controller.getPieChartData().map((section) { return section.copyWith( radius: size * 0.35, showTitle: false, titlePositionPercentageOffset: 0, ); }).toList(), ), swapAnimationDuration: const Duration(milliseconds: 800), ), ), ); }, ); } } // Widget untuk menampilkan pesan saat tidak ada data class EmptyChartMessage extends StatelessWidget { const EmptyChartMessage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.bar_chart_outlined, size: 36, color: Colors.grey.shade300, ), const SizedBox(height: 4), Text( 'Belum ada data', style: TextStyle( color: Colors.grey.shade600, fontSize: 12, ), ), ], ), ); } } // Widget untuk item legenda yang disederhanakan dan konsisten Widget _buildLegendItem(String label, Color color) { return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 8, height: 8, decoration: BoxDecoration( color: color, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: Colors.grey.shade700, ), ), ], ); } class StudentResultDetailPage extends StatefulWidget { final DocumentSnapshot document; const StudentResultDetailPage({ Key? key, required this.document, }) : super(key: key); @override State createState() => _StudentResultDetailPageState(); } class _StudentResultDetailPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final GlobalKey _headerKey = GlobalKey(); @override void initState() { super.initState(); _tabController = TabController(length: 4, vsync: this); // Delay to allow build to complete before animations start Future.delayed(Duration.zero, () { _startInitialAnimations(); }); } void _startInitialAnimations() { // Animation logic for initial page load } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final data = widget.document.data() as Map; final isKerja = data['isKerja'] ?? false; final userName = data['userName'] ?? data['userEmail'] ?? 'Siswa'; final recommendations = List>.from(data['recommendations'] ?? []); final userAnswers = List>.from(data['userAnswers'] ?? []); Map workingMemory = {}; if (data['workingMemory'] != null) { if (data['workingMemory'] is Map) { workingMemory = Map.from(data['workingMemory']); } else if (data['workingMemory'] is List) { // Convert list to map with indices as keys final list = List.from(data['workingMemory']); for (int i = 0; i < list.length; i++) { workingMemory['item_$i'] = list[i]; } } } return Scaffold( body: NestedScrollView( headerSliverBuilder: (context, innerBoxIsScrolled) { return [ SliverAppBar( pinned: true, expandedHeight: 180.0, backgroundColor: Colors.blue.shade800, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Get.back(), ), actions: [ IconButton( icon: const Icon(Icons.share, color: Colors.white), onPressed: () { Get.snackbar( 'Info', 'Fitur berbagi hasil sedang dalam pengembangan', backgroundColor: Colors.blue.shade50, colorText: Colors.blue.shade700, snackPosition: SnackPosition.BOTTOM, animationDuration: const Duration(milliseconds: 800), duration: const Duration(seconds: 3), ); }, ), ], flexibleSpace: FlexibleSpaceBar( title: Text( 'Detail Hasil $userName', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: Colors.white), ), background: Hero( tag: 'student_header_${widget.document.id}', child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.blue.shade800, Colors.indigo.shade900, ], ), ), child: Stack( children: [ Positioned( right: -50, bottom: -50, child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 1500), curve: Curves.elasticOut, builder: (context, value, child) { return Opacity( opacity: 0.2 * value, child: Transform.scale( scale: value, child: Icon( isKerja ? Icons.work : Icons.school, size: 200, color: Colors.white, ), ), ); }, ), ), Positioned( top: 70, left: 20, child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800), curve: Curves.easeOutCubic, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(-20 * (1 - value), 0), child: Row( children: [ CircleAvatar( backgroundColor: Colors.white.withOpacity(0.9), radius: 30, child: Icon( Icons.person, color: Colors.blue.shade700, size: 30, ), ), const SizedBox(width: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data['userName'] ?? 'Siswa', style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), Text( data['userEmail'] ?? '', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 14, ), ), ], ), ], ), ), ); }, ), ), ], ), ), ), ), ), SliverPersistentHeader( delegate: _SliverAppBarDelegate( TabBar( controller: _tabController, tabs: [ Tab(icon: Icon(Icons.person), text: 'Profil'), Tab(icon: Icon(Icons.recommend), text: 'Rekomendasi'), Tab(icon: Icon(Icons.question_answer), text: 'Jawaban'), Tab(icon: Icon(Icons.memory), text: 'Working Memory'), ], labelColor: Colors.blue.shade700, unselectedLabelColor: Colors.grey.shade600, indicatorColor: Colors.blue.shade700, indicatorWeight: 3, ), ), pinned: true, ), ]; }, body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.grey.shade50, Colors.blue.shade50, ], stops: const [0.7, 1.0], ), ), child: TabBarView( controller: _tabController, children: [ _buildProfileTab(data), _buildRecommendationsTab(recommendations, isKerja), _buildAnswersTab(userAnswers), _buildWorkingMemoryTab(workingMemory), ], ), ), ), ); } Widget _buildProfileTab(Map data) { final timestamp = data['timestamp'] is Timestamp ? (data['timestamp'] as Timestamp).toDate() : DateTime.now(); final formattedDate = DateFormat('dd MMMM yyyy, HH:mm').format(timestamp); final isKerja = data['isKerja'] ?? false; final answeredQuestions = data['answeredQuestions'] ?? 0; final totalQuestions = data['totalQuestions'] ?? 0; final completionPercentage = totalQuestions > 0 ? (answeredQuestions / totalQuestions * 100).round() : 0; return SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader('Informasi Siswa', Icons.info), const SizedBox(height: 12), _buildStudentInfoCard(data), const SizedBox(height: 24), _buildSectionHeader('Progres Pengerjaan', Icons.insights), const SizedBox(height: 12), _buildProgressCard( completionPercentage, answeredQuestions, totalQuestions), const SizedBox(height: 24), _buildSectionHeader('Statistik', Icons.bar_chart), const SizedBox(height: 12), _buildStatisticsCard(data), ], ), ); } Widget _buildProgressCard(int percentage, int answered, int total) { return Card( elevation: 4, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ TweenAnimationBuilder( tween: Tween(begin: 0.0, end: percentage / 100), duration: const Duration(milliseconds: 1500), curve: Curves.easeOutCubic, builder: (context, value, child) { return Column( children: [ Stack( alignment: Alignment.center, children: [ SizedBox( height: 120, width: 120, child: CircularProgressIndicator( value: value, strokeWidth: 12, backgroundColor: Colors.grey.shade200, valueColor: AlwaysStoppedAnimation( Colors.blue.shade500, ), ), ), Column( children: [ Text( '${(value * 100).toInt()}%', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), Text( 'Selesai', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ], ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildProgressStat( 'Terjawab', answered.toString(), Icons.check_circle, Colors.green, ), _buildProgressStat( 'Total', total.toString(), Icons.assignment, Colors.blue, ), ], ), ], ); }, ), ], ), ), ); } Widget _buildProgressStat( String label, String value, IconData icon, MaterialColor color) { return Column( children: [ Icon( icon, color: color.shade400, size: 28, ), const SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: color.shade700, ), ), Text( label, style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ); } Widget _buildStatisticsCard(Map data) { // Sample data for demonstration final recommendations = List>.from(data['recommendations'] ?? []); final scores = recommendations.map((r) => (r['score'] ?? 0) as num).toList(); return Card( elevation: 4, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Distribusi Skor Rekomendasi', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), const SizedBox(height: 16), SizedBox( height: 200, child: scores.isEmpty ? Center( child: Text( 'Tidak ada data rekomendasi', style: TextStyle( color: Colors.grey.shade600, ), ), ) : BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: 100, barTouchData: BarTouchData( enabled: true, touchTooltipData: BarTouchTooltipData(), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (double value, TitleMeta meta) { final index = value.toInt(); if (index >= 0 && index < recommendations.length) { return Text( 'R${index + 1}', style: const TextStyle( color: Colors.black, fontWeight: FontWeight.bold, fontSize: 12, ), ); } return const SizedBox(); }, reservedSize: 22, ), ), leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (double value, TitleMeta meta) { if (value == 0 || value == 50 || value == 100) { return Text( '${value.toInt()}%', style: const TextStyle( color: Colors.black, fontSize: 10, ), ); } return const SizedBox(); }, interval: 25, reservedSize: 30, ), ), topTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), borderData: FlBorderData( show: false, ), barGroups: scores.asMap().entries.map((entry) { final index = entry.key; final value = entry.value; return BarChartGroupData( x: index, barRods: [ BarChartRodData( toY: value.toDouble(), gradient: LinearGradient( colors: [ Colors.blue.shade300, Colors.blue.shade700 ], begin: Alignment.bottomCenter, end: Alignment.topCenter, ), width: 20, borderRadius: const BorderRadius.only( topLeft: Radius.circular(6), topRight: Radius.circular(6), ), ), ], ); }).toList(), gridData: FlGridData( show: true, horizontalInterval: 25, getDrawingHorizontalLine: (value) { return FlLine( color: Colors.grey.shade200, strokeWidth: 1, ); }, ), ), swapAnimationDuration: const Duration(milliseconds: 800), ), ), ], ), ), ); } Widget _buildStudentInfoCard(Map data) { final timestamp = data['timestamp'] is Timestamp ? (data['timestamp'] as Timestamp).toDate() : DateTime.now(); final formattedDate = DateFormat('dd MMMM yyyy, HH:mm').format(timestamp); final isKerja = data['isKerja'] ?? false; return Card( elevation: 4, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ CircleAvatar( backgroundColor: Colors.blue.shade50, radius: 24, child: Icon( Icons.person, color: Colors.blue.shade700, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( data['userName'] ?? 'Siswa', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), Text( data['userEmail'] ?? '', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), ), ], ), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), Row( children: [ _buildInfoItem( icon: Icons.calendar_today, label: 'Tanggal', value: formattedDate, ), _buildInfoItem( icon: isKerja ? Icons.work : Icons.school, label: 'Jenis Rekomendasi', value: isKerja ? 'Karir' : 'Kuliah', ), ], ), const SizedBox(height: 12), Row( children: [ _buildInfoItem( icon: Icons.question_answer, label: 'Total Pertanyaan', value: data['totalQuestions']?.toString() ?? '0', ), _buildInfoItem( icon: Icons.check_circle, label: 'Pertanyaan Dijawab', value: data['answeredQuestions']?.toString() ?? '0', ), ], ), ], ), ), ); } Widget _buildRecommendationsTab( List> recommendations, bool isKerja) { return recommendations.isEmpty ? _buildEmptyCenteredMessage( 'Tidak ada rekomendasi yang tersedia', Icons.search_off, ) : ListView.builder( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), itemCount: recommendations.length + 1, // +1 for header itemBuilder: (context, index) { if (index == 0) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader( 'Hasil Rekomendasi', isKerja ? Icons.work : Icons.school, ), const SizedBox(height: 16), ], ); } final recommendationIndex = index - 1; final recommendation = recommendations[recommendationIndex]; return _buildRecommendationCard( recommendation, recommendationIndex, isKerja); }, ); } Widget _buildRecommendationCard( Map recommendation, int index, bool isKerja) { final score = recommendation['score']?.toString() ?? '0'; final careers = List.from(recommendation['careers'] ?? []); final majors = List.from(recommendation['majors'] ?? []); final rules = List.from(recommendation['rules'] ?? []); final title = recommendation['title'] ?? 'Rekomendasi ${index + 1}'; return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300 + (index * 150)), curve: Curves.easeOutCubic, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, 20 * (1 - value)), child: Card( margin: const EdgeInsets.only(bottom: 16), elevation: 4, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: ExpansionTile( initiallyExpanded: index == 0, // First one expanded by default tilePadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), childrenPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), leading: Hero( tag: 'recommendation_$index', child: CircleAvatar( backgroundColor: Colors.blue.shade50, radius: 20, child: Text( '${index + 1}', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.bold, ), ), ), ), title: Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), TweenAnimationBuilder( tween: Tween( begin: 0.0, end: double.parse(score) / 100), duration: const Duration(milliseconds: 1000), builder: (context, value, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Skor: ${(value * 100).toInt()}%', style: TextStyle( fontSize: 14, color: Colors.grey.shade600, ), ), ], ), const SizedBox(height: 4), LinearProgressIndicator( value: value, backgroundColor: Colors.grey.shade200, valueColor: AlwaysStoppedAnimation( value < 0.3 ? Colors.red.shade400 : value < 0.7 ? Colors.orange.shade400 : Colors.green.shade400, ), minHeight: 5, borderRadius: BorderRadius.circular(10), ), ], ); }, ), ], ), children: [ const Divider(), const SizedBox(height: 16), if (isKerja && careers.isNotEmpty) ...[ _buildSectionTitle('Karir yang Direkomendasikan'), const SizedBox(height: 8), _buildChipList(careers), const SizedBox(height: 16), ], if (!isKerja && majors.isNotEmpty) ...[ _buildSectionTitle('Jurusan yang Direkomendasikan'), const SizedBox(height: 8), _buildChipList(majors), const SizedBox(height: 16), ], if (rules.isNotEmpty) ...[ _buildSectionTitle('Aturan yang Terpenuhi'), const SizedBox(height: 8), _buildRulesList(rules), ], ], ), ), ), ); }, ); } Widget _buildAnswersTab(List> userAnswers) { return userAnswers.isEmpty ? _buildEmptyCenteredMessage( 'Tidak ada jawaban yang tersedia', Icons.question_mark, ) : ListView.builder( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), itemCount: userAnswers.length + 1, // +1 for header itemBuilder: (context, index) { if (index == 0) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader('Jawaban Siswa', Icons.question_answer), const SizedBox(height: 16), ], ); } final answerIndex = index - 1; return _buildAnswerCard(userAnswers[answerIndex], answerIndex); }, ); } Widget _buildAnswerCard(Map answer, int index) { final questionText = answer['question'] ?? 'Pertanyaan tidak tersedia'; final userAnswer = answer['answer'] ?? 'Tidak dijawab'; final programName = answer['programName'] ?? ''; final bobot = answer['bobot']?.toString() ?? '0'; return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300 + (index * 100)), curve: Curves.easeOutCubic, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(30 * (1 - value), 0), child: Card( margin: const EdgeInsets.only(bottom: 12), elevation: 3, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 28, height: 28, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.blue.shade100.withOpacity(0.3), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: Text( '${index + 1}', style: TextStyle( color: Colors.blue.shade700, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( questionText, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: userAnswer == 'Ya' ? Colors.green.shade50 : userAnswer == 'Tidak' ? Colors.red.shade50 : Colors.grey.shade50, borderRadius: BorderRadius.circular(10), border: Border.all( color: userAnswer == 'Ya' ? Colors.green.shade200 : userAnswer == 'Tidak' ? Colors.red.shade200 : Colors.grey.shade300, ), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: Text( 'Jawaban: $userAnswer', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: userAnswer == 'Ya' ? Colors.green.shade700 : userAnswer == 'Tidak' ? Colors.red.shade700 : Colors.grey.shade700, ), ), ), ), ], ), if (programName.isNotEmpty || bobot != '0') ...[ const SizedBox(height: 8), Wrap( spacing: 8, children: [ if (programName.isNotEmpty) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Text( programName, style: TextStyle( fontSize: 12, color: Colors.blue.shade700, ), ), ), if (bobot != '0') Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: Colors.amber.shade50, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Text( 'Bobot: $bobot', style: TextStyle( fontSize: 12, color: Colors.amber.shade800, ), ), ), ], ), ], ], ), ), ], ), ), ), ), ); }, ); } Widget _buildWorkingMemoryTab(Map workingMemory) { return workingMemory.isEmpty ? _buildEmptyCenteredMessage( 'Working memory tidak tersedia', Icons.memory_outlined, ) : ListView.builder( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), itemCount: workingMemory.length + 1, // +1 for header itemBuilder: (context, index) { if (index == 0) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionHeader( 'Working Memory', Icons.memory, tooltip: 'Data memori kerja yang digunakan saat proses inferensi', ), const SizedBox(height: 16), Card( elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Expanded( flex: 3, child: Text( 'Variabel', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), ), Expanded( flex: 2, child: Text( 'Nilai', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ), ), ], ), ), ), const SizedBox(height: 8), ], ); } final memoryIndex = index - 1; final entry = workingMemory.entries.elementAt(memoryIndex); return _buildWorkingMemoryItem( entry.key, entry.value, memoryIndex); }, ); } Widget _buildWorkingMemoryItem(String key, dynamic value, int index) { return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300 + (index * 50)), curve: Curves.easeOutCubic, builder: (context, animValue, child) { return Opacity( opacity: animValue, child: Transform.translate( offset: Offset(0, 20 * (1 - animValue)), child: Card( margin: const EdgeInsets.only(bottom: 8), elevation: 2, shadowColor: Colors.blue.shade100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( flex: 3, child: Text( key, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ), Expanded( flex: 2, child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: value is bool && value == true ? Colors.green.shade50 : value is bool && value == false ? Colors.red.shade50 : Colors.blue.shade50, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 2, offset: const Offset(0, 1), ), ], ), child: Text( value.toString(), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: value is bool && value == true ? Colors.green.shade700 : value is bool && value == false ? Colors.red.shade700 : Colors.blue.shade700, ), ), ), ), ], ), ), ), ), ); }, ); } Widget _buildEmptyCenteredMessage(String message, IconData icon) { return Center( child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: const Duration(milliseconds: 800), curve: Curves.elasticOut, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.scale( scale: 0.8 + (0.2 * value), child: Container( width: 250, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.blue.shade100.withOpacity(0.1), spreadRadius: 1, blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( icon, size: 60, color: Colors.grey.shade400, ), const SizedBox(height: 16), Text( message, textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey.shade600, ), ), ], ), ), ), ); }, ), ); } Widget _buildInfoItem({ required IconData icon, required String label, required String value, }) { return Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( icon, size: 16, color: Colors.blue.shade400, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, color: Colors.grey.shade600, ), ), Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ), ], ), ); } Widget _buildSectionHeader(String title, IconData icon, {String? tooltip}) { return Row( children: [ Icon( icon, color: Colors.blue.shade700, size: 20, ), const SizedBox(width: 8), Text( title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue.shade800, ), ), if (tooltip != null) ...[ const SizedBox(width: 8), Tooltip( message: tooltip, child: Icon( Icons.info_outline, size: 16, color: Colors.grey.shade600, ), ), ], ], ); } Widget _buildSectionTitle(String title) { return Text( title, style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blue.shade700, ), ); } Widget _buildChipList(List items) { return Wrap( spacing: 8, runSpacing: 8, children: items.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 600 + (index * 50)), curve: Curves.easeOutCubic, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.scale( scale: 0.8 + (0.2 * value), child: Chip( label: Text( item, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, ), ), backgroundColor: Colors.blue.shade50, labelStyle: TextStyle(color: Colors.blue.shade700), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, elevation: 1, shadowColor: Colors.blue.shade100, ), ), ); }, ); }).toList(), ); } Widget _buildRulesList(List rules) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: rules.asMap().entries.map((entry) { final index = entry.key; final rule = entry.value; return TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300 + (index * 100)), curve: Curves.easeOutCubic, builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(20 * (1 - value), 0), child: Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.check_circle, size: 16, color: Colors.green.shade600, ), const SizedBox(width: 8), Expanded( child: Text( rule, style: const TextStyle(fontSize: 14), ), ), ], ), ), ), ); }, ); }).toList(), ); } } class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { final TabBar tabBar; _SliverAppBarDelegate(this.tabBar); @override Widget build( BuildContext context, double shrinkOffset, bool overlapsContent) { return Container( decoration: BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide( color: Colors.grey.shade200, width: 1, ), ), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.05), spreadRadius: 1, blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: tabBar, ); } @override double get maxExtent => tabBar.preferredSize.height; @override double get minExtent => tabBar.preferredSize.height; @override bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { return false; } }