import 'dart:async'; import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_database/firebase_database.dart'; class TambahSiswa extends StatefulWidget { const TambahSiswa({super.key}); @override State createState() => _TambahSiswaState(); } class _TambahSiswaState extends State { final nama = TextEditingController(); final namaOrtu = TextEditingController(); final nomorAbsen = TextEditingController(); final emailOrtu = TextEditingController(); final passwordOrtu = TextEditingController(); String rfid = ''; String kelasPilihan = 'TK A'; bool isLoading = false; bool isScanning = false; bool showPassword = false; final ref = FirebaseDatabase.instance.ref("rfid"); StreamSubscription? _rfidSubscription; @override void initState() { super.initState(); _rfidSubscription = ref.child("latest").onValue.listen((event) { final data = event.snapshot.value; if (data != null && mounted) { setState(() { rfid = data.toString().toUpperCase(); isScanning = false; }); } }); } @override void dispose() { _rfidSubscription?.cancel(); nama.dispose(); namaOrtu.dispose(); nomorAbsen.dispose(); emailOrtu.dispose(); passwordOrtu.dispose(); super.dispose(); } Future scanKartu() async { if (nama.text.trim().isEmpty || namaOrtu.text.trim().isEmpty || nomorAbsen.text.trim().isEmpty || emailOrtu.text.trim().isEmpty || passwordOrtu.text.trim().isEmpty) { showMsg("Lengkapi data terlebih dahulu"); return; } setState(() { isScanning = true; rfid = ''; }); await ref.child("mode").set("scan"); } Future simpan() async { if (rfid.isEmpty || isScanning) { showMsg("Scan kartu terlebih dahulu"); return; } setState(() => isLoading = true); try { final user = await FirebaseAuth.instance.createUserWithEmailAndPassword( email: emailOrtu.text.trim(), password: passwordOrtu.text.trim(), ); final uid = user.user!.uid; final namaSiswa = nama.text.trim(); final namaAyahIbu = namaOrtu.text.trim(); final noAbsen = nomorAbsen.text.trim(); await FirebaseFirestore.instance.collection('users').doc(uid).set({ 'role': 'ortu', 'nama': namaAyahIbu, 'nama_siswa': namaSiswa, 'kelas': kelasPilihan, 'rfid': rfid, }); await FirebaseFirestore.instance.collection('siswa').add({ 'nama': namaSiswa, 'nama_orang_tua': namaAyahIbu, 'nomor_absen': noAbsen, 'kelas': kelasPilihan, 'rfid': rfid, 'ortu_uid': uid, }); await FirebaseDatabase.instance.ref("siswa/$rfid").set(true); showMsg("Berhasil ditambahkan"); if (mounted) Navigator.pop(context); } catch (e) { showMsg("Error : $e"); } if (mounted) setState(() => isLoading = false); } void showMsg(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); } // ================= WIDGET COMPONENT (MATCHED WITH DASHBOARD STYLE) ================= Widget header() { 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: [ // Ornamen lingkaran besar sama seperti dashboard Positioned( top: -30, left: -20, child: Container( width: 150, height: 150, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withOpacity(0.15), ), ), ), // Garis gelombang latar belakang Positioned.fill( child: CustomPaint(painter: HeaderWavePainter()), ), // Pola titik grid Positioned(top: 40, left: 24, child: _buildGridDots()), Positioned(top: 60, right: 60, child: _buildGridDots()), // Konten Header Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Row( children: [ // Tombol kembali gaya modern 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( "Tambah Siswa", style: TextStyle( color: Color(0xff12175E), fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: -0.5, ), ), SizedBox(height: 4), Text( "Registrasi data & kartu RFID", 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.person_add_alt_1_rounded, color: Color(0xff7F56D9), 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: 4, bottom: 12, top: 16), child: Row( children: [ Container(width: 24, height: 3, decoration: BoxDecoration(color: const Color(0xff7F56D9), borderRadius: BorderRadius.circular(1.5))), const SizedBox(width: 3), Container(width: 3, height: 3, decoration: const BoxDecoration(color: Color(0xff7F56D9), 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 customInput({ required TextEditingController controller, required IconData icon, required String hint, bool password = false, TextInputType keyboardType = TextInputType.text, }) { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: const Color(0xff7F56D9).withOpacity(0.08), blurRadius: 16, spreadRadius: 0, offset: const Offset(0, 6), ), ], ), child: TextField( controller: controller, keyboardType: keyboardType, obscureText: password ? !showPassword : false, textAlignVertical: TextAlignVertical.center, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xff12175E), ), decoration: InputDecoration( border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(vertical: 18, horizontal: 18), prefixIcon: Container( margin: const EdgeInsets.all(8), width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xff7F56D9).withOpacity(0.12), ), child: Icon( icon, color: const Color(0xff7F56D9), size: 22, ), ), hintText: hint, hintStyle: TextStyle( color: const Color(0xff12175E).withOpacity(0.45), fontSize: 15, ), suffixIcon: password ? IconButton( onPressed: () => setState(() => showPassword = !showPassword), icon: Icon( showPassword ? Icons.visibility : Icons.visibility_off, color: const Color(0xff7F56D9).withOpacity(0.6), size: 20, ), ) : null, ), ), ); } Widget kelasDropdown() { return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: const Color(0xff7F56D9).withOpacity(0.08), blurRadius: 16, spreadRadius: 0, offset: const Offset(0, 6), ), ], ), child: DropdownButtonFormField( value: kelasPilihan, isExpanded: true, icon: const Icon(Icons.arrow_drop_down_circle_outlined, color: Color(0xff7F56D9), size: 22), decoration: InputDecoration( border: InputBorder.none, contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 10), prefixIcon: Container( margin: const EdgeInsets.all(8), width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0xff7F56D9).withOpacity(0.12), ), child: const Icon( Icons.school_outlined, color: Color(0xff7F56D9), size: 22, ), ), ), items: const [ DropdownMenuItem(value: 'TK A', child: Text('TK A', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xff12175E)))), DropdownMenuItem(value: 'TK B', child: Text('TK B', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500, color: Color(0xff12175E)))), ], onChanged: (value) { if (value != null) setState(() => kelasPilihan = value); }, ), ); } Widget rfidCard() { return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: const Color(0xff7F56D9).withOpacity(isScanning ? 0.15 : 0.08), blurRadius: 24, spreadRadius: 0, offset: const Offset(0, 12), ), ], border: Border.all( color: isScanning ? const Color(0xff7F56D9).withOpacity(0.6) : Colors.transparent, width: 1.5, ), ), child: Column( children: [ Container( width: 70, height: 70, decoration: BoxDecoration( shape: BoxShape.circle, color: rfid.isEmpty ? Colors.grey.shade100 : const Color(0xff7F56D9).withOpacity(0.12), boxShadow: [ BoxShadow( color: rfid.isEmpty ? Colors.grey.shade200 : const Color(0xff7F56D9).withOpacity(0.2), blurRadius: 12, spreadRadius: 2, ) ], ), child: Icon( Icons.vignette_rounded, size: 32, color: rfid.isEmpty ? Colors.grey.shade400 : const Color(0xff7F56D9), ), ), const SizedBox(height: 16), if (isScanning) ...[ const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2.5, color: Color(0xff7F56D9))), const SizedBox(height: 12), ], Text( isScanning ? "Mendekatkan kartu ke Reader..." : rfid.isEmpty ? "Belum Ada Kartu Terpindai" : rfid, textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: const Color(0xff12175E).withOpacity(rfid.isEmpty ? 0.5 : 1), letterSpacing: rfid.isEmpty ? 0 : 2, ), ), ], ), ); } Widget scanButton() { return SizedBox( width: double.infinity, height: 52, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: LinearGradient( colors: isScanning ? [Colors.grey.shade300, Colors.grey.shade400] : [const Color(0xff7F56D9), const Color(0xff9181F4)], ), boxShadow: [ BoxShadow( color: isScanning ? Colors.transparent : const Color(0xff7F56D9).withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: ElevatedButton.icon( onPressed: isScanning ? null : scanKartu, icon: const Icon(Icons.sensors_rounded, size: 20, color: Colors.white), label: const Text( "Mulai Pindai RFID", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.white), ), style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), ), ), ); } Widget gradientButton({ required String title, required VoidCallback onTap, bool loading = false, }) { return SizedBox( height: 56, width: double.infinity, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: const LinearGradient( colors: [Color(0xff7F56D9), Color(0xff9181F4)], ), boxShadow: [ BoxShadow( color: const Color(0xff7F56D9).withOpacity(0.35), blurRadius: 14, offset: const Offset(0, 6), ), ], ), child: ElevatedButton( onPressed: loading ? null : onTap, style: ElevatedButton.styleFrom( elevation: 0, backgroundColor: Colors.transparent, shadowColor: Colors.transparent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), ), child: loading ? const SizedBox(height: 24, width: 24, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.5)) : Text( title, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold, color: Colors.white), ), ), ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xffF6F5FB), body: SafeArea( child: Column( children: [ header(), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ sectionTitle("Data Akademik Siswa"), customInput(controller: nama, icon: Icons.person_outline_rounded, hint: "Nama Lengkap Siswa"), customInput(controller: nomorAbsen, icon: Icons.tag_rounded, hint: "Nomor Absen", keyboardType: TextInputType.number), kelasDropdown(), sectionTitle("Data Akun Orang Tua"), customInput(controller: namaOrtu, icon: Icons.supervisor_account_rounded, hint: "Nama Lengkap Orang Tua / Wali"), customInput(controller: emailOrtu, icon: Icons.alternate_email_rounded, hint: "Email untuk Login", keyboardType: TextInputType.emailAddress), customInput(controller: passwordOrtu, icon: Icons.lock_rounded, hint: "Password Akun", password: true), sectionTitle("Integrasi Kartu RFID"), scanButton(), const SizedBox(height: 16), rfidCard(), const SizedBox(height: 36), gradientButton(title: "Simpan & Daftarkan Siswa", onTap: simpan, loading: isLoading), const SizedBox(height: 24), ], ), ), ), ], ), ), ); } } // Reuse same wave painter from Dashboard 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; }