import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:firebase_database/firebase_database.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; class AbsensiPage extends StatefulWidget { const AbsensiPage({super.key}); @override State createState() => _AbsensiPageState(); } class _AbsensiPageState extends State { final refAbsen = FirebaseDatabase.instance.ref("absensi"); final refJadwal = FirebaseDatabase.instance.ref("jadwal/absen"); final refIzin = FirebaseDatabase.instance.ref("izin"); final jamMasuk = TextEditingController(); final jamPulang = TextEditingController(); String today = DateTime.now().toString().substring(0, 10); List siswaList = []; @override void initState() { super.initState(); loadJadwal(); loadSiswa(); } @override void dispose() { jamMasuk.dispose(); jamPulang.dispose(); super.dispose(); } // ================= LOAD DATA BACKEND ================= Future loadSiswa() async { final snap = await FirebaseFirestore.instance.collection('siswa').get(); setState(() { siswaList = snap.docs; }); } Future loadJadwal() async { final snap = await refJadwal.get(); if (snap.exists) { final data = snap.value as Map; jamMasuk.text = data['jam_masuk'] ?? ''; jamPulang.text = data['jam_pulang'] ?? ''; } } Future pilihJam(TextEditingController controller) async { final picked = await showTimePicker( context: context, initialTime: TimeOfDay.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light(primary: Color(0xff16A34A)), ), child: child!, ); }, ); if (picked != null) { controller.text = "${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}"; setState(() {}); } } Future simpanJadwal() async { await refJadwal.set({ "jam_masuk": jamMasuk.text, "jam_pulang": jamPulang.text, }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Jadwal berhasil disimpan"), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), ), ); } } // ================= DEKORASI WARNA STATUS ================= Color getStatusTextColor(String status) { switch (status) { case "tepat_waktu": return const Color(0xff16A34A); case "telat": return const Color(0xffEA580C); case "pulang": return const Color(0xff0284C7); case "izin": return const Color(0xff7C3AED); case "sakit": return const Color(0xff9333EA); default: return const Color(0xffDC2626); } } Color getStatusBgColor(String status) { switch (status) { case "tepat_waktu": return const Color(0xffF0FDF4); case "telat": return const Color(0xffFFF7ED); case "pulang": return const Color(0xffF0F9FF); case "izin": return const Color(0xffFAF5FF); case "sakit": return const Color(0xffF3E8FF); default: return const Color(0xffFEF2F2); } } // ================= ACC / TOLAK LOGIC ================= Future acc(String key, Map data) async { if (data['rfid'] == null) return; final uid = data['rfid']; final now = DateTime.now(); final jam = "${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}"; await refAbsen.child("$uid/$today/masuk").set({ "jam": jam, "status": data['jenis'], }); await refIzin.child(key).remove(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("${data['nama']} telah disetujui"), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), ), ); } } Future tolak(String key) async { await refIzin.child(key).remove(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Permintaan izin ditolak"), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))), ), ); } } void bukaFoto(String? base64String) { showDialog( context: context, builder: (_) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xffE6DFFF), Color(0xffCCBFFF)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( "Surat Izin", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xff12175E), ), ), InkWell( onTap: () => Navigator.pop(context), child: const Icon(Icons.close, color: Color(0xff12175E)), ), ], ), ), Padding( padding: const EdgeInsets.all(20), child: (base64String == null || base64String.isEmpty) ? const Text( "Tidak ada gambar surat izin", style: TextStyle(color: Color(0xff12175E), fontSize: 15), ) : Builder( builder: (_) { try { Uint8List bytes = base64Decode(base64String); return ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory(bytes, fit: BoxFit.contain), ); } catch (e) { return const Text( "Gagal memuat gambar", style: TextStyle(color: Color(0xffDC2626), fontSize: 15), ); } }, ), ), ], ), ), ); } Widget header(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; return Container( width: double.infinity, height: screenHeight * 0.24, decoration: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xffE6DFFF), Color(0xffCCBFFF), ], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40), ), boxShadow: [ BoxShadow( color: Color(0xff7F56D9), blurRadius: 24, offset: Offset(0, 8), ), ], ), child: ClipRRect( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40), ), child: Stack( children: [ Positioned( top: -30, left: -20, child: Container( width: 150, height: 150, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.15), ), ), ), Positioned.fill( child: CustomPaint(painter: HeaderWavePainter()), ), Positioned(top: 40, left: 24, child: _buildGridDots()), Positioned(top: 60, right: 60, child: _buildGridDots()), Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Row( children: [ Container( height: 44, width: 44, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.06), blurRadius: 12, offset: const Offset(0, 4), ) ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(14), onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back_ios_new, color: Color(0xff7F56D9), size: 18), ), ), ), const SizedBox(width: 16), Image.asset( 'lib/assets/sekolah.png', width: 50, height: 50, fit: BoxFit.contain, errorBuilder: (_, __, ___) => const Icon(Icons.school, size: 40, color: Color(0xff7F56D9)), ), const SizedBox(width: 12), const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ Text( "Absensi Harian", style: TextStyle( color: Color(0xff12175E), fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: -0.5, ), ), SizedBox(height: 4), Text( "Kelola kehadiran siswa hari ini", style: TextStyle( color: Color(0xff12175E), fontSize: 14, fontWeight: FontWeight.w500, height: 0.7, ), ), ], ), ), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.white.withOpacity(0.4), borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 2), ) ], ), child: const Icon( Icons.assignment_turned_in_rounded, color: Color(0xff16A34A), size: 26, ), ), ], ), ), ], ), ), ); } Widget _buildGridDots() { return Opacity( opacity: 0.25, child: Column( children: List.generate(4, (index) => Row( children: List.generate(4, (index) => Container( width: 2.5, height: 2.5, margin: const EdgeInsets.all(2.5), decoration: const BoxDecoration(color: Color(0xff12175E), shape: BoxShape.circle), )), )), ), ); } Widget sectionTitle(String title) { return Padding( padding: const EdgeInsets.only(left: 24, bottom: 12, top: 16), child: Row( children: [ Container(width: 24, height: 3, decoration: BoxDecoration(color: const Color(0xff16A34A), borderRadius: BorderRadius.circular(1.5))), const SizedBox(width: 3), Container(width: 3, height: 3, decoration: const BoxDecoration(color: Color(0xff16A34A), shape: BoxShape.circle)), const SizedBox(width: 8), Text( title.toUpperCase(), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xff12175E), letterSpacing: 0.5, ), ), ], ), ); } Widget jamInput({ required TextEditingController controller, required String label, required VoidCallback onTap, }) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: const Color(0xff16A34A).withOpacity(0.08), blurRadius: 16, spreadRadius: 0, offset: const Offset(0, 6), ), ], ), child: TextField( controller: controller, readOnly: true, onTap: onTap, textAlign: TextAlign.center, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xff12175E), ), decoration: InputDecoration( border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 20), prefixIcon: Container( margin: const EdgeInsets.all(8), width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xff16A34A).withOpacity(0.12), ), child: const Icon(Icons.access_time_rounded, color: Color(0xff16A34A)), ), hintText: "--:--", hintStyle: TextStyle(color: const Color(0xff12175E).withOpacity(0.3)), ), ), ); } Widget izinCard(Map data, String key) { return Container( width: 260, margin: const EdgeInsets.all(8), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: const Color(0xff7F56D9).withOpacity(0.08), blurRadius: 20, spreadRadius: 0, offset: const Offset(0, 8), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xff7C3AED).withOpacity(0.12), ), child: const Icon(Icons.description_rounded, color: Color(0xff7C3AED), size: 20), ), const SizedBox(width: 10), Expanded( child: Text( data['nama'] ?? 'Tidak ada nama', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xff12175E), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 12), Text( "Jenis: ${data['jenis'] ?? '-'}", style: TextStyle( fontSize: 13, color: const Color(0xff12175E).withOpacity(0.6), ), ), const Spacer(), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () => bukaFoto(data['foto']), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: const Size(60, 30), ), child: const Text( "Lihat Surat", style: TextStyle(color: Color(0xff7F56D9), fontSize: 13, fontWeight: FontWeight.w600), ), ), Row( children: [ SizedBox( height: 32, child: ElevatedButton( onPressed: () => acc(key, data), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xff16A34A), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10), ), child: const Text("ACC", style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), ), const SizedBox(width: 6), SizedBox( height: 32, child: ElevatedButton( onPressed: () => tolak(key), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xffDC2626), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), padding: const EdgeInsets.symmetric(horizontal: 10), ), child: const Text("Tolak", style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), ), ), ], ) ], ) ], ), ); } Widget absensiCard({ required String nama, required String jamMasuk, required String jamPulang, required String statusMasuk, required String statusPulang, }) { return Container( margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: const Color(0xff16A34A).withOpacity(0.08), blurRadius: 20, spreadRadius: 0, offset: const Offset(0, 8), ), ], ), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xff16A34A).withOpacity(0.12), boxShadow: [ BoxShadow( color: const Color(0xff16A34A).withOpacity(0.15), blurRadius: 12, spreadRadius: 2, ), ], ), child: const Icon(Icons.person_rounded, color: Color(0xff16A34A), size: 26), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( nama, style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: Color(0xff12175E), ), ), const SizedBox(height: 6), Text( "Masuk: $jamMasuk", style: TextStyle( fontSize: 14, color: const Color(0xff12175E).withOpacity(0.6), ), ), Text( "Pulang: $jamPulang", style: TextStyle( fontSize: 14, color: const Color(0xff12175E).withOpacity(0.6), ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: getStatusBgColor(statusMasuk), borderRadius: BorderRadius.circular(12), ), child: Text( statusMasuk == "tidak_hadir" ? "BELUM" : statusMasuk.toUpperCase(), style: TextStyle( color: getStatusTextColor(statusMasuk), fontWeight: FontWeight.bold, fontSize: 11, ), ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: getStatusBgColor(statusPulang), borderRadius: BorderRadius.circular(12), ), child: Text( statusPulang == "tidak_hadir" ? "BELUM" : statusPulang.toUpperCase(), style: TextStyle( color: getStatusTextColor(statusPulang), fontWeight: FontWeight.bold, fontSize: 11, ), ), ), ], ) ], ), ); } Widget gradientButton({ required String title, required VoidCallback onTap, }) { return SizedBox( height: 52, width: double.infinity, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: const LinearGradient( colors: [Color(0xff16A34A), Color(0xff22C55E)], ), boxShadow: [ BoxShadow( color: const Color(0xff16A34A).withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ElevatedButton( onPressed: onTap, style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), child: Text( title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xffF6F5FB), body: SafeArea( child: Column( children: [ header(context), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ sectionTitle("Jadwal Kehadiran"), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( children: [ Expanded(child: jamInput(controller: jamMasuk, label: "Jam Masuk", onTap: () => pilihJam(jamMasuk))), const SizedBox(width: 16), Expanded(child: jamInput(controller: jamPulang, label: "Jam Pulang", onTap: () => pilihJam(jamPulang))), ], ), ), const SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 80), child: gradientButton(title: "Simpan Jadwal", onTap: simpanJadwal), ), sectionTitle("Permintaan Izin"), StreamBuilder( stream: refIzin.onValue, builder: (context, snapshot) { if (!snapshot.hasData) return const SizedBox(height: 150); final data = snapshot.data!.snapshot.value as Map? ?? {}; if (data.isEmpty) { return SizedBox( height: 150, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.check_circle_outline, size: 40, color: const Color(0xff12175E).withOpacity(0.3)), const SizedBox(height: 8), Text( "Tidak ada permintaan izin", style: TextStyle(color: const Color(0xff12175E).withOpacity(0.5)), ), ], ), ), ); } return SizedBox( height: 170, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), children: data.entries.map((e) { return izinCard(Map.from(e.value), e.key); }).toList(), ), ); }, ), sectionTitle("Daftar Kehadiran Hari Ini"), StreamBuilder( stream: refAbsen.onValue, builder: (context, snapshot) { if (!snapshot.hasData) { return const Padding( padding: EdgeInsets.all(30.0), child: Center(child: CircularProgressIndicator(color: Color(0xff7F56D9))), ); } final absen = snapshot.data!.snapshot.value as Map? ?? {}; if (siswaList.isEmpty) { return const Center(child: Padding(padding: EdgeInsets.all(20), child: Text("Memuat data siswa..."))); } return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: siswaList.length, itemBuilder: (c, i) { final doc = siswaList[i]; final uid = doc['rfid']; final nama = doc['nama']; String statusMasuk = "tidak_hadir"; String jamMasukSiswa = "-"; String statusPulang = "tidak_hadir"; String jamPulangSiswa = "-"; if (absen[uid] != null && absen[uid][today] != null) { final dataHariIni = absen[uid][today] as Map; if (dataHariIni['masuk'] != null) { statusMasuk = dataHariIni['masuk']['status'] ?? "tidak_hadir"; jamMasukSiswa = dataHariIni['masuk']['jam'] ?? "-"; } if (dataHariIni['pulang'] != null) { statusPulang = dataHariIni['pulang']['status'] ?? "tidak_hadir"; jamPulangSiswa = dataHariIni['pulang']['jam'] ?? "-"; } } return absensiCard( nama: nama, jamMasuk: jamMasukSiswa, jamPulang: jamPulangSiswa, statusMasuk: statusMasuk, statusPulang: statusPulang, ); }, ); }, ), const SizedBox(height: 20), ], ), ), ), ], ), ), ); } } class HeaderWavePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.3) ..style = PaintingStyle.stroke ..strokeWidth = 1.8; final path = Path(); path.moveTo(0, size.height * 0.4); path.quadraticBezierTo(size.width * 0.5, size.height * 0.1, size.width, size.height * 0.3); canvas.drawPath(path, paint); final path2 = Path(); path2.moveTo(0, size.height * 0.6); path2.quadraticBezierTo(size.width * 0.6, size.height * 0.2, size.width, size.height * 0.5); canvas.drawPath(path2, paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }