import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:geolocator/geolocator.dart'; import '../../../services/auth_service.dart'; class LaporanUserScreen extends StatefulWidget { final String lokasi; final String tanggal; final String shift; final String jadwalId; final String namaPetugas; const LaporanUserScreen({ super.key, required this.lokasi, required this.tanggal, required this.shift, required this.jadwalId, required this.namaPetugas, }); @override State createState() => _LaporanUserScreenState(); } class _LaporanUserScreenState extends State { final _formKey = GlobalKey(); final ImagePicker _picker = ImagePicker(); final Color primaryBlue = const Color(0xFF2F5BEA); final Color scaffoldBg = const Color(0xFFF4F6FB); // Batas sesuai backend Laravel static const int maxFoto = 5; static const int maxVideo = 2; static const double maxFotoMB = 4.0; static const double maxVideoMB = 20.0; List images = []; List videos = []; double? latitude; double? longitude; bool isLoading = false; // ── STATE PROGRESS UPLOAD ── double _uploadProgress = 0.0; String _uploadStatus = ''; final TextEditingController keteranganController = TextEditingController(); @override void initState() { super.initState(); ambilLokasi(); } @override void dispose() { keteranganController.dispose(); super.dispose(); } Future ambilLokasi() async { if (!await Geolocator.isLocationServiceEnabled()) return; LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) return; Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); if (mounted) { setState(() { latitude = position.latitude; longitude = position.longitude; }); } } Future ambilFotoKamera() async { if (images.length >= maxFoto) { _showSnackBar("Maksimal $maxFoto foto", Colors.orange); return; } final pickedFile = await _picker.pickImage( source: ImageSource.camera, imageQuality: 70, ); if (pickedFile != null) { final file = File(pickedFile.path); final sizeMB = file.lengthSync() / (1024 * 1024); if (sizeMB > maxFotoMB) { _showSnackBar( "Foto terlalu besar (${sizeMB.toStringAsFixed(1)}MB), maks ${maxFotoMB.toInt()}MB", Colors.red, ); return; } setState(() => images.add(file)); } } Future ambilVideoKamera() async { if (videos.length >= maxVideo) { _showSnackBar("Maksimal $maxVideo video", Colors.orange); return; } final pickedVideo = await _picker.pickVideo( source: ImageSource.camera, maxDuration: const Duration(seconds: 30), // batasi 30 detik ); if (pickedVideo != null) { final file = File(pickedVideo.path); final sizeMB = file.lengthSync() / (1024 * 1024); if (sizeMB > maxVideoMB) { _showSnackBar( "Video terlalu besar (${sizeMB.toStringAsFixed(1)}MB), maks ${maxVideoMB.toInt()}MB", Colors.red, ); return; } setState(() => videos.add(file)); } } void hapusFoto(int index) => setState(() => images.removeAt(index)); void hapusVideo(int index) => setState(() => videos.removeAt(index)); Future kirimLaporan() async { if (!_formKey.currentState!.validate()) return; if (images.isEmpty && videos.isEmpty) { _showSnackBar("Minimal 1 foto atau video", Colors.red); return; } if (latitude == null || longitude == null) { _showSnackBar("Lokasi belum terdeteksi", Colors.orange); return; } // Validasi ukuran file sekali lagi sebelum kirim for (var img in images) { final sizeMB = img.lengthSync() / (1024 * 1024); if (sizeMB > maxFotoMB) { _showSnackBar( "Ada foto yang terlalu besar (${sizeMB.toStringAsFixed(1)}MB)", Colors.red, ); return; } } for (var vid in videos) { final sizeMB = vid.lengthSync() / (1024 * 1024); if (sizeMB > maxVideoMB) { _showSnackBar( "Ada video yang terlalu besar (${sizeMB.toStringAsFixed(1)}MB)", Colors.red, ); return; } } // ── Reset progress & mulai loading ── setState(() { isLoading = true; _uploadProgress = 0.0; _uploadStatus = 'Menyiapkan file...'; }); try { final result = await AuthService.kirimLaporanMultiMedia( jadwalId: widget.jadwalId, lokasiId: "1", keterangan: keteranganController.text, latitude: latitude!, longitude: longitude!, images: images, videos: videos, // ── Callback progress dari AuthService ── onProgress: (progress, status) { if (mounted) { setState(() { _uploadProgress = progress; _uploadStatus = status; }); } }, ); if (mounted) { setState(() { isLoading = false; _uploadProgress = 0.0; _uploadStatus = ''; }); _showSnackBar( result['message'] ?? 'Gagal', result['success'] ? Colors.green : Colors.red, ); if (result['success']) Navigator.pop(context); } } catch (e) { if (mounted) { setState(() { isLoading = false; _uploadProgress = 0.0; _uploadStatus = ''; }); _showSnackBar("Terjadi kesalahan: ${e.toString()}", Colors.red); } } } void _showSnackBar(String msg, Color color) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: color, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ); } void pilihSumberFoto() => ambilFotoKamera(); void pilihSumberVideo() => ambilVideoKamera(); @override Widget build(BuildContext context) { final waktu = DateFormat('HH:mm').format(DateTime.now()); return Scaffold( backgroundColor: scaffoldBg, appBar: AppBar( title: const Text( "Input Laporan", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 18, ), ), centerTitle: true, backgroundColor: primaryBlue, elevation: 0, iconTheme: const IconThemeData(color: Colors.white), shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)), ), ), body: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoCard(waktu), const SizedBox(height: 16), _buildDokumentasiCard(), const SizedBox(height: 16), _buildKeteranganCard(), const SizedBox(height: 24), _buildSubmitButton(), const SizedBox(height: 30), ], ), ), ), ); } // ── INFO CARD ── Widget _buildInfoCard(String waktu) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: primaryBlue.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: primaryBlue, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Row( children: const [ Icon(Icons.assignment_outlined, color: Colors.white, size: 20), SizedBox(width: 8), Text( "Informasi Penugasan", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _infoRow(Icons.person_rounded, "Petugas", widget.namaPetugas, const Color(0xFF5B8DEF)), _divider(), _infoRow(Icons.location_on_rounded, "Lokasi", widget.lokasi, Colors.redAccent), _divider(), _infoRow(Icons.calendar_month_rounded, "Tanggal", widget.tanggal, Colors.orange), _divider(), _infoRow(Icons.wb_sunny_rounded, "Shift", widget.shift, Colors.amber.shade700), _divider(), _infoRow(Icons.access_time_rounded, "Waktu", waktu, Colors.teal), _divider(), _buildGpsRow(), ], ), ), ], ), ); } Widget _infoRow( IconData icon, String label, String value, Color iconColor) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, size: 18, color: iconColor), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 11, color: Colors.grey.shade500, fontWeight: FontWeight.w500, ), ), Text( value, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF2D3243), ), ), ], ), ], ), ); } Widget _buildGpsRow() { final bool terkunci = latitude != null; return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: (terkunci ? Colors.green : Colors.red).withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: Icon( terkunci ? Icons.my_location_rounded : Icons.location_off_rounded, size: 18, color: terkunci ? Colors.green : Colors.red, ), ), const SizedBox(width: 12), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "GPS Status", style: TextStyle( fontSize: 11, color: Colors.grey.shade500, fontWeight: FontWeight.w500, ), ), Row( children: [ Container( width: 7, height: 7, decoration: BoxDecoration( color: terkunci ? Colors.green : Colors.red, shape: BoxShape.circle, ), ), const SizedBox(width: 5), Text( terkunci ? "Lokasi Terkunci" : "Mencari Lokasi...", style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: terkunci ? Colors.green : Colors.red, ), ), ], ), ], ), ], ), ); } Widget _divider() => Divider(height: 1, color: Colors.grey.shade100); // ── DOKUMENTASI CARD ── Widget _buildDokumentasiCard() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: primaryBlue.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: primaryBlue, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Row( children: [ const Icon(Icons.photo_library_outlined, color: Colors.white, size: 20), const SizedBox(width: 8), const Text( "Bukti Dokumentasi", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), const Spacer(), _counterBadge( "${images.length}/$maxFoto foto", Colors.blue.shade200), const SizedBox(width: 6), _counterBadge( "${videos.length}/$maxVideo video", Colors.purple.shade200), ], ), ), Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Row( children: [ Expanded( child: _mediaBtn( "Ambil Foto", Icons.add_a_photo_rounded, images.length >= maxFoto ? null : pilihSumberFoto, Colors.blue, images.length >= maxFoto, ), ), const SizedBox(width: 12), Expanded( child: _mediaBtn( "Rekam Video", Icons.videocam_rounded, videos.length >= maxVideo ? null : pilihSumberVideo, Colors.deepPurple, videos.length >= maxVideo, ), ), ], ), const SizedBox(height: 8), Row( children: [ Icon(Icons.info_outline, size: 12, color: Colors.grey.shade400), const SizedBox(width: 4), Text( "Foto maks ${maxFotoMB.toInt()}MB • Video maks ${maxVideoMB.toInt()}MB (maks 30 detik)", style: TextStyle( fontSize: 11, color: Colors.grey.shade400, ), ), ], ), if (images.isNotEmpty || videos.isNotEmpty) ...[ const SizedBox(height: 16), _buildMediaGrid(), ], ], ), ), ], ), ); } Widget _counterBadge(String label, Color color) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withOpacity(0.3), borderRadius: BorderRadius.circular(20), ), child: Text( label, style: const TextStyle(color: Colors.white, fontSize: 10), ), ); } Widget _mediaBtn(String label, IconData icon, VoidCallback? action, Color color, bool disabled) { return GestureDetector( onTap: action, child: Container( padding: const EdgeInsets.symmetric(vertical: 14), decoration: BoxDecoration( color: disabled ? Colors.grey.shade100 : color.withOpacity(0.08), borderRadius: BorderRadius.circular(14), border: Border.all( color: disabled ? Colors.grey.shade300 : color.withOpacity(0.25), ), ), child: Column( children: [ Icon(icon, color: disabled ? Colors.grey.shade400 : color, size: 26), const SizedBox(height: 6), Text( label, style: TextStyle( color: disabled ? Colors.grey.shade400 : color, fontSize: 12, fontWeight: FontWeight.w600, ), ), ], ), ), ); } Widget _buildMediaGrid() { return Wrap( spacing: 10, runSpacing: 10, children: [ ...images.asMap().entries.map((e) => _mediaItem( Image.file(e.value, width: 90, height: 90, fit: BoxFit.cover), () => hapusFoto(e.key), Colors.blue, )), ...videos.asMap().entries.map((e) => _mediaItem( Container( width: 90, height: 90, color: Colors.deepPurple.shade50, child: Icon(Icons.play_circle_fill, color: Colors.deepPurple.shade300, size: 36), ), () => hapusVideo(e.key), Colors.deepPurple, )), ], ); } Widget _mediaItem(Widget child, VoidCallback onDelete, Color color) { return Stack( children: [ Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.3)), ), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: child, ), ), Positioned( top: 4, right: 4, child: GestureDetector( onTap: onDelete, child: Container( width: 22, height: 22, decoration: const BoxDecoration( color: Colors.red, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 13, color: Colors.white), ), ), ), ], ); } // ── KETERANGAN CARD ── Widget _buildKeteranganCard() { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: primaryBlue.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( color: primaryBlue, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), ), child: Row( children: const [ Icon(Icons.notes_rounded, color: Colors.white, size: 20), SizedBox(width: 8), Text( "Keterangan", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ], ), ), Padding( padding: const EdgeInsets.all(16), child: TextFormField( controller: keteranganController, maxLines: 4, decoration: InputDecoration( hintText: "Tulis detail laporan patroli di sini...", hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 13), filled: true, fillColor: Colors.grey.shade50, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade200), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey.shade200), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: primaryBlue, width: 1.5), ), contentPadding: const EdgeInsets.all(14), ), validator: (v) => v!.isEmpty ? "Keterangan wajib diisi" : null, ), ), ], ), ); } // ── SUBMIT BUTTON + PROGRESS BAR ── Widget _buildSubmitButton() { return Column( children: [ // ── Progress bar muncul saat isLoading ── if (isLoading) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( // null = indeterminate (animasi penuh), > 0 = deterministic value: _uploadProgress > 0 ? _uploadProgress : null, backgroundColor: Colors.grey.shade200, color: primaryBlue, minHeight: 7, ), ), const SizedBox(height: 6), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( _uploadStatus.isNotEmpty ? _uploadStatus : 'Mengupload...', style: TextStyle( fontSize: 12, color: Colors.grey.shade600, fontWeight: FontWeight.w500, ), ), if (_uploadProgress > 0) Text( '${(_uploadProgress * 100).toInt()}%', style: TextStyle( fontSize: 12, color: primaryBlue, fontWeight: FontWeight.bold, ), ), ], ), ], ), ), const SizedBox(height: 8), ], // ── Tombol kirim ── SizedBox( width: double.infinity, height: 54, child: ElevatedButton( onPressed: isLoading ? null : kirimLaporan, style: ElevatedButton.styleFrom( backgroundColor: primaryBlue, disabledBackgroundColor: primaryBlue.withOpacity(0.6), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16)), elevation: 3, shadowColor: primaryBlue.withOpacity(0.4), ), child: isLoading ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( width: 18, height: 18, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2), ), const SizedBox(width: 10), Text( _uploadStatus.isNotEmpty ? _uploadStatus : 'Mengupload...', style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), ], ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.send_rounded, color: Colors.white, size: 18), SizedBox(width: 8), Text( "KIRIM LAPORAN", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 15, letterSpacing: 0.5, ), ), ], ), ), ), ], ); } }