import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:open_file/open_file.dart'; import 'package:path_provider/path_provider.dart'; import '../../../services/auth_service.dart'; import 'detail_laporan_admin_screen.dart'; import 'package:permission_handler/permission_handler.dart'; Future requestPermission() async { await Permission.storage.request(); } class LaporanAdminScreen extends StatefulWidget { const LaporanAdminScreen({super.key}); @override State createState() => _LaporanAdminScreenState(); } class _LaporanAdminScreenState extends State { List laporanList = []; List filteredLaporan = []; bool isLoading = true; bool isDownloading = false; TextEditingController searchController = TextEditingController(); String selectedMonth = "Semua"; String selectedYear = "Semua"; // ✅ Tambahan: state untuk rentang tanggal DateTime? dateFrom; DateTime? dateTo; final Map months = { "Semua": null, "Januari": 1, "Februari": 2, "Maret": 3, "April": 4, "Mei": 5, "Juni": 6, "Juli": 7, "Agustus": 8, "September": 9, "Oktober": 10, "November": 11, "Desember": 12, }; List years = ["Semua"]; @override void initState() { super.initState(); getLaporan(); } String _formatDateTime(String? createdAt) { if (createdAt == null || createdAt.isEmpty) return ""; final date = DateTime.tryParse(createdAt); if (date == null) return ""; final local = date.toLocal(); const bulanNama = [ '', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', ]; final tgl = local.day.toString().padLeft(2, '0'); final bln = bulanNama[local.month]; final thn = local.year; final jam = local.hour.toString().padLeft(2, '0'); final mnt = local.minute.toString().padLeft(2, '0'); return "$tgl $bln $thn • $jam:$mnt"; } // ✅ Format DateTime ke string "dd MMM yyyy" untuk tampilan String _formatDate(DateTime? date) { if (date == null) return "Pilih tanggal"; const bulanNama = [ '', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', ]; return "${date.day.toString().padLeft(2, '0')} ${bulanNama[date.month]} ${date.year}"; } // ✅ Format DateTime ke "yyyy-MM-dd" untuk query param API String _formatDateParam(DateTime date) { return "${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}"; } void generateYears() { Set yearSet = {"Semua"}; for (var laporan in laporanList) { String createdAt = laporan['created_at'] ?? ""; DateTime? date = DateTime.tryParse(createdAt); if (date != null) { yearSet.add(date.year.toString()); } } List result = yearSet.toList(); result.remove("Semua"); result.sort(); result.insert(0, "Semua"); setState(() { years = result; }); } Future getLaporan() async { try { setState(() => isLoading = true); final token = await AuthService.getToken(); final response = await http.get( Uri.parse('${AuthService.baseUrl}/admin/laporan'), headers: { "Accept": "application/json", "Authorization": "Bearer $token", }, ); final data = jsonDecode(response.body); if (response.statusCode == 200) { setState(() { laporanList = data['data'] ?? []; filteredLaporan = laporanList; isLoading = false; }); generateYears(); } } catch (e) { debugPrint("Error laporan: $e"); setState(() => isLoading = false); } } // ✅ Tampilkan bottom sheet pilihan mode download void showDownloadOptions() { // Local state untuk bottom sheet int tabIndex = 0; // 0 = Bulan/Tahun, 1 = Rentang Tanggal String sheetMonth = selectedMonth; String sheetYear = selectedYear; DateTime? sheetFrom = dateFrom; DateTime? sheetTo = dateTo; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) { return StatefulBuilder( builder: (ctx, setSheetState) { return Container( padding: EdgeInsets.only( bottom: MediaQuery.of(ctx).viewInsets.bottom, ), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle bar Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 16), const Text( "Download Rekap PDF", style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), const Text( "Pilih rentang waktu laporan", style: TextStyle(color: Colors.grey, fontSize: 13), ), const SizedBox(height: 16), // Tab Selector Container( decoration: BoxDecoration( color: const Color(0xFFF1F4F8), borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.all(4), child: Row( children: [ _buildTab( label: "Bulan / Tahun", icon: Icons.calendar_month_outlined, selected: tabIndex == 0, onTap: () => setSheetState(() => tabIndex = 0), ), _buildTab( label: "Rentang Tanggal", icon: Icons.date_range_outlined, selected: tabIndex == 1, onTap: () => setSheetState(() => tabIndex = 1), ), ], ), ), const SizedBox(height: 20), // === Tab 0: Bulan / Tahun === if (tabIndex == 0) ...[ Row( children: [ Expanded( child: _buildDropdown( "Bulan", sheetMonth, months.keys.toList(), (v) => setSheetState(() => sheetMonth = v!), ), ), const SizedBox(width: 12), Expanded( child: _buildDropdown( "Tahun", sheetYear, years, (v) => setSheetState(() => sheetYear = v!), ), ), ], ), ], // === Tab 1: Rentang Tanggal === if (tabIndex == 1) ...[ Row( children: [ Expanded( child: _buildDatePickerButton( label: "Dari", date: sheetFrom, onTap: () async { final picked = await showDatePicker( context: ctx, initialDate: sheetFrom ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) => Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: Color(0xFF2F5BEA), ), ), child: child!, ), ); if (picked != null) { setSheetState(() => sheetFrom = picked); } }, ), ), const SizedBox(width: 12), Expanded( child: _buildDatePickerButton( label: "Sampai", date: sheetTo, onTap: () async { final picked = await showDatePicker( context: ctx, initialDate: sheetTo ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime.now(), builder: (context, child) => Theme( data: Theme.of(context).copyWith( colorScheme: const ColorScheme.light( primary: Color(0xFF2F5BEA), ), ), child: child!, ), ); if (picked != null) { setSheetState(() => sheetTo = picked); } }, ), ), ], ), // Validasi tanggal if (sheetFrom != null && sheetTo != null && sheetFrom!.isAfter(sheetTo!)) ...[ const SizedBox(height: 8), const Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange, size: 14), SizedBox(width: 4), Text( "Tanggal mulai tidak boleh melebihi tanggal akhir", style: TextStyle( color: Colors.orange, fontSize: 11), ), ], ), ], ], const SizedBox(height: 24), // Tombol Download SizedBox( width: double.infinity, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2F5BEA), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), ), icon: const Icon(Icons.download_rounded), label: const Text( "Download PDF", style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold), ), onPressed: () { // Validasi rentang tanggal if (tabIndex == 1) { if (sheetFrom == null || sheetTo == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( "⚠️ Pilih tanggal mulai dan akhir terlebih dahulu"), ), ); return; } if (sheetFrom!.isAfter(sheetTo!)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( "⚠️ Tanggal mulai tidak boleh melebihi tanggal akhir"), ), ); return; } setState(() { dateFrom = sheetFrom; dateTo = sheetTo; }); Navigator.pop(ctx); downloadPDF( mode: 'range', fromDate: sheetFrom, toDate: sheetTo, ); } else { setState(() { selectedMonth = sheetMonth; selectedYear = sheetYear; }); Navigator.pop(ctx); downloadPDF( mode: 'month_year', month: sheetMonth, year: sheetYear, ); } }, ), ), ], ), ), ); }, ); }, ); } // ✅ Widget tab selector Widget _buildTab({ required String label, required IconData icon, required bool selected, required VoidCallback onTap, }) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: selected ? const Color(0xFF2F5BEA) : Colors.transparent, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, size: 15, color: selected ? Colors.white : Colors.grey.shade600), const SizedBox(width: 6), Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: selected ? Colors.white : Colors.grey.shade600, ), ), ], ), ), ), ); } // ✅ Widget tombol date picker Widget _buildDatePickerButton({ required String label, required DateTime? date, required VoidCallback onTap, }) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: const Color(0xFFF1F4F8), borderRadius: BorderRadius.circular(10), border: Border.all( color: date != null ? const Color(0xFF2F5BEA).withValues(alpha: 0.4) : Colors.transparent, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 11, color: Color(0xFF2F5BEA), fontWeight: FontWeight.w600), ), const SizedBox(height: 4), Row( children: [ const Icon(Icons.calendar_today_outlined, size: 13, color: Colors.grey), const SizedBox(width: 6), Expanded( child: Text( _formatDate(date), style: TextStyle( fontSize: 12, color: date != null ? Colors.black87 : Colors.grey, fontWeight: date != null ? FontWeight.w500 : FontWeight.normal, ), overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), ); } // ✅ downloadPDF sekarang terima parameter mode, fromDate, toDate, month, year Future downloadPDF({ String mode = 'month_year', String? month, String? year, DateTime? fromDate, DateTime? toDate, }) async { if (isDownloading) return; setState(() => isDownloading = true); // var status = await Permission.storage.request(); // debugPrint("Storage status: $status"); // if (!status.isGranted) { // ScaffoldMessenger.of(context).showSnackBar( // const SnackBar(content: Text("❌ Izin storage ditolak"))); // setState(() => isDownloading = false); // return; // } double progressValue = 0; late StateSetter setStateDialog; showDialog( context: context, barrierDismissible: false, builder: (ctx) => StatefulBuilder( builder: (ctx, setS) { setStateDialog = setS; return AlertDialog( title: const Text("Mengunduh laporan..."), content: Column( mainAxisSize: MainAxisSize.min, children: [ LinearProgressIndicator( value: progressValue == 0 ? null : progressValue), const SizedBox(height: 10), Text( progressValue == 0 ? "Mempersiapkan..." : "${(progressValue * 100).toStringAsFixed(0)}%", ), ], ), ); }, ), ); try { final token = await AuthService.getToken(); final queryParams = {}; if (mode == 'range' && fromDate != null && toDate != null) { // ✅ Mode rentang tanggal: kirim param "dari" & "sampai" queryParams['dari'] = _formatDateParam(fromDate); queryParams['sampai'] = _formatDateParam(toDate); } else { // Mode bulan / tahun final bulan = months[month ?? selectedMonth]; final tahun = (year ?? selectedYear) == "Semua" ? null : (year ?? selectedYear); if (bulan != null) queryParams['bulan'] = bulan.toString(); if (tahun != null) queryParams['tahun'] = tahun; } final url = Uri.parse("${AuthService.baseUrl}/admin/export-pdf") .replace(queryParameters: queryParams) .toString(); debugPrint("URL: $url"); // Nama file dinamis sesuai mode final String namaFile; if (mode == 'range' && fromDate != null && toDate != null) { namaFile = "laporan_${_formatDateParam(fromDate)}_sd_${_formatDateParam(toDate)}_${DateTime.now().millisecondsSinceEpoch}.pdf"; } else { namaFile = "laporan_${month ?? selectedMonth}_${year ?? selectedYear}_${DateTime.now().millisecondsSinceEpoch}.pdf"; } final tempDir = await getTemporaryDirectory(); final tempPath = "${tempDir.path}/$namaFile"; Dio dio = Dio(); Response response = await dio.download( url, tempPath, options: Options( headers: { "Authorization": "Bearer $token", "Accept": "application/pdf", }, responseType: ResponseType.bytes, ), onReceiveProgress: (received, total) { if (total != -1) { setStateDialog(() { progressValue = received / total; }); } }, ); if (response.statusCode != 200) throw Exception("Gagal download"); debugPrint("Android version test"); // final downloadDir = Directory("/storage/emulated/0/Download"); // if (!downloadDir.existsSync()) downloadDir.createSync(recursive: true); // final finalPath = "${downloadDir.path}/$namaFile"; // File(tempPath).copySync(finalPath); // File(tempPath).deleteSync(); final dir = await getApplicationDocumentsDirectory(); final finalPath = "${dir.path}/$namaFile"; File(tempPath).copySync(finalPath); File(tempPath).deleteSync(); await Future.delayed(const Duration(milliseconds: 500)); if (!mounted) return; Navigator.pop(context); await OpenFile.open(finalPath); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("File berhasil dibuka"), backgroundColor: Colors.green, ), ); } catch (e) { debugPrint("ERROR: $e"); if (e is DioException) { debugPrint("RESPONSE: ${e.response?.data}"); debugPrint("STATUS: ${e.response?.statusCode}"); } if (!mounted) return; Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("❌ Download gagal, cek server / token"), backgroundColor: Colors.red, ), ); } finally { if (mounted) setState(() => isDownloading = false); } } void filterLaporan(String query) { final filtered = laporanList.where((laporan) { final lokasi = (laporan['jadwal']?['lokasi']?['nama_lokasi'] ?? "") .toString() .toLowerCase(); final petugas = (laporan['user']?['name'] ?? "").toString().toLowerCase(); final keterangan = (laporan['keterangan'] ?? "").toString().toLowerCase(); return lokasi.contains(query.toLowerCase()) || petugas.contains(query.toLowerCase()) || keterangan.contains(query.toLowerCase()); }).toList(); setState(() => filteredLaporan = filtered); } void filterByDate() { List temp = laporanList.where((laporan) { String createdAt = laporan['created_at'] ?? ""; DateTime? date = DateTime.tryParse(createdAt); if (date == null) return false; bool matchMonth = selectedMonth == "Semua" || date.month == months[selectedMonth]; bool matchYear = selectedYear == "Semua" || date.year.toString() == selectedYear; return matchMonth && matchYear; }).toList(); setState(() => filteredLaporan = temp); } Color getStatusColor(String status) { switch (status.toLowerCase()) { case "disetujui": return const Color(0xFF27AE60); case "ditolak": return const Color(0xFFEB5757); default: return const Color(0xFFF2994A); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FD), appBar: AppBar( title: const Text( "Laporan Patroli", style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: const Color(0xFF2F5BEA), centerTitle: true, elevation: 0, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)), ), actions: [ IconButton( icon: isDownloading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2), ) : const Icon(Icons.download, color: Colors.white), tooltip: "Download Rekap PDF", // ✅ Panggil showDownloadOptions, bukan langsung downloadPDF onPressed: isDownloading ? null : showDownloadOptions, ), ], ), body: isLoading ? const Center( child: CircularProgressIndicator(color: Color(0xFF2F5BEA))) : Column( children: [ /// --- FILTER BOX --- Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Column( children: [ Row( children: [ Expanded( child: _buildDropdown( "Bulan", selectedMonth, months.keys.toList(), (v) { setState(() => selectedMonth = v!); filterByDate(); }, ), ), const SizedBox(width: 12), Expanded( child: _buildDropdown( "Tahun", selectedYear, years, (v) { setState(() => selectedYear = v!); filterByDate(); }, ), ), ], ), const SizedBox(height: 12), TextField( controller: searchController, onChanged: filterLaporan, decoration: InputDecoration( hintText: "Cari lokasi atau petugas...", prefixIcon: const Icon(Icons.search, color: Color(0xFF2F5BEA)), filled: true, fillColor: const Color(0xFFF1F4F8), contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), ], ), ), /// --- LIST LAPORAN --- Expanded( child: RefreshIndicator( onRefresh: getLaporan, child: filteredLaporan.isEmpty ? const Center( child: Text("Laporan tidak ditemukan", style: TextStyle(color: Colors.grey)), ) : ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), itemCount: filteredLaporan.length, itemBuilder: (context, index) { final laporan = filteredLaporan[index]; final String status = laporan['status'] ?? "menunggu"; final String petugas = laporan['user']?['name'] ?? "Anonim"; final String lokasi = laporan['jadwal']?['lokasi'] ?['nama_lokasi'] ?? "Lokasi"; final String waktuInput = _formatDateTime(laporan['created_at']); return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => DetailLaporanAdminScreen( laporan: laporan), ), ).then((_) => getLaporan()); }, child: Padding( padding: const EdgeInsets.all(16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: const Color(0xFF2F5BEA) .withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.assignment_outlined, color: Color(0xFF2F5BEA)), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(lokasi, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 6), Row( children: [ const Icon( Icons.person_outline, size: 14, color: Colors.grey), const SizedBox(width: 4), Text(petugas, style: const TextStyle( color: Colors.black87, fontSize: 13)), ], ), const SizedBox(height: 4), Text( laporan['keterangan'] ?? "-", maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.grey, fontSize: 12), ), if (waktuInput.isNotEmpty) ...[ const SizedBox(height: 6), Row( children: [ const Icon( Icons .calendar_today_outlined, size: 12, color: Colors.grey), const SizedBox(width: 4), Text(waktuInput, style: const TextStyle( color: Colors.grey, fontSize: 11)), ], ), ], ], ), ), const SizedBox(width: 8), _buildStatusBadge(status), ], ), ), ), ); }, ), ), ), ], ), ); } Widget _buildDropdown( String label, String value, List items, Function(String?) onChanged, ) { return DropdownButtonFormField( value: value, decoration: InputDecoration( labelText: label, labelStyle: const TextStyle(fontSize: 12, color: Color(0xFF2F5BEA)), filled: true, fillColor: const Color(0xFFF1F4F8), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), ), items: items .map((m) => DropdownMenuItem( value: m, child: Text(m, style: const TextStyle(fontSize: 13)))) .toList(), onChanged: onChanged, ); } Widget _buildStatusBadge(String status) { final color = getStatusColor(status); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(8), ), child: Text( status.toUpperCase(), style: TextStyle( color: color, fontSize: 9, fontWeight: FontWeight.bold), ), ); } }