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

697 lines
22 KiB
Dart

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<JadwalUserScreen> createState() => _JadwalUserScreenState();
}
class _JadwalUserScreenState extends State<JadwalUserScreen> {
List<Map<String, dynamic>> jadwalList = [];
bool isLoading = true;
String namaUser = '';
int selectedMonth = DateTime.now().month;
int selectedYear = DateTime.now().year;
List<int> years = [DateTime.now().year];
Future<void> getUserLocal() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
namaUser = prefs.getString('nama_user') ?? '';
});
}
void generateYears() {
Set<int> yearSet = {DateTime.now().year};
for (var jadwal in jadwalList) {
DateTime date = DateTime.parse(jadwal['tanggal']);
yearSet.add(date.year);
}
List<int> result = yearSet.toList()..sort();
setState(() {
years = result;
if (!years.contains(selectedYear)) {
selectedYear = DateTime.now().year;
}
});
}
Future<void> 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<Map<String, dynamic>> temp =
List<Map<String, dynamic>>.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<Map<String, dynamic>> 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<String, dynamic> 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<void> 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<DateTime>? _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<int>(
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<int>(
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)),
],
);
}
}