TKK_E32230254/lib/laporan_page.dart

979 lines
42 KiB
Dart

import 'dart:io';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:excel/excel.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
class LaporanPage extends StatefulWidget {
const LaporanPage({super.key});
@override
State<LaporanPage> createState() => _LaporanPageState();
}
class _LaporanPageState extends State<LaporanPage> {
final refAbsen = FirebaseDatabase.instance.ref("absensi");
List siswaList = [];
String selectedFilter = "harian";
DateTime selectedDate = DateTime.now();
DateTime? selectedMonth;
DateTime? selectedYear;
@override
void initState() {
super.initState();
loadSiswa();
}
// ================= LOAD DATA SISWA =================
Future<void> loadSiswa() async {
final snap = await FirebaseFirestore.instance.collection("siswa").get();
setState(() {
siswaList = snap.docs;
});
}
// ================= WARNA STATUS SESUAI TEMA =================
Color getStatusTextColor(String status) {
switch (status) {
case "tepat_waktu":
return const Color(0xff16A34A);
case "telat":
return const Color(0xffEA580C);
case "izin":
return const Color(0xff7C3AED);
case "sakit":
return const Color(0xff9333EA);
case "pulang":
return const Color(0xff0284C7);
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 "izin":
return const Color(0xffFAF5FF);
case "sakit":
return const Color(0xffF3E8FF);
case "pulang":
return const Color(0xffF0F9FF);
default:
return const Color(0xffFEF2F2);
}
}
// ================= ICON STATUS =================
IconData getStatusIcon(String status) {
switch (status) {
case "tepat_waktu":
return Icons.check_circle_rounded;
case "telat":
return Icons.warning_rounded;
case "izin":
return Icons.description_rounded;
case "sakit":
return Icons.local_hospital_rounded;
case "pulang":
return Icons.exit_to_app_rounded;
default:
return Icons.cancel_rounded;
}
}
// ================= UBAH NAMA STATUS =================
String getStatusLabel(String status) {
switch (status) {
case "tepat_waktu":
return "Tepat Waktu";
case "telat":
return "Telat";
case "izin":
return "Izin";
case "sakit":
return "Sakit";
case "pulang":
return "Pulang";
default:
return "Tidak Hadir";
}
}
// ================= HAPUS RIWAYAT =================
Future<void> hapusRiwayat() async {
try {
await refAbsen.remove();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Semua riwayat absensi berhasil dihapus"),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Gagal menghapus riwayat: $e"),
backgroundColor: const Color(0xffDC2626),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
),
);
}
}
}
// ================= KONFIRMASI HAPUS =================
void showHapusDialog() {
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: const Text(
"Hapus Riwayat Absensi",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff12175E),
),
),
content: const Text(
"Apakah Anda yakin ingin menghapus SEMUA riwayat absensi? Tindakan ini tidak dapat dibatalkan.",
style: TextStyle(color: Color(0xff12175E), height: 1.4),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(
"Batal",
style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w600),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffDC2626),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
onPressed: () {
Navigator.pop(context);
hapusRiwayat();
},
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.delete_rounded, size: 18, color: Colors.white),
SizedBox(width: 6),
Text("Hapus", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
),
),
],
),
);
}
DateTime? parseDateKey(String value) {
final normalized = value.replaceAll('/', '-');
return DateTime.tryParse(normalized);
}
String formatDateKey(DateTime value) {
return "${value.year.toString().padLeft(4, '0')}-${value.month.toString().padLeft(2, '0')}-${value.day.toString().padLeft(2, '0')}";
}
bool isDateInSelectedFilter(String tanggal) {
final parsed = parseDateKey(tanggal);
if (parsed == null) {
return false;
}
switch (selectedFilter) {
case "harian":
return parsed.year == selectedDate.year &&
parsed.month == selectedDate.month &&
parsed.day == selectedDate.day;
case "mingguan": {
final startOfWeek = selectedDate.subtract(Duration(days: selectedDate.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
return !parsed.isBefore(startOfWeek) && !parsed.isAfter(endOfWeek);
}
case "bulanan":
return parsed.year == selectedDate.year && parsed.month == selectedDate.month;
case "tahunan":
return parsed.year == selectedDate.year;
default:
return true;
}
}
List<String> getFilteredDates(Map<dynamic, dynamic> absen) {
final dates = <String>{};
absen.forEach((uid, value) {
if (value is Map) {
value.forEach((tanggal, detail) {
if (tanggal is String && isDateInSelectedFilter(tanggal)) {
dates.add(tanggal);
}
});
}
});
final sorted = dates.toList()..sort((a, b) => b.compareTo(a));
return sorted;
}
void moveFilter(int step) {
setState(() {
switch (selectedFilter) {
case 'harian':
selectedDate = selectedDate.add(Duration(days: step));
break;
case 'mingguan':
selectedDate = selectedDate.add(Duration(days: step * 7));
break;
case 'bulanan':
selectedDate = DateTime(selectedDate.year, selectedDate.month + step, 1);
break;
case 'tahunan':
selectedDate = DateTime(selectedDate.year + step, 1, 1);
break;
}
});
}
String getFilterLabel() {
switch (selectedFilter) {
case 'harian':
return 'Tanggal ${formatDateKey(selectedDate)}';
case 'mingguan':
final startOfWeek = selectedDate.subtract(Duration(days: selectedDate.weekday - 1));
final endOfWeek = startOfWeek.add(const Duration(days: 6));
return '${formatDateKey(startOfWeek)} - ${formatDateKey(endOfWeek)}';
case 'bulanan':
return '${_monthName(selectedDate.month)} ${selectedDate.year}';
case 'tahunan':
return '${selectedDate.year}';
default:
return 'Filter';
}
}
String _monthName(int month) {
const months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
return months[month - 1];
}
Future<void> exportToExcel(Map<dynamic, dynamic> absen) async {
try {
final excel = Excel.createExcel();
final sheet = excel['Laporan Absensi'];
final filteredDates = getFilteredDates(absen);
sheet.appendRow([
TextCellValue('Nama'),
TextCellValue('Kelas'),
TextCellValue('Tanggal'),
TextCellValue('Status Masuk'),
TextCellValue('Jam Masuk'),
TextCellValue('Status Pulang'),
TextCellValue('Jam Pulang'),
]);
for (final tanggal in filteredDates) {
for (final siswa in siswaList) {
final uid = siswa['rfid'];
final nama = siswa['nama'] ?? '-';
final kelas = siswa['kelas'] ?? '-';
String statusMasuk = 'tidak_hadir';
String jamMasuk = '-';
String statusPulang = 'tidak_hadir';
String jamPulang = '-';
if (absen[uid] != null && absen[uid][tanggal] != null) {
final dataHariIni = Map<String, dynamic>.from(absen[uid][tanggal]);
if (dataHariIni['masuk'] != null) {
final masuk = Map<String, dynamic>.from(dataHariIni['masuk']);
statusMasuk = masuk['status'] ?? 'tidak_hadir';
jamMasuk = masuk['jam'] ?? '-';
}
if (dataHariIni['pulang'] != null) {
final pulang = Map<String, dynamic>.from(dataHariIni['pulang']);
statusPulang = pulang['status'] ?? 'tidak_hadir';
jamPulang = pulang['jam'] ?? '-';
}
}
sheet.appendRow([
TextCellValue(nama.toString()),
TextCellValue(kelas.toString()),
TextCellValue(tanggal.toString()),
TextCellValue(getStatusLabel(statusMasuk)),
TextCellValue(jamMasuk),
TextCellValue(getStatusLabel(statusPulang)),
TextCellValue(jamPulang),
]);
}
}
final bytes = excel.encode();
if (bytes == null) {
throw Exception('Gagal membuat file Excel');
}
final dir = await getDownloadsDirectory();
if (dir == null) {
throw Exception('Folder Downloads tidak tersedia');
}
final fileName = 'laporan_absensi_${DateTime.now().millisecondsSinceEpoch}.xlsx';
final file = File('${dir.path}/$fileName');
await file.writeAsBytes(bytes);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('File Excel berhasil disimpan di ${file.path}'),
behavior: SnackBarBehavior.floating,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal mengekspor Excel: $e'),
backgroundColor: const Color(0xffDC2626),
behavior: SnackBarBehavior.floating,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
),
);
}
}
}
// ================= HEADER DESAIN SAMA =================
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: [
// Ornamen lingkaran
Positioned(
top: -30,
left: -20,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.15),
),
),
),
// Garis gelombang
Positioned.fill(
child: CustomPaint(painter: HeaderWavePainter()),
),
// Pola titik
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
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(
"Laporan Absensi",
style: TextStyle(
color: Color(0xff12175E),
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: -0.5,
),
),
SizedBox(height: 4),
Text(
"Riwayat kehadiran siswa",
style: TextStyle(
color: Color(0xff12175E),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 0.7,
),
),
],
),
),
],
),
),
],
),
),
);
}
Widget _buildGridDots() {
return Opacity(
opacity: 0.25,
child: Column(
children: List.generate(4, (_) => Row(
children: List.generate(4, (_) => 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(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,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xffF6F5FB),
body: SafeArea(
child: Column(
children: [
header(context),
Expanded(
child: StreamBuilder(
stream: refAbsen.onValue,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator(color: Color(0xff7F56D9)));
}
final absen = snapshot.data!.snapshot.value as Map? ?? {};
if (siswaList.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.person_off_rounded, size: 60, color: Color(0xff12175E)),
SizedBox(height: 12),
Text("Data siswa belum dimuat", style: TextStyle(color: Color(0xff12175E))),
],
),
);
}
final tanggalList = getFilteredDates(absen);
return Column(
children: [
Container(
margin: const EdgeInsets.fromLTRB(24, 16, 24, 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: const Color(0xff7F56D9).withOpacity(0.08),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Expanded(
child: Text(
'Filter laporan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: Color(0xff12175E),
),
),
),
IconButton(
onPressed: () => exportToExcel(absen),
icon: const Icon(Icons.download_rounded, color: Color(0xff7F56D9)),
tooltip: 'Download file Excel',
),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ChoiceChip(
label: const Text('Harian'),
selected: selectedFilter == 'harian',
onSelected: (_) => setState(() => selectedFilter = 'harian'),
selectedColor: const Color(0xff7F56D9),
labelStyle: TextStyle(color: selectedFilter == 'harian' ? Colors.white : const Color(0xff12175E)),
),
ChoiceChip(
label: const Text('Mingguan'),
selected: selectedFilter == 'mingguan',
onSelected: (_) => setState(() => selectedFilter = 'mingguan'),
selectedColor: const Color(0xff7F56D9),
labelStyle: TextStyle(color: selectedFilter == 'mingguan' ? Colors.white : const Color(0xff12175E)),
),
ChoiceChip(
label: const Text('Bulanan'),
selected: selectedFilter == 'bulanan',
onSelected: (_) => setState(() => selectedFilter = 'bulanan'),
selectedColor: const Color(0xff7F56D9),
labelStyle: TextStyle(color: selectedFilter == 'bulanan' ? Colors.white : const Color(0xff12175E)),
),
ChoiceChip(
label: const Text('Tahunan'),
selected: selectedFilter == 'tahunan',
onSelected: (_) => setState(() => selectedFilter = 'tahunan'),
selectedColor: const Color(0xff7F56D9),
labelStyle: TextStyle(color: selectedFilter == 'tahunan' ? Colors.white : const Color(0xff12175E)),
),
],
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xffF6F5FB),
borderRadius: BorderRadius.circular(14),
),
child: Row(
children: [
IconButton(
onPressed: () => moveFilter(-1),
icon: const Icon(Icons.chevron_left_rounded, color: Color(0xff7F56D9)),
),
Expanded(
child: Text(
getFilterLabel(),
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Color(0xff12175E),
),
),
),
IconButton(
onPressed: () => moveFilter(1),
icon: const Icon(Icons.chevron_right_rounded, color: Color(0xff7F56D9)),
),
],
),
),
],
),
),
Expanded(
child: tanggalList.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history_rounded, size: 60, color: Color(0xff12175E)),
SizedBox(height: 12),
Text(
"Belum ada riwayat absensi pada periode ini",
style: TextStyle(color: Color(0xff12175E)),
),
],
),
)
: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
itemCount: tanggalList.length,
itemBuilder: (context, index) {
final tanggal = tanggalList[index];
return Container(
margin: const EdgeInsets.only(bottom: 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: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
iconColor: const Color(0xff7F56D9),
collapsedIconColor: const Color(0xff7F56D9),
tilePadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xff7F56D9).withOpacity(0.12),
borderRadius: BorderRadius.circular(14),
),
child: const Icon(
Icons.calendar_month_rounded,
color: Color(0xff7F56D9),
size: 24,
),
),
title: Text(
tanggal,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
color: Color(0xff12175E),
),
),
children: siswaList.map((doc) {
final uid = doc['rfid'];
final nama = doc['nama'];
final kelas = doc['kelas'] ?? "-";
String statusMasuk = "tidak_hadir";
String jamMasuk = "-";
String statusPulang = "tidak_hadir";
String jamPulang = "-";
if (absen[uid] != null && absen[uid][tanggal] != null) {
final dataHariIni = Map<String, dynamic>.from(absen[uid][tanggal]);
if (dataHariIni['masuk'] != null) {
final masuk = Map<String, dynamic>.from(dataHariIni['masuk']);
statusMasuk = masuk['status'] ?? "tidak_hadir";
jamMasuk = masuk['jam'] ?? "-";
}
if (dataHariIni['pulang'] != null) {
final pulang = Map<String, dynamic>.from(dataHariIni['pulang']);
statusPulang = pulang['status'] ?? "tidak_hadir";
jamPulang = pulang['jam'] ?? "-";
}
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xffF6F5FB),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: getStatusBgColor(statusMasuk),
shape: BoxShape.circle,
),
child: Icon(
getStatusIcon(statusMasuk),
color: getStatusTextColor(statusMasuk),
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: Color(0xff12175E),
),
),
const SizedBox(height: 2),
Text(
"Kelas: $kelas",
style: TextStyle(
fontSize: 13,
color: const Color(0xff12175E).withOpacity(0.6),
),
),
],
),
),
],
),
const SizedBox(height: 14),
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: getStatusBgColor(statusMasuk),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Masuk",
style: TextStyle(
color: getStatusTextColor(statusMasuk).withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
getStatusLabel(statusMasuk),
style: TextStyle(
color: getStatusTextColor(statusMasuk),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
const SizedBox(height: 2),
Text(
jamMasuk,
style: TextStyle(
fontSize: 12,
color: const Color(0xff12175E).withOpacity(0.6),
),
),
],
),
),
),
const SizedBox(width: 10),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration(
color: getStatusBgColor(statusPulang),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Pulang",
style: TextStyle(
color: getStatusTextColor(statusPulang).withOpacity(0.8),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
getStatusLabel(statusPulang),
style: TextStyle(
color: getStatusTextColor(statusPulang),
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
const SizedBox(height: 2),
Text(
jamPulang,
style: TextStyle(
fontSize: 12,
color: const Color(0xff12175E).withOpacity(0.6),
),
),
],
),
),
),
],
),
],
),
);
}).toList(),
),
),
);
},
),
),
// ✅ TOMBOL HAPUS DIPINDAH KE BAWAH
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton.icon(
onPressed: showHapusDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffDC2626),
foregroundColor: Colors.white,
elevation: 2,
shadowColor: const Color(0xffDC2626).withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
),
icon: const Icon(Icons.delete_rounded, size: 20),
label: const Text(
"Hapus Semua Riwayat",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
],
);
},
),
),
],
),
),
);
}
}
// Widget gelombang yang sama
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;
}