import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../services/auth_service.dart'; import 'laporan_user_screen.dart'; class JadwalUserScreen extends StatefulWidget { const JadwalUserScreen({super.key}); @override State createState() => _JadwalUserScreenState(); } class _JadwalUserScreenState extends State { List> jadwalList = []; bool isLoading = true; String namaUser = ''; int selectedMonth = DateTime.now().month; int selectedYear = DateTime.now().year; List years = [DateTime.now().year]; Future getUserLocal() async { final prefs = await SharedPreferences.getInstance(); setState(() { namaUser = prefs.getString('nama_user') ?? ''; }); } void generateYears() { Set yearSet = {DateTime.now().year}; for (var jadwal in jadwalList) { DateTime date = DateTime.parse(jadwal['tanggal']); yearSet.add(date.year); } List result = yearSet.toList()..sort(); setState(() { years = result; if (!years.contains(selectedYear)) { selectedYear = DateTime.now().year; } }); } Future fetchJadwal() async { String? token = await AuthService.getToken(); if (token == null) { setState(() => isLoading = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Token tidak ditemukan, silakan login ulang')), ); } return; } try { final response = await http.get( Uri.parse("${AuthService.baseUrl}/jadwal"), headers: { "Accept": "application/json", "Authorization": "Bearer $token", }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); List> temp = List>.from(data['jadwal']); // Sort awal dari server: urut tanggal ascending temp.sort((a, b) => DateTime.parse(a['tanggal']) .compareTo(DateTime.parse(b['tanggal']))); setState(() { jadwalList = temp; isLoading = false; }); generateYears(); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Error ${response.statusCode}: ${response.body}')), ); } setState(() => isLoading = false); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Error: $e')), ); } setState(() => isLoading = false); } } // ── Filter + Sort: hari ini di atas, lewat di bawah ── List> get filteredJadwal { if (jadwalList.isEmpty) return []; final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final filtered = jadwalList.where((jadwal) { final date = DateTime.parse(jadwal['tanggal']); return date.month == selectedMonth && date.year == selectedYear; }).toList(); // Prioritas urutan tampilan: // 0 = hari ini (paling atas) // 1 = akan datang // 2 = sudah lewat (paling bawah) int priority(Map jadwal) { final tanggal = DateTime.parse(jadwal['tanggal']); if (tanggal.year == today.year && tanggal.month == today.month && tanggal.day == today.day) return 0; if (tanggal.isAfter(today)) return 1; return 2; } filtered.sort((a, b) { final pa = priority(a); final pb = priority(b); if (pa != pb) return pa.compareTo(pb); // Dalam grup yang sama, urutkan tanggal ascending return DateTime.parse(a['tanggal']) .compareTo(DateTime.parse(b['tanggal'])); }); return filtered; } @override void initState() { super.initState(); loadData(); } Future loadData() async { setState(() => isLoading = true); await getUserLocal(); await fetchJadwal(); } Color getStatusColor(String status) { switch (status.toLowerCase()) { case 'selesai': return const Color(0xFF4CAF50); case 'sedang': return const Color(0xFF2196F3); case 'libur': return Colors.redAccent; default: return const Color(0xFFFF9800); } } String getStatusText(String status) { switch (status.toLowerCase()) { case 'sedang': return "Sedang Patroli"; case 'selesai': return "Selesai Patroli"; case 'libur': return "Libur"; default: return "Belum Patroli"; } } // bool _dalamJamPatroli(String? jamMulaiStr, String? jamSelesaiStr) { // if (jamMulaiStr == null || jamSelesaiStr == null) return false; // final now = DateTime.now(); // final mulaiParts = jamMulaiStr.split(':'); // final selesaiParts = jamSelesaiStr.split(':'); // final jamMulai = DateTime(now.year, now.month, now.day, // int.parse(mulaiParts[0]), int.parse(mulaiParts[1])); // final jamSelesai = DateTime(now.year, now.month, now.day, // int.parse(selesaiParts[0]), int.parse(selesaiParts[1])); // return now.isAfter(jamMulai) && now.isBefore(jamSelesai); // } // bool _belumWaktuPatroli(String? jamMulaiStr) { // if (jamMulaiStr == null) return false; // final now = DateTime.now(); // final mulaiParts = jamMulaiStr.split(':'); // final jamMulai = DateTime(now.year, now.month, now.day, // int.parse(mulaiParts[0]), int.parse(mulaiParts[1])); // return now.isBefore(jamMulai); // } // bool _sudahLewatJamPatroli(String? jamSelesaiStr) { // if (jamSelesaiStr == null) return false; // final now = DateTime.now(); // final selesaiParts = jamSelesaiStr.split(':'); // final jamSelesai = DateTime(now.year, now.month, now.day, // int.parse(selesaiParts[0]), int.parse(selesaiParts[1])); // return now.isAfter(jamSelesai); // } List? _rangePatroli(String? jamMulaiStr, String? jamSelesaiStr) { if (jamMulaiStr == null || jamSelesaiStr == null) return null; final now = DateTime.now(); final mulaiParts = jamMulaiStr.split(':'); final selesaiParts = jamSelesaiStr.split(':'); DateTime jamMulai = DateTime(now.year, now.month, now.day, int.parse(mulaiParts[0]), int.parse(mulaiParts[1])); DateTime jamSelesai = DateTime(now.year, now.month, now.day, int.parse(selesaiParts[0]), int.parse(selesaiParts[1])); if (!jamSelesai.isAfter(jamMulai)) { if (now.isBefore(jamSelesai)) { jamMulai = jamMulai.subtract(const Duration(days: 1)); } else { jamSelesai = jamSelesai.add(const Duration(days: 1)); } } return [jamMulai, jamSelesai]; } bool _dalamJamPatroli(String? jamMulaiStr, String? jamSelesaiStr) { final range = _rangePatroli(jamMulaiStr, jamSelesaiStr); if (range == null) return false; final now = DateTime.now(); return now.isAfter(range[0]) && now.isBefore(range[1]); } bool _belumWaktuPatroli(String? jamMulaiStr, String? jamSelesaiStr) { final range = _rangePatroli(jamMulaiStr, jamSelesaiStr); if (range == null) return false; return DateTime.now().isBefore(range[0]); } bool _sudahLewatJamPatroli(String? jamMulaiStr, String? jamSelesaiStr) { final range = _rangePatroli(jamMulaiStr, jamSelesaiStr); if (range == null) return false; return DateTime.now().isAfter(range[1]); } Widget _buildFilter() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), color: Colors.white, child: Row( children: [ Expanded( child: DropdownButtonFormField( value: selectedMonth, decoration: const InputDecoration( labelText: "Bulan", border: OutlineInputBorder(), isDense: true, ), items: const [ DropdownMenuItem(value: 1, child: Text("Januari")), DropdownMenuItem(value: 2, child: Text("Februari")), DropdownMenuItem(value: 3, child: Text("Maret")), DropdownMenuItem(value: 4, child: Text("April")), DropdownMenuItem(value: 5, child: Text("Mei")), DropdownMenuItem(value: 6, child: Text("Juni")), DropdownMenuItem(value: 7, child: Text("Juli")), DropdownMenuItem(value: 8, child: Text("Agustus")), DropdownMenuItem(value: 9, child: Text("September")), DropdownMenuItem(value: 10, child: Text("Oktober")), DropdownMenuItem(value: 11, child: Text("November")), DropdownMenuItem(value: 12, child: Text("Desember")), ], onChanged: (value) => setState(() => selectedMonth = value!), ), ), const SizedBox(width: 10), Expanded( child: DropdownButtonFormField( value: years.contains(selectedYear) ? selectedYear : years.first, decoration: const InputDecoration( labelText: "Tahun", border: OutlineInputBorder(), isDense: true, ), items: years .map((year) => DropdownMenuItem( value: year, child: Text(year.toString()), )) .toList(), onChanged: (value) => setState(() => selectedYear = value!), ), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF5F7FB), appBar: AppBar( toolbarHeight: 70, title: const Text( 'Jadwal Patroli', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 18, ), ), centerTitle: true, backgroundColor: const Color(0xFF2F5BEA), elevation: 0, iconTheme: const IconThemeData(color: Colors.white), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)), ), ), body: RefreshIndicator( onRefresh: loadData, child: isLoading ? const Center( child: CircularProgressIndicator(color: Color(0xFF2F5BEA)), ) : Column( children: [ _buildFilter(), Expanded(child: _buildContent()), ], ), ), ); } Widget _buildContent() { if (filteredJadwal.isEmpty) { return ListView( children: [ SizedBox(height: MediaQuery.of(context).size.height * 0.3), const Center( child: Column( children: [ Icon(Icons.event_busy, size: 80, color: Colors.grey), SizedBox(height: 16), Text( "Tidak ada jadwal untuk bulan ini", style: TextStyle(color: Colors.grey, fontSize: 16), ), ], ), ), ], ); } return ListView.builder( padding: const EdgeInsets.fromLTRB(20, 10, 20, 20), itemCount: filteredJadwal.length, itemBuilder: (context, index) { final jadwal = filteredJadwal[index]; final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); final tomorrow = today.add(const Duration(days: 1)); final tanggalJadwal = DateTime.parse(jadwal['tanggal']); final bool isToday = tanggalJadwal.year == today.year && tanggalJadwal.month == today.month && tanggalJadwal.day == today.day; final bool isBesok = tanggalJadwal.year == tomorrow.year && tanggalJadwal.month == tomorrow.month && tanggalJadwal.day == tomorrow.day; final bool isLewat = tanggalJadwal.isBefore(today); final bool isFuture = tanggalJadwal.isAfter(today); final status = jadwal['status'] ?? 'belum'; final isLibur = status.toLowerCase() == 'libur'; final laporanCount = jadwal['laporan_count'] ?? 0; final minimumTerpenuhi = jadwal['minimum_terpenuhi'] == true; final isSelesai = minimumTerpenuhi || status.toLowerCase() == 'selesai'; final namaLokasi = isLibur ? 'Hari Libur' : (jadwal['nama_lokasi'] ?? '-'); final jamMulaiStr = jadwal['jam_mulai'] as String?; final jamSelesaiStr = jadwal['jam_selesai'] as String?; final jamDisplay = (jamMulaiStr != null && jamSelesaiStr != null) ? "$jamMulaiStr - $jamSelesaiStr" : null; final namaShift = jadwal['nama_shift'] as String?; bool isDisabled = true; if (isToday && !isLibur) { isDisabled = !_dalamJamPatroli(jamMulaiStr, jamSelesaiStr); } String infoText; Color infoColor; if (isLibur) { infoText = "Hari istirahat, tidak ada patroli"; infoColor = Colors.redAccent; } else if (isSelesai) { infoText = "Minimum patroli terpenuhi"; infoColor = Colors.green; } else if (isToday) { if (_belumWaktuPatroli(jamMulaiStr, jamSelesaiStr)) { infoText = "Belum waktunya patroli (mulai $jamMulaiStr)"; infoColor = Colors.orange; } else if (_sudahLewatJamPatroli(jamMulaiStr, jamSelesaiStr)) { infoText = "Waktu patroli sudah habis"; infoColor = Colors.grey; } else { infoText = "Klik untuk patroli"; infoColor = const Color(0xFF2F5BEA); } } else if (isLewat) { infoText = "Patroli terlewat"; infoColor = Colors.grey; } else if (isFuture) { infoText = "Belum waktunya patroli"; infoColor = Colors.orange; } else { infoText = ""; infoColor = Colors.grey; } return _JadwalCard( lokasi: namaLokasi, jam: jamDisplay, tanggal: jadwal['tanggal'] ?? '-', status: getStatusText(status), laporanCount: laporanCount, minimumTerpenuhi: minimumTerpenuhi, statusColor: getStatusColor(status), isLewat: isLewat, isToday: isToday, isBesok: isBesok, isFuture: isFuture, isLibur: isLibur, infoText: infoText, infoColor: infoColor, onTap: isDisabled ? null : () async { await Navigator.push( context, MaterialPageRoute( builder: (_) => LaporanUserScreen( lokasi: jadwal['nama_lokasi'] ?? '-', tanggal: jadwal['tanggal'] ?? '-', shift: namaShift ?? '-', jadwalId: jadwal['id'].toString(), namaPetugas: namaUser.isEmpty ? '-' : namaUser, ), ), ); fetchJadwal(); }, ); }, ); } } // ════════════════════════════════════════════════════════════════════════════ // CARD WIDGET // ════════════════════════════════════════════════════════════════════════════ class _JadwalCard extends StatelessWidget { final String lokasi; final String? jam; final String tanggal; final String status; final int laporanCount; final bool minimumTerpenuhi; final Color statusColor; final bool isLewat; final bool isToday; final bool isBesok; final bool isFuture; final bool isLibur; final String infoText; final Color infoColor; final VoidCallback? onTap; const _JadwalCard({ required this.lokasi, required this.jam, required this.tanggal, required this.status, required this.laporanCount, required this.minimumTerpenuhi, required this.statusColor, required this.isLewat, required this.isToday, required this.isBesok, required this.isFuture, required this.isLibur, required this.infoText, required this.infoColor, this.onTap, }); @override Widget build(BuildContext context) { Color cardColor = Colors.white; if (isLibur) { cardColor = Colors.red.shade50; } else if (isLewat) { cardColor = Colors.grey.shade200; } else if (isToday) { cardColor = const Color(0xFFE8F0FF); } return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: cardColor, borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(15), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── HEADER: Lokasi + Badge Status ── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( lokasi, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isLibur ? Colors.redAccent : const Color(0xFF2D3243), ), ), ), if (isLibur) _badge("Libur", Colors.redAccent) else _buildStatusBadge(), ], ), if (isToday && !isLibur) _badge("Hari Ini", const Color(0xFF2F5BEA)), if (isBesok && !isLibur) _badge("Besok", Colors.orange), const Divider(height: 24), // ── INFO: Jam & Tanggal ── if (isLibur) _buildInfoItem(Icons.calendar_today_rounded, tanggal) else Row( children: [ if (jam != null) ...[ _buildInfoItem(Icons.access_time_rounded, jam!), const SizedBox(width: 20), ], _buildInfoItem( Icons.calendar_today_rounded, tanggal), ], ), // ── PROGRESS ── if (!isLibur) ...[ const SizedBox(height: 12), Row( children: [ const Text( "Progress Patroli : ", style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, ), ), Text( "$laporanCount / 3 minimum", style: TextStyle( fontSize: 12, color: minimumTerpenuhi ? Colors.green : const Color(0xFF2F5BEA), fontWeight: FontWeight.bold, ), ), ], ), ], const SizedBox(height: 10), // ── ACTION TEXT ── Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Flexible( child: Text( infoText, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: infoColor, ), textAlign: TextAlign.end, ), ), if (onTap != null) const Icon( Icons.chevron_right, size: 16, color: Color(0xFF2F5BEA), ), ], ), ], ), ), ), ), ); } Widget _badge(String text, Color color) { return Container( margin: const EdgeInsets.only(top: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6), ), child: Text( text, style: const TextStyle(color: Colors.white, fontSize: 10), ), ); } Widget _buildStatusBadge() { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: statusColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: statusColor.withOpacity(0.5)), ), child: Text( status, style: TextStyle( color: statusColor, fontSize: 11, fontWeight: FontWeight.w800, ), ), ); } Widget _buildInfoItem(IconData icon, String text) { return Row( children: [ Icon(icon, size: 16, color: Colors.grey[600]), const SizedBox(width: 6), Text(text, style: TextStyle(color: Colors.grey[700], fontSize: 13)), ], ); } }