import 'dart:io'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:image_picker/image_picker.dart'; import 'login_page.dart'; class DashboardOrtu extends StatefulWidget { const DashboardOrtu({super.key}); @override State createState() => _DashboardOrtuState(); } class _DashboardOrtuState extends State { final refAbsen = FirebaseDatabase.instance.ref("absensi"); final refIzin = FirebaseDatabase.instance.ref("izin"); final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final Set notified = {}; String today() => DateTime.now().toString().substring(0, 10); @override void initState() { super.initState(); initNotif(); } Future initNotif() async { const android = AndroidInitializationSettings('@mipmap/ic_launcher'); const settings = InitializationSettings(android: android); await flutterLocalNotificationsPlugin.initialize(settings: settings); } Future showNotif(String title, String body) async { const androidDetails = AndroidNotificationDetails( 'absensi_channel', 'Absensi', importance: Importance.max, priority: Priority.high, ); const details = NotificationDetails(android: androidDetails); await flutterLocalNotificationsPlugin.show( id: 0, title: title, body: body, notificationDetails: details, ); } Color warnaBgStatus(String status) { if (status == "tepat_waktu") return const Color(0xffE8F5E9); if (status == "telat") return const Color(0xffFFF3E0); if (status == "pulang") return const Color(0xffE3F2FD); if (status == "izin") return const Color(0xffE0F2FE); if (status == "sakit") return const Color(0xffF3E5F5); return const Color(0xffFFEBEE); } Color warnaTeksStatus(String status) { if (status == "tepat_waktu") return const Color(0xff2E7D32); if (status == "telat") return const Color(0xffEF6C00); if (status == "pulang") return const Color(0xff1565C0); if (status == "izin") return const Color(0xff0284C7); if (status == "sakit") return const Color(0xff8E24AA); return const Color(0xffD32F2F); } IconData iconStatus(String status) { if (status == "tepat_waktu") return Icons.check_circle_rounded; if (status == "telat") return Icons.warning_rounded; if (status == "pulang") return Icons.exit_to_app_rounded; if (status == "izin") return Icons.info_rounded; if (status == "sakit") return Icons.local_hospital_rounded; return Icons.cancel_rounded; } void kirimIzin(String nama, String rfid) { final jenis = ValueNotifier("izin"); File? imageFile; showDialog( context: context, builder: (_) => StatefulBuilder( builder: (context, setStateDialog) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), title: const Text( "Ajukan Izin / Sakit", style: TextStyle(fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ ValueListenableBuilder( valueListenable: jenis, builder: (_, val, __) { return DropdownButtonFormField( value: val, decoration: InputDecoration( labelText: "Pilih Keterangan", labelStyle: const TextStyle(color: Color(0xff6B7280)), border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xffE5E7EB)), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: const BorderSide(color: Color(0xff7F56D9), width: 2), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), ), items: const [ DropdownMenuItem(value: "izin", child: Text("Izin / Berhalangan")), DropdownMenuItem(value: "sakit", child: Text("Sakit")), ], onChanged: (v) => jenis.value = v!, ); }, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: () async { final picker = ImagePicker(); final picked = await picker.pickImage(source: ImageSource.gallery, imageQuality: 50); if (picked != null) { setStateDialog(() => imageFile = File(picked.path)); } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffF3E8FF), foregroundColor: const Color(0xff7F56D9), elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), minimumSize: const Size(double.infinity, 50), ), icon: const Icon(Icons.image_rounded), label: const Text("Pilih Foto Surat Dokter / Wali", style: TextStyle(fontWeight: FontWeight.w600)), ), if (imageFile != null) ...[ const SizedBox(height: 16), Stack( children: [ ClipRRect( borderRadius: BorderRadius.circular(16), child: Image.file(imageFile!, height: 160, width: double.infinity, fit: BoxFit.cover), ), Positioned( top: 8, right: 8, child: GestureDetector( onTap: () => setStateDialog(() => imageFile = null), child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration(color: Colors.black54, shape: BoxShape.circle), child: const Icon(Icons.close, color: Colors.white, size: 18), ), ), ) ], ) ] ], ), ), actionsPadding: const EdgeInsets.only(bottom: 16, right: 16, left: 16), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Batal", style: TextStyle(color: Color(0xff6B7280), fontWeight: FontWeight.w600)), ), ElevatedButton( onPressed: () async { if (imageFile == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Pilih foto surat terlebih dahulu"), backgroundColor: Colors.amber), ); return; } try { final bytes = await imageFile!.readAsBytes(); final base64 = base64Encode(bytes); await refIzin.push().set({ "nama": nama, "rfid": rfid, "jenis": jenis.value, "foto": base64, "tanggal": today(), "waktu": ServerValue.timestamp, }); if (context.mounted) Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Izin berhasil dikirim"), backgroundColor: Colors.green), ); } catch (e) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Gagal: $e"))); } }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xff7F56D9), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), ), child: const Text("Kirim Permohonan", style: TextStyle(fontWeight: FontWeight.w600)), ) ], ); }, ), ); } /// ✅ Fungsi Reset Data Lama Future resetDataLama(String rfid) async { try { final now = DateTime.now(); final batas = DateTime(now.year, now.month, now.day - 30); // Simpan hanya 30 hari terakhir final snapshot = await refAbsen.child(rfid).get(); if (snapshot.exists) { final data = Map.from(snapshot.value as Map); for (final tanggal in data.keys) { try { final tgl = DateTime.parse(tanggal); if (tgl.isBefore(batas)) { await refAbsen.child("$rfid/$tanggal").remove(); } } catch (_) {} } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Data lama berhasil dibersihkan"), backgroundColor: Colors.blue), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Gagal bersihkan data: $e"), backgroundColor: Colors.red), ); } } } /// ✅ Tampilkan Riwayat Absen void lihatRiwayatAbsen(String rfid, String nama) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(28)), ), isScrollControlled: true, builder: (_) => DraggableScrollableSheet( initialChildSize: 0.75, maxChildSize: 0.9, minChildSize: 0.5, expand: false, builder: (_, scrollController) => Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "Riwayat Absen: $nama", style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), IconButton( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.close, color: Color(0xff6B7280)), ) ], ), const Divider(), const SizedBox(height: 8), Expanded( child: StreamBuilder( stream: refAbsen.child(rfid).orderByKey().limitToLast(30).onValue, builder: (_, snap) { if (!snap.hasData || snap.data?.snapshot.value == null) { return const Center(child: Text("Belum ada riwayat absen")); } final data = Map.from(snap.data!.snapshot.value as Map); final daftarTanggal = data.keys.toList()..sort((a, b) => b.compareTo(a)); return ListView.builder( controller: scrollController, itemCount: daftarTanggal.length, itemBuilder: (_, i) { final tgl = daftarTanggal[i]; final absen = Map.from(data[tgl]); String jamMasuk = "-", statusMasuk = "tidak_hadir"; String jamPulang = "-", statusPulang = "tidak_hadir"; if (absen['masuk'] != null) { final m = Map.from(absen['masuk']); jamMasuk = m['jam'] ?? "-"; statusMasuk = m['status'] ?? "tidak_hadir"; } else { jamMasuk = absen['jam'] ?? "-"; statusMasuk = absen['status'] ?? "tidak_hadir"; } if (absen['pulang'] != null) { final p = Map.from(absen['pulang']); jamPulang = p['jam'] ?? "-"; statusPulang = p['status'] ?? "tidak_hadir"; } return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: const [BoxShadow(color: Color(0x0A000000), blurRadius: 6)], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( tgl, style: const TextStyle(fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _buildRiwayatItem("Masuk", jamMasuk, statusMasuk), _buildRiwayatItem("Pulang", jamPulang, statusPulang), ], ) ], ), ); }, ); }, ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: () async { Navigator.pop(context); await resetDataLama(rfid); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffEF4444), foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.symmetric(vertical: 12), ), icon: const Icon(Icons.delete_sweep), label: const Text("Bersihkan Data Lama (>30 Hari)"), ), ) ], ), ), ), ); } Widget _buildRiwayatItem(String label, String jam, String status) { return Row( children: [ Icon(iconStatus(status), size: 18, color: warnaTeksStatus(status)), const SizedBox(width: 6), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 12, color: Color(0xff6B7280), fontWeight: FontWeight.w500)), Text(jam, style: const TextStyle(fontWeight: FontWeight.bold)), ], ) ], ); } /// ✅ Langsung buka riwayat tanpa pilih anak void bukaRiwayatLangsung() async { final uidUser = FirebaseAuth.instance.currentUser!.uid; final snap = await FirebaseFirestore.instance .collection('siswa') .where('ortu_uid', isEqualTo: uidUser) .limit(1) // Ambil data anak pertama saja .get(); if (!mounted) return; if (snap.docs.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Belum ada data anak terdaftar")), ); return; } final data = snap.docs.first.data(); final nama = data['nama'] ?? 'Tidak ada nama'; final rfid = data['rfid'] ?? ''; lihatRiwayatAbsen(rfid, nama); } @override Widget build(BuildContext context) { final uidUser = FirebaseAuth.instance.currentUser!.uid; final screen = MediaQuery.of(context); return Scaffold( backgroundColor: const Color(0xffF9FAFB), body: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: Stack( children: [ // Background Header Gradasi Melengkung Halus Container( width: double.infinity, height: screen.size.height * 0.38, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xffECE7FF), Color(0xffF9FAFB)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40), ), ), ), // Konten Utama Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 50), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Top Bar: Brand Logo & Aksi Tombol Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Image.asset( 'lib/assets/sekolah.png', width: 50, height: 50, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const Icon(Icons.school_rounded, size: 45, color: Color(0xff7F56D9)), ), const SizedBox(width: 12), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("E-Absensi", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: Color(0xff1E1B4B))), Text("Sistem Pemantauan", style: TextStyle(fontSize: 12, color: Color(0xff6B7280))), ], ) ], ), Row( children: [ // ✅ Klik lonceng langsung ke riwayat _buildIconButton(Icons.notifications_none_rounded, bukaRiwayatLangsung), const SizedBox(width: 10), _buildIconButton(Icons.logout_rounded, () async { await FirebaseAuth.instance.signOut(); if (context.mounted) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const LoginPage()), (_) => false, ); } }), ], ) ], ), const SizedBox(height: 32), // Banner Selamat Datang Terintegrasi Ilustrasi Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xff7F56D9), Color(0xff633BB3)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(28), boxShadow: [ BoxShadow(color: const Color(0xff7F56D9).withOpacity(0.2), blurRadius: 15, offset: const Offset(0, 8)) ], ), child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Selamat Datang Kembali,", style: TextStyle(fontSize: 14, color: Color(0xffE2D6FF))), const SizedBox(height: 4), const Text( "Dashboard\nWali Murid", style: TextStyle(fontSize: 26, fontWeight: FontWeight.bold, color: Colors.white, height: 1.2), ), const SizedBox(height: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration(color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(100)), child: Text( today(), style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500), ), ) ], ), Positioned( right: -10, bottom: -10, child: Opacity( opacity: 0.9, child: Image.asset( 'lib/assets/anak_rfid.png', width: screen.size.width * 0.38, height: 130, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const SizedBox(), ), ), ) ], ), ), const SizedBox(height: 32), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Daftar Kehadiran Anak", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), ], ), const SizedBox(height: 16), // Data Siswa & Log Stream Builder StreamBuilder( stream: FirebaseFirestore.instance .collection('siswa') .where('ortu_uid', isEqualTo: uidUser) .snapshots(), builder: (_, siswaSnap) { if (!siswaSnap.hasData) { return const Center(child: CircularProgressIndicator(color: Color(0xff7F56D9))); } final siswaList = siswaSnap.data!.docs; if (siswaList.isEmpty) { return Container( width: double.infinity, padding: const EdgeInsets.all(32), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xffF3E5F5)), ), child: const Column( children: [ Icon(Icons.child_care_rounded, size: 48, color: Color(0xff9CA3AF)), SizedBox(height: 12), Text("Belum ada data relasi anak terdaftar.", style: TextStyle(color: Color(0xff6B7280))), ], ), ); } return Column( children: siswaList.map((doc) { final data = doc.data(); final rfid = data['rfid'] ?? ''; final nama = data['nama'] ?? 'Nama Siswa'; final kelas = data['kelas'] ?? '-'; final namaOrtu = data['nama_orang_tua'] ?? 'Nama Orang Tua'; return StreamBuilder( stream: refAbsen.child("$rfid/${today()}").onValue, builder: (_, absenSnap) { String statusMasuk = "tidak_hadir"; String jamMasuk = "-"; String statusPulang = "tidak_hadir"; String jamPulang = "-"; String statusUtama = "tidak_hadir"; if (absenSnap.hasData && absenSnap.data!.snapshot.value != null) { final raw = absenSnap.data!.snapshot.value as Map; final absenData = Map.from(raw); if (absenData['masuk'] != null) { final m = Map.from(absenData['masuk']); statusMasuk = m['status'] ?? "tidak_hadir"; jamMasuk = m['jam'] ?? "-"; } else { statusMasuk = absenData['status'] ?? "tidak_hadir"; jamMasuk = absenData['jam'] ?? "-"; } if (absenData['pulang'] != null) { final p = Map.from(absenData['pulang']); statusPulang = p['status'] ?? "tidak_hadir"; jamPulang = p['jam'] ?? "-"; } if (statusPulang != "tidak_hadir") { statusUtama = statusPulang; } else { statusUtama = statusMasuk; } // Pengkondisian Notifikasi Pemicu Lokal final keyMasuk = "$rfid-masuk-$statusMasuk-$jamMasuk"; if (!notified.contains(keyMasuk) && statusMasuk != "tidak_hadir") { notified.add(keyMasuk); WidgetsBinding.instance.addPostFrameCallback((_) { showNotif("Absen Masuk $nama", "Status: ${statusMasuk.replaceAll('_', ' ')} • Jam: $jamMasuk"); }); } final keyPulang = "$rfid-pulang-$statusPulang-$jamPulang"; if (!notified.contains(keyPulang) && statusPulang != "tidak_hadir") { notified.add(keyPulang); WidgetsBinding.instance.addPostFrameCallback((_) { showNotif("Absen Pulang $nama", "Status: $statusPulang • Jam: $jamPulang"); }); } } return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow(color: const Color(0xff1E1B4B).withOpacity(0.04), blurRadius: 16, offset: const Offset(0, 4)) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Info Siswa Di Dalam Kartu Padding( padding: const EdgeInsets.all(20), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: const Color(0xffF3E8FF), borderRadius: BorderRadius.circular(16), ), child: const Icon(Icons.face_rounded, color: Color(0xff7F56D9), size: 26), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( nama, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), Text( "Kelas $kelas", style: const TextStyle(fontSize: 13, color: Color(0xff6B7280), fontWeight: FontWeight.w500), ), const SizedBox(height: 4), Text( "👨‍👩‍👧 $namaOrtu", style: const TextStyle(fontSize: 12, color: Color(0xff7F56D9), fontWeight: FontWeight.w600), ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: warnaBgStatus(statusUtama), borderRadius: BorderRadius.circular(100), ), child: Text( statusUtama.toUpperCase().replaceAll('_', ' '), style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: warnaTeksStatus(statusUtama)), ), ) ], ), ), // Linimasa Detail Alur Absensi Masuk & Pulang Container( margin: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0xffF9FAFB), borderRadius: BorderRadius.circular(20), ), child: Row( children: [ _buildTimeLogTile("Masuk Sekolah", jamMasuk, statusMasuk), Container(width: 1, height: 45, color: const Color(0xffE5E7EB)), _buildTimeLogTile("Pulang Sekolah", jamPulang, statusPulang), ], ), ), Padding( padding: const EdgeInsets.all(20), child: Row( children: [ Expanded( child: ElevatedButton( onPressed: () => kirimIzin(nama, rfid), style: ElevatedButton.styleFrom( backgroundColor: Colors.white, foregroundColor: const Color(0xff7F56D9), elevation: 0, side: const BorderSide(color: Color(0xffE5E7EB)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), minimumSize: const Size(double.infinity, 48), ), child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.edit_document, size: 18), SizedBox(width: 8), Text("Ajukan Izin / Sakit", style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14)), ], ), ), ), ], ), ) ], ), ); }, ); }).toList(), ); }, ), ], ), ), ], ), ), ); } Widget _buildIconButton(IconData icon, VoidCallback onTap) { return Container( width: 44, height: 44, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow(color: const Color(0xff1E1B4B).withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2)) ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: onTap, child: Icon(icon, color: const Color(0xff7F56D9), size: 22), ), ), ); } Widget _buildTimeLogTile(String label, String time, String status) { return Expanded( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(iconStatus(status), size: 16, color: warnaTeksStatus(status)), const SizedBox(width: 6), Text(label, style: const TextStyle(fontSize: 12, color: Color(0xff6B7280), fontWeight: FontWeight.w500)), ], ), const SizedBox(height: 6), Text( time, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xff1E1B4B)), ), ], ), ); } }