import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../../../services/auth_service.dart'; import '../../../services/notification_service.dart'; import '../../../screens/dashboard/user/notification_screen.dart'; class UserHome extends StatefulWidget { const UserHome({super.key}); @override State createState() => _UserHomeState(); } class _UserHomeState extends State { late String _timeString; late Timer _timer; late Timer _notifTimer; String? fotoUser; String namaUser = '-'; String shift = '-'; String jamShift = '-'; String lokasiShift = '-'; String laporanJudul = '-'; String laporanWaktu = '-'; String laporanDeskripsi = '-'; int _unreadCount = 0; @override void initState() { super.initState(); _timeString = DateFormat('HH:mm:ss').format(DateTime.now()); _timer = Timer.periodic( const Duration(seconds: 1), (Timer t) => _updateTime(), ); loadData(); _notifTimer = Timer.periodic(const Duration(seconds: 5), (timer) { _loadUnreadCount(); }); } Future loadData() async { await getUser(); await fetchJadwal(); await fetchLaporanTerakhir(); await NotificationService.checkJadwalNotification(); await NotificationService.checkLaporanDitolakNotification(); await _loadUnreadCount(); } Future _loadUnreadCount() async { final list = await NotificationService.getLocalNotifications(); if (mounted) { setState(() { _unreadCount = list.where((n) => !n.isRead).length; }); } } @override void dispose() { _timer.cancel(); _notifTimer.cancel(); super.dispose(); } void _updateTime() { final formattedTime = DateFormat('HH:mm:ss').format(DateTime.now()); if (mounted) { setState(() { _timeString = formattedTime; }); } } String getTanggalHari() { final now = DateTime.now(); return DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(now); } Future getUser() async { final prefs = await SharedPreferences.getInstance(); namaUser = prefs.getString('nama_user') ?? '-'; fotoUser = prefs.getString('foto'); if (fotoUser == null || fotoUser == '') { final result = await AuthService.getProfile(); if (result['success']) { fotoUser = result['data']['foto']; } } if (mounted) setState(() {}); } Future fetchJadwal() async { String? token = await AuthService.getToken(); 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); debugPrint("JADWAL RESPONSE: ${response.body}"); if (data['jadwal'] != null && data['jadwal'].length > 0) { final now = DateTime.now(); final today = DateFormat( 'yyyy-MM-dd', ).format(now); // semua shift hari ini final jadwalHariIni = data['jadwal'] .where((j) => j['tanggal'] == today && j['status_jadwal'] != 'libur' && j['nama_shift'] != null) .toList(); if (jadwalHariIni.isEmpty) return; dynamic shiftAktif; for (var jadwal in jadwalHariIni) { try { final mulai = jadwal['jam_mulai']; final selesai = jadwal['jam_selesai']; final mulaiParts = mulai.split(':'); final selesaiParts = selesai.split(':'); final mulaiDate = DateTime( now.year, now.month, now.day, int.parse(mulaiParts[0]), int.parse(mulaiParts[1]), ); final selesaiDate = DateTime( now.year, now.month, now.day, int.parse(selesaiParts[0]), int.parse(selesaiParts[1]), ); // kalau sekarang di antara shift if (!now.isBefore(mulaiDate) && now.isBefore(selesaiDate)) { shiftAktif = jadwal; break; } } catch (e) { debugPrint("ERROR PARSE SHIFT: $e"); } } // kalau tidak ada shift aktif, // ambil shift terakhir hari ini shiftAktif ??= jadwalHariIni.last; debugPrint("SHIFT AKTIF: $shiftAktif"); setState(() { shift = shiftAktif['nama_shift'] ?? '-'; jamShift = "${shiftAktif['jam_mulai']} - ${shiftAktif['jam_selesai']}"; lokasiShift = shiftAktif['nama_lokasi'] ?? '-'; }); } } } catch (e) { debugPrint("Error fetch jadwal: $e"); } } Future fetchLaporanTerakhir() async { String? token = await AuthService.getToken(); try { final response = await http.get( Uri.parse("${AuthService.baseUrl}/laporan"), headers: { "Accept": "application/json", "Authorization": "Bearer $token", }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); debugPrint("LAPORAN: ${response.body}"); if (data['data'] != null && data['data'].length > 0) { final laporan = data['data'][0]; setState(() { laporanJudul = laporan['jadwal']?['lokasi']?['nama_lokasi'] ?? laporan['lokasi']?['nama_lokasi'] ?? laporan['nama_lokasi'] ?? '-'; laporanDeskripsi = laporan['keterangan'] ?? '-'; laporanWaktu = laporan['waktu_patroli'] ?? laporan['created_at'] ?? '-'; }); } } } catch (e) { debugPrint("Error fetch laporan: $e"); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FD), body: SingleChildScrollView( child: Column( children: [ // ================= HEADER ================= Stack( clipBehavior: Clip.none, children: [ Container( height: 240, width: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF1A39B1), Color(0xFF4263EB)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(40), bottomRight: Radius.circular(40), ), ), padding: const EdgeInsets.fromLTRB(25, 60, 25, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text( 'Patroli Kampus', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.bold, letterSpacing: 0.8, ), ), Text( 'Politeknik Negeri Jember', style: TextStyle( color: Colors.white70, fontSize: 12, ), ), ], ), ), Row( children: [ Stack( children: [ IconButton( icon: const Icon( Icons.notifications_rounded, color: Colors.white, size: 28, ), onPressed: () async { await Navigator.push( context, MaterialPageRoute( builder: (_) => const NotificationPage(), ), ); await _loadUnreadCount(); }, ), if (_unreadCount > 0) Positioned( right: 6, top: 6, child: Container( padding: const EdgeInsets.all(5), decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), constraints: const BoxConstraints( minWidth: 18, minHeight: 18, ), child: Text( _unreadCount > 9 ? '9+' : '$_unreadCount', style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), ), ), ], ), const SizedBox(width: 8), Hero( tag: 'logo_polije', child: Image.asset( 'assets/images/logopolije.png', height: 45, ), ), ], ), ], ), const SizedBox(height: 20), // DATE & TIME Container( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 12, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.calendar_today_rounded, size: 14, color: Colors.white, ), const SizedBox(width: 8), Text( getTanggalHari(), style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 15), const VerticalDivider( color: Colors.white54, thickness: 1, ), const Icon( Icons.watch_later_rounded, size: 14, color: Colors.white, ), const SizedBox(width: 8), Text( _timeString, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), // ================= PROFILE CARD ================= Padding( padding: const EdgeInsets.only(top: 185, left: 20, right: 20), child: Container( padding: const EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.06), blurRadius: 20, offset: const Offset(0, 10), ), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: const Color(0xFF4263EB), width: 2, ), ), child: CircleAvatar( radius: 26, backgroundColor: const Color(0xFFF0F3FF), backgroundImage: (fotoUser != null && fotoUser != '') ? NetworkImage(fotoUser!) : null, child: (fotoUser == null || fotoUser == '') ? const Icon( Icons.person_rounded, color: Color(0xFF1A39B1), size: 30, ) : null, ), ), const SizedBox(width: 15), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Selamat Bertugas,', style: TextStyle( fontSize: 12, color: Colors.grey, fontWeight: FontWeight.w500, ), ), Text( namaUser, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: Color(0xFF1E293B), ), ), ], ), ), const Icon( Icons.verified_user_rounded, color: Colors.green, size: 20, ), ], ), ), ), ], ), // ================= CONTENT ================= Padding( padding: const EdgeInsets.fromLTRB(20, 40, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionTitle("Shift Hari Ini"), const SizedBox(height: 15), _buildShiftCard( shift: shift, jam: jamShift, lokasi: lokasiShift, ), const SizedBox(height: 30), _buildSectionTitle("Laporan Terakhir"), const SizedBox(height: 15), _buildLaporanCard( judul: laporanJudul, waktu: laporanWaktu, deskripsi: laporanDeskripsi, ), ], ), ), ], ), ), ); } Widget _buildSectionTitle(String title) { return Row( children: [ Container( width: 4, height: 18, decoration: BoxDecoration( color: const Color(0xFF3B82F6), borderRadius: BorderRadius.circular(10), ), ), const SizedBox(width: 10), Text( title, style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 18, color: Color(0xFF1E293B), letterSpacing: 0.2, ), ), ], ); } Widget _buildShiftCard({ required String shift, required String jam, required String lokasi, }) { // Hitung durasi String durasi = '-'; try { final parts = jam.split(' - '); if (parts.length == 2) { final mulai = parts[0].trim().split(':'); final selesai = parts[1].trim().split(':'); final jamMulai = int.parse(mulai[0]); final jamSelesai = int.parse(selesai[0]); int diff = jamSelesai - jamMulai; if (diff < 0) diff += 24; durasi = '$diff jam'; } } catch (_) {} return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Left accent bar Container( width: 4, decoration: BoxDecoration( color: const Color(0xFF2F5BEA), borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular(16), ), ), ), // Card body Expanded( child: Container( padding: const EdgeInsets.fromLTRB(16, 16, 16, 14), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: Colors.grey.shade200, width: 0.5), right: BorderSide(color: Colors.grey.shade200, width: 0.5), bottom: BorderSide(color: Colors.grey.shade200, width: 0.5), ), borderRadius: const BorderRadius.only( topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nama shift + badge Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( shift == '-' ? 'Tidak ada shift' : shift, style: TextStyle( fontSize: 13, color: Colors.grey.shade600, ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFE8F0FE), borderRadius: BorderRadius.circular(20), ), child: const Text( 'Aktif', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF2F5BEA), ), ), ), ], ), const SizedBox(height: 6), // Jam besar Text( jam == '-' ? '--:-- – --:--' : jam, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), letterSpacing: -0.5, ), ), const SizedBox(height: 2), Text( 'Durasi $durasi', style: TextStyle(fontSize: 11, color: Colors.grey.shade400), ), const SizedBox(height: 14), // Divider Divider(height: 1, color: Colors.grey.shade100), const SizedBox(height: 12), // 2 col bawah Row( children: [ // Durasi Expanded( child: Row( children: [ const Icon(Icons.access_time_rounded, size: 13, color: Color(0xFF2F5BEA)), const SizedBox(width: 7), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Durasi', style: TextStyle(fontSize: 11, color: Colors.grey.shade400)), Text(durasi, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF1E293B))), ], ), ], ), ), // Divider vertikal Container(width: 0.5, height: 30, color: Colors.grey.shade200), const SizedBox(width: 14), // Lokasi Expanded( child: Row( children: [ const Icon(Icons.location_on_rounded, size: 13, color: Color(0xFF2F5BEA)), const SizedBox(width: 7), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Lokasi', style: TextStyle(fontSize: 11, color: Colors.grey.shade400)), Text( lokasi == '-' ? '-' : lokasi, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500, color: Color(0xFF1E293B)), overflow: TextOverflow.ellipsis, ), ], ), ), ], ), ), ], ), ], ), ), ), ], ), ); } Widget _buildLaporanCard({ required String judul, required String waktu, required String deskripsi, }) { // Format waktu String waktuFormatted = waktu; try { final dt = DateTime.parse(waktu); waktuFormatted = DateFormat('dd MMM yyyy · HH:mm', 'id_ID').format(dt); } catch (_) {} return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( width: 4, decoration: const BoxDecoration( color: Color(0xFF2F5BEA), borderRadius: BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular(16), ), ), ), Expanded( child: Container( decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: Colors.grey.shade200, width: 0.5), right: BorderSide(color: Colors.grey.shade200, width: 0.5), bottom: BorderSide(color: Colors.grey.shade200, width: 0.5), ), borderRadius: const BorderRadius.only( topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Icon( Icons.location_on_rounded, size: 13, color: Color(0xFF2F5BEA), ), const SizedBox(width: 5), Text( judul == '-' ? 'Belum ada laporan' : judul, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), ), ), ], ), Text( waktuFormatted, style: TextStyle( fontSize: 11, color: Colors.grey.shade400, ), ), ], ), ), Divider(height: 1, color: Colors.grey.shade100), // Preview teks besar Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), child: Text( deskripsi == '-' ? 'Tidak ada keterangan.' : deskripsi, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w500, color: Color(0xFF1E293B), height: 1.4, ), ), ), Divider(height: 1, color: Colors.grey.shade100), // Footer Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 12), child: Row( children: [ const Icon( Icons.access_time_rounded, size: 11, color: Color(0xFF2F5BEA), ), const SizedBox(width: 5), Text( 'Dilaporkan $waktuFormatted', style: TextStyle( fontSize: 11, color: Colors.grey.shade400, ), ), ], ), ), ], ), ), ), ], ), ); } }