368 lines
12 KiB
Dart
368 lines
12 KiB
Dart
// 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<StudentModel> 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<ViolationHistory> realViolations =
|
|
violationSnapshot.docs.map((v) {
|
|
return ViolationHistory.fromFirestore(
|
|
v.data() as Map<String, dynamic>,
|
|
);
|
|
}).toList();
|
|
|
|
// Ambil data absensi real
|
|
List<AttendanceLog> realAttendance = await getAttendanceData(user.uid);
|
|
|
|
return StudentModel.fromFirestore(
|
|
studentDoc.data() as Map<String, dynamic>,
|
|
studentDoc.id,
|
|
violations: realViolations,
|
|
attendance: realAttendance,
|
|
);
|
|
} catch (e) {
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<List<AttendanceLog>> getAttendanceData(String studentId) async {
|
|
List<AttendanceLog> logs = [];
|
|
try {
|
|
// Ambil data dari koleksi 'absensi'
|
|
QuerySnapshot absensiSnapshot =
|
|
await _firestore.collection('absensi').get();
|
|
|
|
for (var doc in absensiSnapshot.docs) {
|
|
Map<String, dynamic> docData = doc.data() as Map<String, dynamic>;
|
|
|
|
// 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<List<Map<String, dynamic>>> 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<List<Map<String, dynamic>>> 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<void> markNotificationAsRead(String notificationId) async {
|
|
await _firestore.collection('notifikasi').doc(notificationId).update({
|
|
'status': 'read',
|
|
});
|
|
}
|
|
|
|
Future<void> 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<String> _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<void> _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: <String, String>{
|
|
'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<void> 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<String?> 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<void> 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<void> 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");
|
|
}
|
|
}
|
|
}
|