presensi/BBS/lib/view/home/daftar_absensi.dart

609 lines
20 KiB
Dart

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<DaftarAbsensiPage> createState() => _DaftarAbsensiPageState();
}
class _DaftarAbsensiPageState extends State<DaftarAbsensiPage> {
DateTime? _selectedDate;
String? _selectedStatus;
String? _selectedClockType;
final ScrollController _scrollController = ScrollController();
Future<List<Attendance>> 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<Attendance> 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<List<Attendance>>(
future: fetchAttendance(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(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<String> 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<String>(
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<String, dynamic> 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;
}
}