// lib/core/service/student_service.dart import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:googleapis_auth/auth_io.dart' as auth; import 'package:http/http.dart' as http; import 'package:smpn1_lengkong/core/models/attendance_log_model.dart'; import 'package:smpn1_lengkong/core/models/student_model.dart'; import 'package:smpn1_lengkong/core/models/violation_history_model.dart'; import 'package:url_launcher/url_launcher.dart'; class StudentService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseAuth _auth = FirebaseAuth.instance; Future getCurrentStudentData() async { try { final user = _auth.currentUser; if (user == null) { throw Exception("Sesi berakhir, silakan login kembali."); } // MODIFIKASI: Menambahkan GetOptions agar mengambil data terbaru dari server // Ini memastikan informasi pribadi yang baru diedit langsung sinkron DocumentSnapshot studentDoc = await _firestore .collection('siswa') .doc(user.uid) .get(const GetOptions(source: Source.serverAndCache)); if (!studentDoc.exists) throw Exception("Data siswa tidak ditemukan."); // Bagian pengambilan data pelanggaran tetap sama QuerySnapshot violationSnapshot = await _firestore .collection('violations') .where('student_id', isEqualTo: user.uid) .get(); List realViolations = violationSnapshot.docs.map((v) { return ViolationHistory.fromFirestore( v.data() as Map, ); }).toList(); // Ambil data absensi real List realAttendance = await getAttendanceData(user.uid); return StudentModel.fromFirestore( studentDoc.data() as Map, studentDoc.id, violations: realViolations, attendance: realAttendance, ); } catch (e) { rethrow; } } Future> getAttendanceData(String studentId) async { List logs = []; try { // Ambil data dari koleksi 'absensi' QuerySnapshot absensiSnapshot = await _firestore.collection('absensi').get(); for (var doc in absensiSnapshot.docs) { Map docData = doc.data() as Map; // Cek apakah data untuk UID siswa ini ada di dokumen tersebut if (docData['data'] != null && docData['data'][studentId] != null) { var studentRecord = docData['data'][studentId]; var metadata = docData['metadata']; // Ambil tanggal dari metadata (format YYYY-MM-DD) String? rawDate = metadata != null ? metadata['tanggal'] : null; if (rawDate != null) { logs.add( AttendanceLog( date: DateTime.parse(rawDate), status: studentRecord['status'] ?? 'Hadir', description: studentRecord['mapel'] ?? '-', className: studentRecord['kelas'] ?? '-', ), ); } } } // Urutkan dari yang terbaru logs.sort((a, b) => b.date.compareTo(a.date)); return logs; } catch (e) { debugPrint("Error detail fetch absensi: $e"); return []; } } Future>> getBimbinganHistory( String studentId, String className, ) async { try { final snapshot = await _firestore .collection('Bimbingan') .where('student_id', isEqualTo: studentId) .where( 'kelas', isEqualTo: className, ) // Filter berdasarkan kelas yang sedang dilihat .orderBy('createdAt', descending: true) .get(); return snapshot.docs.map((doc) => {'id': doc.id, ...doc.data()}).toList(); } catch (e) { debugPrint("Error detail fetch bimbingan: $e"); return []; } } // Di dalam class StudentService Future>> getNotifications( String userId, String className, ) async { final snapshot = await _firestore .collection('notifikasi') .where('id_user', isEqualTo: userId) .where('kelas', isEqualTo: className) // TAMBAHKAN FILTER INI .get(); return snapshot.docs.map((doc) => {'id': doc.id, ...doc.data()}).toList(); } Future markNotificationAsRead(String notificationId) async { await _firestore.collection('notifikasi').doc(notificationId).update({ 'status': 'read', }); } Future markAllAsRead(String userId, String className) async { try { // Ambil semua notifikasi unread milik user di kelas ini final querySnapshot = await _firestore .collection('notifikasi') .where('id_user', isEqualTo: userId) .where('kelas', isEqualTo: className) .where('status', isEqualTo: 'unread') .get(); if (querySnapshot.docs.isEmpty) return; // Gunakan WriteBatch untuk efisiensi update massal WriteBatch batch = _firestore.batch(); for (var doc in querySnapshot.docs) { batch.update(doc.reference, {'status': 'read'}); } await batch.commit(); } catch (e) { debugPrint("Error mark all as read: $e"); } } Future _getAccessToken() async { try { // Pastikan nama file ini sesuai dengan yang ada di folder assets dan pubspec.yaml final serviceAccountJson = await rootBundle.loadString( 'assets/service.json', ); final data = json.decode(serviceAccountJson); final accountCredentials = auth.ServiceAccountCredentials.fromJson(data); final scopes = ['https://www.googleapis.com/auth/firebase.messaging']; final client = await auth.clientViaServiceAccount( accountCredentials, scopes, ); return client.credentials.accessToken.data; } catch (e) { debugPrint("Gagal mendapatkan Access Token: $e"); rethrow; } } Future _sendPushNotification( String token, String title, String body, ) async { try { final String accessToken = await _getAccessToken(); const String projectId = 'smpn1lengkong-b7621'; final String url = 'https://fcm.googleapis.com/v1/projects/$projectId/messages:send'; final response = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $accessToken', }, body: jsonEncode({ 'message': { 'token': token, 'notification': {'title': title, 'body': body}, 'android': { 'priority': 'high', // Memastikan prioritas tinggi 'notification': { 'channel_id': 'high_importance_channel', // Harus sama dengan di FcmService 'sound': 'default', 'click_action': 'FLUTTER_NOTIFICATION_CLICK', // Penting untuk perangkat fisik 'notification_priority': 'PRIORITY_MAX', // Tambahan untuk Android lawas/fisik }, }, 'data': { 'type': 'info', 'click_action': 'FLUTTER_NOTIFICATION_CLICK', }, }, }), ); // Tambahkan log untuk debugging di perangkat fisik if (response.statusCode != 200) { debugPrint("FCM Error: ${response.statusCode} - ${response.body}"); } } catch (e) { debugPrint("Error FCM V1: $e"); } } // 3. Fungsi Update Data & Trigger Notifikasi Future updateStudentPersonalData({ required StudentModel student, required String fatherName, required String motherName, required String job, required String address, required String phoneNumber, }) async { try { // Update Profil Siswa await _firestore.collection('siswa').doc(student.id).update({ 'nama_ayah': fatherName, 'nama_ibu': motherName, 'pekerjaan_ortu': job, 'alamat': address, 'no_hp': phoneNumber, 'updated_at': FieldValue.serverTimestamp(), }); // Ambil Token FCM DocumentSnapshot userDoc = await _firestore.collection('siswa').doc(student.id).get(); String? fcmToken = userDoc.get('fcm_token'); final String currentTime = DateTime.now().toString().substring(0, 19); final String title = "📝 Pembaruan Informasi Pribadi"; final String body = "Yth. Bapak/Ibu Wali, Informasi pribadi untuk siswa atas nama ${student.name} telah berhasil diperbarui. Terima kasih."; // Tambah Dokumen ke Koleksi Notifikasi await _firestore.collection('notifikasi').add({ 'id_user': student.id, 'angkatan': student.angkatan, 'body': body, 'tanggal': currentTime, 'fcm_token': fcmToken ?? "", 'kelas': student.className, 'nama_siswa': student.name, 'siswa_id': student.id, 'status': 'unread', 'title': title, 'type': 'info', }); // Kirim Notifikasi ke Device if (fcmToken != null && fcmToken.isNotEmpty) { await _sendPushNotification(fcmToken, title, body); } } catch (e) { debugPrint("Error update data: $e"); throw Exception("Gagal menyimpan perubahan."); } } // Fungsi untuk mengambil link WA dari koleksi administrasi Future getContactLink() async { try { // Memastikan query sesuai dengan aturan read di Firebase final snapshot = await _firestore.collection('administrasi').limit(1).get(); if (snapshot.docs.isNotEmpty) { return snapshot.docs.first.data()['contact']; } return null; } catch (e) { // Jika masih error permission, akan tercetak di sini debugPrint("Error fetch contact: $e"); return null; } } // lib/core/service/student_service.dart Future launchWhatsApp(String url) async { try { final Uri uri = Uri.parse(url); // Gunakan LaunchMode.externalApplication agar Android // langsung mencari aplikasi WA atau Browser di luar app kita if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { throw 'WhatsApp tidak terinstal atau tautan tidak valid'; } } catch (e) { debugPrint("Error launching URL: $e"); rethrow; } } Future sendBimbinganNotification({ required String studentId, required String studentName, required String className, required String category, }) async { try { // 1. Ambil Token FCM terbaru dari database DocumentSnapshot userDoc = await _firestore.collection('siswa').doc(studentId).get(); String? fcmToken = userDoc.get('fcm_token'); final String currentTime = DateTime.now().toString().substring(0, 19); final String title = "🗓️ Permohonan Kunjungan Dikirim"; final String body = "Permohonan kunjungan untuk $studentName (Kelas $className) telah berhasil diajukan ke sekolah. Mohon tunggu konfirmasi jadwal."; // 2. Simpan ke koleksi 'notifikasi' agar tampil di NotificationScreen await _firestore.collection('notifikasi').add({ 'id_user': studentId, 'body': body, 'tanggal': currentTime, 'fcm_token': fcmToken ?? "", 'kelas': className, 'nama_siswa': studentName, 'siswa_id': studentId, 'status': 'unread', 'title': title, 'type': 'info', }); // 3. Kirim Push Notification ke perangkat if (fcmToken != null && fcmToken.isNotEmpty) { await _sendPushNotification(fcmToken, title, body); } } catch (e) { debugPrint("Gagal mengirim notifikasi bimbingan: $e"); } } }