import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:intl/intl.dart'; import 'package:qyuota/config/colors.dart'; import 'package:qyuota/config/api_config.dart'; import 'package:qyuota/services/api_service.dart'; import 'package:qyuota/services/auth_service.dart'; class DaftarAbsensiPage extends StatefulWidget { const DaftarAbsensiPage({Key? key}) : super(key: key); @override State createState() => _DaftarAbsensiPageState(); } class _DaftarAbsensiPageState extends State { DateTime? _selectedDate; String? _selectedStatus; String? _selectedClockType; final ScrollController _scrollController = ScrollController(); Future> fetchAttendance() async { try { final token = await AuthService().getToken(); if (token == null) { throw Exception('Token not found. Please login again.'); } final response = await http.get( Uri.parse('${ApiConfig.baseUrl}/api/mobile/presensi'), headers: ApiConfig.authHeaders(token), ); if (response.statusCode == 200) { final jsonResponse = json.decode(response.body); if (jsonResponse['success'] == true && jsonResponse['data'] != null) { List data = jsonResponse['data']; List attendances = data.map((data) => Attendance.fromJson(data)).toList(); return attendances.where((attendance) { bool matchesDate = true; bool matchesStatus = true; bool matchesClockType = true; if (_selectedDate != null) { matchesDate = DateFormat('yyyy-MM-dd').format(DateTime.parse(attendance.date)) == DateFormat('yyyy-MM-dd').format(_selectedDate!); } if (_selectedStatus != null) { matchesStatus = attendance.status.toLowerCase() == _selectedStatus!.toLowerCase(); } if (_selectedClockType != null) { matchesClockType = attendance.clockType.toLowerCase() == _selectedClockType!.toLowerCase(); } return matchesDate && matchesStatus && matchesClockType; }).toList(); } } throw Exception(json.decode(response.body)['message'] ?? 'Failed to load attendance'); } catch (e) { print('Error fetching attendance: $e'); // Add logging throw Exception('Failed to load attendance: ${e.toString()}'); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[50], appBar: AppBar( elevation: 0, backgroundColor: ConstColors.primaryColor, leading: IconButton( icon: const Icon(Icons.arrow_back_ios, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), title: const Text( "Daftar Absensi", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), centerTitle: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( bottom: Radius.circular(20), ), ), ), body: Column( children: [ _buildFilters(), Expanded( child: RefreshIndicator( onRefresh: () async { setState(() {}); }, child: FutureBuilder>( future: fetchAttendance(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(ConstColors.primaryColor), ), ); } else if (snapshot.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 60, color: Colors.red[300]), const SizedBox(height: 16), Text( 'Terjadi kesalahan', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), const SizedBox(height: 8), Text( snapshot.error.toString(), style: TextStyle(color: Colors.grey[600]), textAlign: TextAlign.center, ), ], ), ); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.event_busy, size: 60, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Tidak ada data absensi', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), const SizedBox(height: 8), Text( 'Silakan pilih filter lain', style: TextStyle(color: Colors.grey[600]), ), ], ), ); } else { return ListView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), itemCount: snapshot.data!.length, itemBuilder: (context, index) { final item = snapshot.data![index]; return _buildAttendanceCard(item); }, ); } }, ), ), ), ], ), ); } Widget _buildFilters() { return Container( margin: const EdgeInsets.all(16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 10, offset: const Offset(0, 1), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Filter', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: ConstColors.primaryColor, ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: InkWell( onTap: () async { final date = await showDatePicker( context: context, initialDate: _selectedDate ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: ConstColors.primaryColor, ), ), child: child!, ); }, ); if (date != null) { setState(() => _selectedDate = date); } }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey[300]!), ), child: Row( children: [ Icon(Icons.calendar_today, size: 20, color: Colors.grey[600]), const SizedBox(width: 8), Text( _selectedDate == null ? 'Pilih Tanggal' : DateFormat('dd/MM/yyyy').format(_selectedDate!), style: TextStyle(color: Colors.grey[700]), ), ], ), ), ), ), const SizedBox(width: 8), Container( decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(10), ), child: IconButton( icon: Icon(Icons.refresh, color: Colors.grey[600]), onPressed: () { setState(() { _selectedDate = null; _selectedStatus = null; _selectedClockType = null; }); }, ), ), ], ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildDropdown( value: _selectedStatus, hint: 'Status', items: ['Hadir', 'Terlambat', 'Sakit', 'Izin'], onChanged: (value) => setState(() => _selectedStatus = value), ), ), const SizedBox(width: 8), Expanded( child: _buildDropdown( value: _selectedClockType, hint: 'Tipe', items: ['in', 'out'], onChanged: (value) => setState(() => _selectedClockType = value), itemBuilder: (item) => item.toUpperCase(), ), ), ], ), ], ), ); } Widget _buildDropdown({ required String? value, required String hint, required List items, required Function(String?) onChanged, String Function(String)? itemBuilder, }) { return Container( decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey[300]!), ), child: DropdownButtonHideUnderline( child: DropdownButtonFormField( decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), border: InputBorder.none, ), value: value, hint: Text(hint, style: TextStyle(color: Colors.grey[600])), items: items.map((item) => DropdownMenuItem( value: item, child: Text( itemBuilder?.call(item) ?? item, style: TextStyle(color: Colors.grey[700]), ), )).toList(), onChanged: onChanged, icon: Icon(Icons.arrow_drop_down, color: Colors.grey[600]), isExpanded: true, dropdownColor: Colors.white, ), ), ); } Widget _buildAttendanceCard(Attendance item) { return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), spreadRadius: 1, blurRadius: 10, offset: const Offset(0, 1), ), ], ), child: Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: ConstColors.primaryColor.withOpacity(0.05), borderRadius: const BorderRadius.vertical(top: Radius.circular(15)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.event, size: 20, color: Colors.grey[700], ), const SizedBox(width: 8), Text( DateFormat('dd MMMM yyyy').format( DateTime.parse(item.date), ), style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.grey[800], ), ), ], ), _buildStatusBadge(item.status), ], ), ), Padding( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ item.status.toLowerCase() == 'izin' || item.status.toLowerCase() == 'sakit' ? _buildAbsenceInfo( item.status, item.time, item.status.toLowerCase() == 'izin' ? Icons.event_note : Icons.healing, item.status.toLowerCase() == 'izin' ? Colors.purple : Colors.red, ) : _buildTimeInfo( item.clockType == 'in' ? 'Masuk' : 'Keluar', item.time, item.clockType == 'in' ? Icons.login : Icons.logout, item.clockType == 'in' ? Colors.green : Colors.red, ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( 'Keterangan', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( item.keterangan, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, ), ), ], ), ], ), ), ], ), ); } Widget _buildStatusBadge(String status) { Color color; IconData icon; switch (status.toLowerCase()) { case 'hadir': color = Colors.green; icon = Icons.check_circle; break; case 'terlambat': color = Colors.orange; icon = Icons.warning; break; case 'izin': color = Colors.purple; icon = Icons.event_note; break; case 'sakit': color = Colors.red; icon = Icons.healing; break; default: color = Colors.blue; icon = Icons.info; } return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: color.withOpacity(0.5)), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(width: 4), Text( status, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ); } Widget _buildTimeInfo(String label, String time, IconData icon, Color color) { return Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 20), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( time, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ], ); } Widget _buildAbsenceInfo(String label, String time, IconData icon, Color color) { return Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 20), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), const SizedBox(height: 4), Text( time, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ], ), ], ); } } class Attendance { final String date; final String status; final String clockType; final String keterangan; final String foto; final double latitude; final double longitude; final String time; Attendance({ required this.date, required this.status, required this.clockType, required this.keterangan, required this.foto, required this.latitude, required this.longitude, }) : time = DateFormat('HH:mm').format( DateTime.parse(date).toLocal() // Konversi ke zona waktu lokal ); factory Attendance.fromJson(Map json) { try { return Attendance( date: json['created_at'] ?? DateTime.now().toIso8601String(), status: json['status'] ?? 'Unknown', clockType: json['clock_type'] ?? 'in', keterangan: json['keterangan'] ?? '-', foto: json['photo'] ?? '', latitude: _parseDouble(json['latitude']) ?? 0.0, longitude: _parseDouble(json['longitude']) ?? 0.0, ); } catch (e) { print('Error parsing attendance data: $e'); print('JSON data: $json'); rethrow; } } static double? _parseDouble(dynamic value) { if (value == null) return null; if (value is num) return value.toDouble(); if (value is String) { try { return double.parse(value); } catch (e) { return null; } } return null; } }