873 lines
30 KiB
Dart
873 lines
30 KiB
Dart
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<AbsensiPage> createState() => _AbsensiPageState();
|
|
}
|
|
|
|
class _AbsensiPageState extends State<AbsensiPage> {
|
|
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<void> loadSiswa() async {
|
|
final snap = await FirebaseFirestore.instance.collection('siswa').get();
|
|
setState(() {
|
|
siswaList = snap.docs;
|
|
});
|
|
}
|
|
|
|
Future<void> 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<void> 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<void> 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<void> 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<void> 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<String, dynamic> 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<String, dynamic>.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;
|
|
} |