826 lines
37 KiB
Dart
826 lines
37 KiB
Dart
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<DashboardOrtu> createState() => _DashboardOrtuState();
|
|
}
|
|
|
|
class _DashboardOrtuState extends State<DashboardOrtu> {
|
|
final refAbsen = FirebaseDatabase.instance.ref("absensi");
|
|
final refIzin = FirebaseDatabase.instance.ref("izin");
|
|
|
|
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
|
|
FlutterLocalNotificationsPlugin();
|
|
final Set<String> notified = {};
|
|
|
|
String today() => DateTime.now().toString().substring(0, 10);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
initNotif();
|
|
}
|
|
|
|
Future<void> initNotif() async {
|
|
const android = AndroidInitializationSettings('@mipmap/ic_launcher');
|
|
const settings = InitializationSettings(android: android);
|
|
await flutterLocalNotificationsPlugin.initialize(settings: settings);
|
|
}
|
|
|
|
Future<void> 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<String>(
|
|
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<void> 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<String, dynamic>.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<String, dynamic>.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<String, dynamic>.from(data[tgl]);
|
|
|
|
String jamMasuk = "-", statusMasuk = "tidak_hadir";
|
|
String jamPulang = "-", statusPulang = "tidak_hadir";
|
|
|
|
if (absen['masuk'] != null) {
|
|
final m = Map<String, dynamic>.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<String, dynamic>.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<String, dynamic>.from(raw);
|
|
|
|
if (absenData['masuk'] != null) {
|
|
final m = Map<String, dynamic>.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<String, dynamic>.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)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
} |