TKK_E32230254/lib/dashboard_ortu.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)),
),
],
),
);
}
}