609 lines
20 KiB
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;
|
|
}
|
|
} |