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

562 lines
19 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import '../../../services/auth_service.dart';
// Model untuk menyimpan media (foto/video)
class MediaItem {
final XFile file;
final bool isVideo;
MediaItem({required this.file, required this.isVideo});
}
class EditLaporanScreen extends StatefulWidget {
final Map laporan;
const EditLaporanScreen({super.key, required this.laporan});
@override
State<EditLaporanScreen> createState() => _EditLaporanScreenState();
}
class _EditLaporanScreenState extends State<EditLaporanScreen> {
final _formKey = GlobalKey<FormState>();
late TextEditingController keteranganController;
List<MediaItem> selectedMedia =
[]; // ✅ Ganti dari List<XFile> ke List<MediaItem>
bool isLoading = false;
@override
void initState() {
super.initState();
keteranganController = TextEditingController(
text: widget.laporan['keterangan'] ?? '',
);
}
@override
void dispose() {
keteranganController.dispose();
super.dispose();
}
// ====== AMBIL FOTO DARI KAMERA ======
Future<void> capturePhoto() async {
final picker = ImagePicker();
final photo = await picker.pickImage(
source: ImageSource.camera,
imageQuality: 85,
);
if (photo != null) {
setState(() {
selectedMedia.add(MediaItem(file: photo, isVideo: false));
});
}
}
// ====== REKAM VIDEO DARI KAMERA ======
Future<void> captureVideo() async {
final picker = ImagePicker();
final video = await picker.pickVideo(
source: ImageSource.camera,
maxDuration: const Duration(minutes: 5), // Batas durasi 5 menit
);
if (video != null) {
setState(() {
selectedMedia.add(MediaItem(file: video, isVideo: true));
});
}
}
// ====== HAPUS MEDIA ======
void removeMedia(int index) {
setState(() {
selectedMedia.removeAt(index);
});
}
// ====== TAMPILKAN BOTTOM SHEET PILIHAN MEDIA ======
void showMediaPicker() {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
const Text(
"Tambah Media",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16),
_MediaOption(
icon: Icons.camera_alt,
label: "Ambil Foto",
subtitle: "Buka kamera untuk foto",
color: const Color(0xFF2F5BEA),
onTap: () {
Navigator.pop(context);
capturePhoto();
},
),
_MediaOption(
icon: Icons.videocam,
label: "Rekam Video",
subtitle: "Buka kamera untuk video (maks. 5 menit)",
color: Colors.deepOrange,
onTap: () {
Navigator.pop(context);
captureVideo();
},
),
const SizedBox(height: 8),
],
),
),
);
},
);
}
// ====== SUBMIT ULANG ======
Future<void> submitUlang() async {
if (!_formKey.currentState!.validate()) return;
setState(() => isLoading = true);
try {
final token = await AuthService.getToken();
final laporanId = widget.laporan['id'];
final request = http.MultipartRequest(
'POST',
Uri.parse('${AuthService.baseUrl}/laporan/$laporanId/resubmit'),
);
request.headers['Authorization'] = 'Bearer $token';
request.headers['Accept'] = 'application/json';
request.fields['keterangan'] = keteranganController.text;
// ✅ Tambahkan foto dan video sesuai field-nya
for (var media in selectedMedia) {
if (media.isVideo) {
request.files.add(
await http.MultipartFile.fromPath('video[]', media.file.path),
);
} else {
request.files.add(
await http.MultipartFile.fromPath('foto[]', media.file.path),
);
}
}
final response = await request.send();
if (!mounted) return;
setState(() => isLoading = false);
if (response.statusCode == 200) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("✅ Laporan berhasil dikirim ulang"),
backgroundColor: Colors.green,
),
);
Navigator.pop(context);
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("❌ Gagal mengirim ulang laporan"),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
setState(() => isLoading = false);
debugPrint("Resubmit error: $e");
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("❌ Terjadi kesalahan")));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF8FAFF),
appBar: AppBar(
title: const Text(
"Edit & Kirim Ulang",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
backgroundColor: const Color(0xFF2F5BEA),
iconTheme: const IconThemeData(color: Colors.white),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ====== INFO PENOLAKAN ======
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.info_outline, color: Colors.red, size: 18),
SizedBox(width: 6),
Text(
"Alasan Penolakan:",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
],
),
const SizedBox(height: 8),
Text(
widget.laporan['catatan_penolakan'] ?? '-',
style: const TextStyle(fontSize: 14),
),
],
),
),
const SizedBox(height: 24),
// ====== KETERANGAN ======
const Text(
"Keterangan Laporan",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
const SizedBox(height: 8),
TextFormField(
controller: keteranganController,
maxLines: 4,
decoration: InputDecoration(
hintText: "Tulis keterangan laporan...",
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
),
validator:
(v) =>
(v == null || v.trim().isEmpty) ? "Wajib diisi" : null,
),
const SizedBox(height: 24),
// ====== MEDIA (FOTO & VIDEO) ======
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Ganti Media (opsional)",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
),
if (selectedMedia.isNotEmpty)
Text(
"${selectedMedia.length} file",
style: const TextStyle(
color: Color(0xFF2F5BEA),
fontSize: 13,
),
),
],
),
const SizedBox(height: 8),
// Tombol pilih media
GestureDetector(
onTap: showMediaPicker,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF2F5BEA)),
),
child: Column(
children: [
const Icon(
Icons.perm_media_outlined,
size: 40,
color: Color(0xFF2F5BEA),
),
const SizedBox(height: 8),
Text(
selectedMedia.isEmpty
? "Ketuk untuk tambah foto / video"
: "Ketuk untuk tambah lebih banyak",
style: const TextStyle(color: Color(0xFF2F5BEA)),
),
const SizedBox(height: 4),
// Label pilihan
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_PillLabel(
icon: Icons.camera_alt,
label: "Foto Kamera",
color: const Color(0xFF2F5BEA),
),
const SizedBox(width: 8),
_PillLabel(
icon: Icons.videocam,
label: "Video",
color: Colors.deepOrange,
),
const SizedBox(width: 8),
],
),
],
),
),
),
// ====== PREVIEW MEDIA ======
if (selectedMedia.isNotEmpty) ...[
const SizedBox(height: 12),
SizedBox(
height: 110,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: selectedMedia.length,
itemBuilder: (context, index) {
final media = selectedMedia[index];
return Stack(
children: [
Container(
margin: const EdgeInsets.only(right: 8),
width: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade200,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child:
media.isVideo
// Tampilkan placeholder video
? Container(
color: Colors.black87,
child: const Center(
child: Icon(
Icons.play_circle_fill,
color: Colors.white,
size: 40,
),
),
)
// Tampilkan preview foto
: Image.file(
File(media.file.path),
fit: BoxFit.cover,
width: 100,
height: 110,
),
),
),
// Badge tipe media
Positioned(
bottom: 6,
left: 6,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color:
media.isVideo
? Colors.deepOrange
: const Color(0xFF2F5BEA),
borderRadius: BorderRadius.circular(4),
),
child: Text(
media.isVideo ? "VIDEO" : "FOTO",
style: const TextStyle(
color: Colors.white,
fontSize: 9,
fontWeight: FontWeight.bold,
),
),
),
),
// Tombol hapus
Positioned(
top: 4,
right: 12,
child: GestureDetector(
onTap: () => removeMedia(index),
child: Container(
width: 22,
height: 22,
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 14,
),
),
),
),
],
);
},
),
),
],
const SizedBox(height: 32),
// ====== TOMBOL SUBMIT ======
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: isLoading ? null : submitUlang,
icon:
isLoading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.send),
label: Text(isLoading ? "Mengirim..." : "Kirim Ulang"),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2F5BEA),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
),
);
}
}
// ====== WIDGET HELPER: Opsi di Bottom Sheet ======
class _MediaOption extends StatelessWidget {
final IconData icon;
final String label;
final String subtitle;
final Color color;
final VoidCallback onTap;
const _MediaOption({
required this.icon,
required this.label,
required this.subtitle,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color),
),
title: Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
subtitle: Text(subtitle, style: const TextStyle(fontSize: 12)),
onTap: onTap,
);
}
}
// ====== WIDGET HELPER: Pill Label ======
class _PillLabel extends StatelessWidget {
final IconData icon;
final String label;
final Color color;
const _PillLabel({
required this.icon,
required this.label,
required this.color,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: color),
const SizedBox(width: 4),
Text(
label,
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}