MIF_E31231033/lib/screens/dashboard/user/user_home.dart

842 lines
28 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<UserHome> createState() => _UserHomeState();
}
class _UserHomeState extends State<UserHome> {
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<void> loadData() async {
await getUser();
await fetchJadwal();
await fetchLaporanTerakhir();
await NotificationService.checkJadwalNotification();
await NotificationService.checkLaporanDitolakNotification();
await _loadUnreadCount();
}
Future<void> _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<void> 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<void> 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<void> 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,
),
),
],
),
),
],
),
),
),
],
),
);
}
}