381 lines
11 KiB
Dart
381 lines
11 KiB
Dart
// lib/features/attendance/attendance_screen.dart
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:smpn1_lengkong/core/models/attendance_log_model.dart';
|
|
import 'package:smpn1_lengkong/core/models/student_model.dart';
|
|
import 'package:smpn1_lengkong/core/constants/app_colors.dart';
|
|
import 'package:smpn1_lengkong/core/service/student_service.dart';
|
|
import 'package:smpn1_lengkong/features/attendance/widgets/attendance_history_card.dart';
|
|
|
|
class AttendanceScreen extends StatefulWidget {
|
|
const AttendanceScreen({super.key});
|
|
|
|
@override
|
|
State<AttendanceScreen> createState() => _AttendanceScreenState();
|
|
}
|
|
|
|
class _AttendanceScreenState extends State<AttendanceScreen> {
|
|
// Simpan student di state agar data bisa diupdate saat refresh
|
|
StudentModel? _studentData;
|
|
String?
|
|
_currentClassFilter; // Variabel pengunci kelas agar tidak reset ke default
|
|
String _filter = 'Semua';
|
|
int? _selectedMonthIndex;
|
|
bool _isInitialized = false;
|
|
|
|
final List<String> _months = [
|
|
'Januari',
|
|
'Februari',
|
|
'Maret',
|
|
'April',
|
|
'Mei',
|
|
'Juni',
|
|
'Juli',
|
|
'Agustus',
|
|
'September',
|
|
'Oktober',
|
|
'November',
|
|
'Desember',
|
|
];
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// Inisialisasi data awal dari arguments
|
|
if (!_isInitialized) {
|
|
final args = ModalRoute.of(context)!.settings.arguments as StudentModel;
|
|
_studentData = args;
|
|
_currentClassFilter =
|
|
args.className; // Mengunci kelas berdasarkan argumen
|
|
_isInitialized = true;
|
|
}
|
|
}
|
|
|
|
/// Fungsi untuk menarik data terbaru dari API/Firestore
|
|
Future<void> _handleRefresh() async {
|
|
if (_studentData == null) return;
|
|
|
|
try {
|
|
// Mengambil data student terbaru menggunakan StudentService
|
|
final updatedStudent = await StudentService().getCurrentStudentData();
|
|
|
|
setState(() {
|
|
// PERBAIKAN: Gunakan copyWith agar className tetap pada kelas yang dipilih user
|
|
_studentData = updatedStudent.copyWith(className: _currentClassFilter);
|
|
});
|
|
|
|
if (mounted) {}
|
|
} catch (e) {
|
|
if (mounted) {}
|
|
}
|
|
}
|
|
|
|
List<AttendanceLog> _getFilteredLogs(
|
|
List<AttendanceLog> allLogs,
|
|
String viewingClass,
|
|
) {
|
|
// 1. Filter berdasarkan kelas yang sedang dikunci
|
|
final classFiltered =
|
|
allLogs.where((log) => log.className == viewingClass).toList();
|
|
|
|
// 2. Sort data terbaru paling atas
|
|
classFiltered.sort((a, b) => b.date.compareTo(a.date));
|
|
|
|
// 3. Filter berdasarkan periode/bulan
|
|
return classFiltered.where((log) {
|
|
if (_filter.startsWith('Bulan:')) {
|
|
return log.date.month == _selectedMonthIndex;
|
|
}
|
|
return true;
|
|
}).toList();
|
|
}
|
|
|
|
Map<String, int> _calculateSummary(List<AttendanceLog> filteredLogs) {
|
|
int present = 0, sick = 0, permit = 0, alpha = 0;
|
|
for (var log in filteredLogs) {
|
|
if (log.status == 'Hadir') present++;
|
|
if (log.status == 'Sakit') sick++;
|
|
if (log.status == 'Izin') permit++;
|
|
if (log.status == 'Alpha') alpha++;
|
|
}
|
|
return {'Hadir': present, 'Sakit': sick, 'Izin': permit, 'Alpha': alpha};
|
|
}
|
|
|
|
void _showMonthPicker(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: const Text("Pilih Bulan"),
|
|
content: SizedBox(
|
|
width: double.maxFinite,
|
|
child: GridView.builder(
|
|
shrinkWrap: true,
|
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: 3,
|
|
childAspectRatio: 2,
|
|
mainAxisSpacing: 10,
|
|
crossAxisSpacing: 10,
|
|
),
|
|
itemCount: _months.length,
|
|
itemBuilder: (context, index) {
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
_selectedMonthIndex = index + 1;
|
|
_filter = "Bulan: ${_months[index]}";
|
|
});
|
|
Navigator.pop(context);
|
|
},
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
border: Border.all(
|
|
color: AppColors.primaryBlue.withOpacity(0.3),
|
|
),
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
),
|
|
child: Text(
|
|
_months[index],
|
|
style: TextStyle(fontSize: 12.sp),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Pastikan data tersedia
|
|
if (_studentData == null) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
|
|
// Menggunakan variabel pengunci sebagai acuan kelas yang dilihat
|
|
final viewingClass = _currentClassFilter!;
|
|
final filteredLogs = _getFilteredLogs(
|
|
_studentData!.attendanceLogs,
|
|
viewingClass,
|
|
);
|
|
final summaryCounts = _calculateSummary(filteredLogs);
|
|
|
|
return Scaffold(
|
|
backgroundColor: AppColors.backgroundWhite,
|
|
appBar: AppBar(
|
|
title: Text(
|
|
"Kehadiran Kelas $viewingClass",
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
color: AppColors.primaryBlue,
|
|
child: Column(
|
|
children: [
|
|
_buildSummaryHeader(summaryCounts),
|
|
|
|
// Filter Buttons
|
|
Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 16.h),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_buildFilterButton("Semua"),
|
|
SizedBox(width: 12.w),
|
|
_buildMonthFilterTrigger(),
|
|
],
|
|
),
|
|
),
|
|
|
|
Expanded(
|
|
child:
|
|
filteredLogs.isEmpty
|
|
? _buildEmptyStateScrollable()
|
|
: ListView.builder(
|
|
physics: const AlwaysScrollableScrollPhysics(
|
|
parent: BouncingScrollPhysics(),
|
|
),
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
|
itemCount: filteredLogs.length,
|
|
itemBuilder: (context, index) {
|
|
return AttendanceHistoryCard(
|
|
log: filteredLogs[index],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMonthFilterTrigger() {
|
|
final bool isMonthSelected = _filter.contains("Bulan:");
|
|
return InkWell(
|
|
onTap: () => _showMonthPicker(context),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
|
|
decoration: BoxDecoration(
|
|
color: isMonthSelected ? AppColors.primaryBlue : Colors.white,
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.filter_list,
|
|
size: 16.sp,
|
|
color: isMonthSelected ? Colors.white : Colors.grey.shade600,
|
|
),
|
|
SizedBox(width: 6.w),
|
|
Text(
|
|
"Pilih Bulan",
|
|
style: TextStyle(
|
|
color: isMonthSelected ? Colors.white : Colors.grey.shade600,
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryHeader(Map<String, int> summaryCounts) {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 24.w),
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.primaryBlue,
|
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
"Periode: $_filter",
|
|
style: TextStyle(color: Colors.white70, fontSize: 12.sp),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
_buildSummaryItem(
|
|
"Hadir",
|
|
summaryCounts['Hadir']!,
|
|
AppColors.successGreen,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
_buildSummaryItem(
|
|
"Sakit",
|
|
summaryCounts['Sakit']!,
|
|
Colors.orange,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
_buildSummaryItem(
|
|
"Izin",
|
|
summaryCounts['Izin']!,
|
|
Colors.blueAccent,
|
|
),
|
|
SizedBox(width: 12.w),
|
|
_buildSummaryItem(
|
|
"Alpha",
|
|
summaryCounts['Alpha']!,
|
|
AppColors.dangerRed,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSummaryItem(String label, int count, Color color) {
|
|
return Column(
|
|
children: [
|
|
Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 12.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(16.r),
|
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
|
),
|
|
child: Text(
|
|
count.toString(),
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterButton(String label) {
|
|
final isSelected = _filter == label;
|
|
return InkWell(
|
|
onTap: () => setState(() => _filter = label),
|
|
child: Container(
|
|
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 8.h),
|
|
decoration: BoxDecoration(
|
|
color: isSelected ? AppColors.primaryBlue : Colors.white,
|
|
borderRadius: BorderRadius.circular(20.r),
|
|
border: Border.all(
|
|
color: isSelected ? AppColors.primaryBlue : Colors.grey.shade300,
|
|
),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: isSelected ? Colors.white : Colors.grey.shade600,
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyStateScrollable() {
|
|
return ListView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
children: [
|
|
SizedBox(height: 100.h),
|
|
Icon(
|
|
Icons.calendar_month_outlined,
|
|
size: 60.sp,
|
|
color: Colors.grey.shade300,
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
"Tidak ada data absensi\npada periode ini",
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(color: Colors.grey.shade500),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|