1229 lines
45 KiB
Dart
1229 lines
45 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:geolocator/geolocator.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'dashboard_page.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
import 'package:latlong2/latlong.dart';
|
|
import 'package:geocoding/geocoding.dart';
|
|
import 'package:map_picker/map_picker.dart';
|
|
import 'dart:io';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'dart:async';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import './services/api_services.dart';
|
|
import 'package:dio/dio.dart';
|
|
import './config/theme_colors.dart';
|
|
import './widgets/custom_bottom_bar.dart';
|
|
|
|
class ReportJalan extends StatefulWidget {
|
|
@override
|
|
_ReportJalanState createState() => _ReportJalanState();
|
|
}
|
|
|
|
class _ReportJalanState extends State<ReportJalan> {
|
|
double? _latitude;
|
|
double? _longitude;
|
|
Uint8List? _imageData;
|
|
String? _selectedDisasterType;
|
|
|
|
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
|
final TextEditingController _descriptionController = TextEditingController();
|
|
final TextEditingController _locationController = TextEditingController();
|
|
final mapController = MapController();
|
|
MapPickerController mapPickerController = MapPickerController();
|
|
|
|
List<String> provinsiList = [];
|
|
List<String> kabupatenList = [];
|
|
List<String> kecamatanList = [];
|
|
List<String> kelurahanList = [];
|
|
|
|
String? selectedProvinsi;
|
|
String? selectedKabupaten;
|
|
String? selectedKecamatan;
|
|
String? selectedKelurahan;
|
|
|
|
List<Marker> _markers = [];
|
|
|
|
// Tambahkan variabel untuk menyimpan file foto
|
|
XFile? _photoFile;
|
|
|
|
final ApiService _apiService = ApiService();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_markers.add(
|
|
Marker(
|
|
width: 30.0,
|
|
height: 30.0,
|
|
point: LatLng(-8.009560, 112.950670),
|
|
child: Container(
|
|
child: FlutterLogo(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleMarkerDrag(Marker marker, LatLng newPosition) {
|
|
setState(() {
|
|
_latitude = newPosition.latitude;
|
|
_longitude = newPosition.longitude;
|
|
});
|
|
}
|
|
|
|
void _updateLocationText() {
|
|
setState(() {
|
|
_locationController.text = 'Lat: $_latitude, Lng: $_longitude';
|
|
});
|
|
}
|
|
|
|
void _pickLocation() {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
title: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.location_on,
|
|
color: ThemeColors.accent(context),
|
|
size: 50,
|
|
),
|
|
SizedBox(height: 10),
|
|
Text(
|
|
'Konfirmasi Lokasi',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
'Apakah Anda yakin ingin menggunakan lokasi ini?',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
if (cameraPosition != null) ...[
|
|
SizedBox(height: 10),
|
|
Text(
|
|
'Koordinat: (${cameraPosition!.latitude.toStringAsFixed(6)}, ${cameraPosition!.longitude.toStringAsFixed(6)})',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
actions: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
style: TextButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
),
|
|
child: Text(
|
|
'Batal',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
if (cameraPosition != null) {
|
|
setState(() {
|
|
_latitude = cameraPosition!.latitude;
|
|
_longitude = cameraPosition!.longitude;
|
|
_locationController.text = 'Lat: ${_latitude!.toStringAsFixed(6)}, Lng: ${_longitude!.toStringAsFixed(6)}';
|
|
});
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Row(
|
|
children: [
|
|
Icon(Icons.check_circle, color: Colors.white),
|
|
SizedBox(width: 8),
|
|
Text('Lokasi berhasil dipilih'),
|
|
],
|
|
),
|
|
backgroundColor: Colors.green,
|
|
duration: Duration(seconds: 2),
|
|
behavior: SnackBarBehavior.floating,
|
|
margin: EdgeInsets.all(8),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ThemeColors.accent(context),
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Pilih Lokasi',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
actionsAlignment: MainAxisAlignment.spaceBetween,
|
|
contentPadding: EdgeInsets.fromLTRB(24, 20, 24, 0),
|
|
actionsPadding: EdgeInsets.all(16),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _getCurrentLocation() async {
|
|
bool serviceEnabled;
|
|
LocationPermission permission;
|
|
|
|
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
|
if (!serviceEnabled) {
|
|
return Future.error('Location services are disabled.');
|
|
}
|
|
|
|
permission = await Geolocator.checkPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
permission = await Geolocator.requestPermission();
|
|
if (permission == LocationPermission.denied) {
|
|
return Future.error('Location permissions are denied');
|
|
}
|
|
}
|
|
|
|
if (permission == LocationPermission.deniedForever) {
|
|
return Future.error(
|
|
'Location permissions are permanently denied, we cannot request permissions.');
|
|
}
|
|
|
|
Position position = await Geolocator.getCurrentPosition(
|
|
desiredAccuracy: LocationAccuracy.high);
|
|
|
|
setState(() {
|
|
_latitude = position.latitude;
|
|
_longitude = position.longitude;
|
|
_locationController.text = 'Lat: $_latitude, Lng: $_longitude';
|
|
cameraPosition = LatLng(_latitude!, _longitude!);
|
|
mapController.move(cameraPosition, cameraZoom);
|
|
});
|
|
}
|
|
|
|
LatLng cameraPosition = LatLng(-8.135565, 113.221188);
|
|
double cameraZoom = 14;
|
|
var textController = TextEditingController();
|
|
|
|
Future<void> _pickImage() async {
|
|
try {
|
|
// Bersihkan memori dari foto sebelumnya jika ada
|
|
if (_imageData != null) {
|
|
setState(() {
|
|
_imageData = null;
|
|
_photoFile = null;
|
|
});
|
|
}
|
|
|
|
// Cek permission kamera dengan timeout
|
|
final status = await Permission.camera.status.timeout(
|
|
Duration(seconds: 5),
|
|
onTimeout: () => throw TimeoutException('Timeout saat meminta izin kamera'),
|
|
);
|
|
|
|
if (!status.isGranted) {
|
|
final result = await Permission.camera.request().timeout(
|
|
Duration(seconds: 5),
|
|
onTimeout: () => throw TimeoutException('Timeout saat meminta izin kamera'),
|
|
);
|
|
|
|
if (result.isDenied) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Izin kamera diperlukan untuk mengambil foto'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Buka kamera dengan pengaturan yang lebih optimal
|
|
final ImagePicker picker = ImagePicker();
|
|
final XFile? photo = await picker.pickImage(
|
|
source: ImageSource.camera,
|
|
imageQuality: 60, // Kualitas lebih rendah untuk performa lebih baik
|
|
maxWidth: 1024, // Ukuran lebih kecil
|
|
maxHeight: 768, // Aspek ratio 4:3 untuk foto
|
|
preferredCameraDevice: CameraDevice.rear,
|
|
).timeout(
|
|
Duration(seconds: 120),
|
|
onTimeout: () => throw TimeoutException('Timeout saat mengambil foto'),
|
|
);
|
|
|
|
if (photo != null && mounted) {
|
|
// Baca dan kompres file
|
|
final bytes = await photo.readAsBytes();
|
|
|
|
// Perbarui state
|
|
setState(() {
|
|
_photoFile = photo;
|
|
_imageData = bytes;
|
|
});
|
|
|
|
// Feedback sukses
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Foto berhasil diambil'),
|
|
backgroundColor: Colors.green,
|
|
duration: Duration(seconds: 1),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} on TimeoutException catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Waktu pengambilan foto habis: ${e.message}'),
|
|
backgroundColor: Colors.orange,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Gagal mengambil foto: ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _submitReport() async {
|
|
if (!mounted) return;
|
|
if (!_formKey.currentState!.validate()) {
|
|
showErrorDialog(context, 'Mohon lengkapi semua field yang diperlukan');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showLoadingDialog(context);
|
|
|
|
if (_imageData == null) {
|
|
Navigator.pop(context); // Tutup loading dialog
|
|
showErrorDialog(context, 'Mohon ambil foto kerusakan jalan terlebih dahulu');
|
|
return;
|
|
}
|
|
|
|
if (_latitude == null || _longitude == null) {
|
|
Navigator.pop(context); // Tutup loading dialog
|
|
showErrorDialog(context, 'Mohon pilih lokasi jalan yang rusak');
|
|
return;
|
|
}
|
|
|
|
if (_selectedDisasterType == null || _selectedDisasterType!.isEmpty) {
|
|
Navigator.pop(context); // Tutup loading dialog
|
|
showErrorDialog(context, 'Mohon pilih tingkat kerusakan jalan');
|
|
return;
|
|
}
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
final tempFile = File('${tempDir.path}/temp_image.jpg');
|
|
await tempFile.writeAsBytes(_imageData!);
|
|
|
|
// Tambahkan waktu WIB
|
|
final now = DateTime.now();
|
|
final wibTime = now.toUtc().add(Duration(hours: 7)); // Konversi ke WIB (UTC+7)
|
|
final wibTimeString = wibTime.toIso8601String().replaceAll('Z', '+07:00'); // Format ke WIB
|
|
|
|
final response = await _apiService.laporJalan(
|
|
lokasi: _locationController.text,
|
|
latitude: _latitude!,
|
|
longitude: _longitude!,
|
|
jenisRusak: _selectedDisasterType!,
|
|
deskripsi: _descriptionController.text,
|
|
foto: tempFile,
|
|
waktu: wibTimeString,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
Navigator.pop(context);
|
|
|
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
await showSuccessDialog(context);
|
|
Navigator.pushAndRemoveUntil(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => DashboardPage()),
|
|
(route) => false,
|
|
);
|
|
} else {
|
|
// Handle error response
|
|
final responseData = response.data;
|
|
String errorMessage = responseData['message'] ?? 'Terjadi kesalahan saat mengirim laporan';
|
|
|
|
if (errorMessage.contains('luar wilayah Lumajang')) {
|
|
showErrorDialog(context, 'Lokasi yang dipilih berada di luar wilayah Lumajang. Mohon pilih lokasi yang berada dalam wilayah Lumajang.');
|
|
} else if (errorMessage.contains('batas maksimal laporan')) {
|
|
showErrorDialog(context, 'Anda telah mencapai batas maksimal laporan hari ini (10 laporan). Silakan coba lagi besok.');
|
|
} else {
|
|
showErrorDialog(context, errorMessage);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
if (!mounted) return;
|
|
Navigator.pop(context); // Tutup loading dialog
|
|
|
|
String errorMessage = '';
|
|
if (e is NetworkException) {
|
|
errorMessage = 'Tidak dapat terhubung ke server. Mohon periksa koneksi internet Anda.';
|
|
} else if (e is TimeoutException) {
|
|
errorMessage = 'Waktu koneksi habis. Silakan coba lagi.';
|
|
} else if (e is ServerException) {
|
|
errorMessage = 'Terjadi kesalahan pada server: ${e.message}';
|
|
} else if (e is ValidationException) {
|
|
errorMessage = e.message;
|
|
} else {
|
|
errorMessage = 'Terjadi kesalahan yang tidak diketahui. Silakan coba lagi nanti.';
|
|
}
|
|
|
|
showErrorDialog(context, errorMessage);
|
|
}
|
|
}
|
|
|
|
void _onMapMove(MapPosition position, bool hasGesture) {
|
|
if (hasGesture) {
|
|
setState(() {
|
|
cameraPosition = position.center!;
|
|
// textController.text = "checking ...";
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
|
|
|
return Scaffold(
|
|
backgroundColor: ThemeColors.primary(context),
|
|
body: Stack(
|
|
children: [
|
|
// Layer 1: Background image
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
isDarkMode
|
|
? 'assets/images/background_dark.png'
|
|
: 'assets/images/background_light.png',
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
|
|
// Layer 2: Content
|
|
SafeArea(
|
|
child: Column(
|
|
children: [
|
|
// Custom AppBar
|
|
Container(
|
|
padding: EdgeInsets.all(16),
|
|
child: Text(
|
|
'Lapor Jalan Rusak',
|
|
style: TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
color: ThemeColors.textPrimary(context),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Main Content dengan padding bottom untuk bottom bar
|
|
Expanded(
|
|
child: SingleChildScrollView(
|
|
physics: ClampingScrollPhysics(),
|
|
child: Padding(
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
bottom: 100, // Tambahkan padding untuk bottom bar
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// Map Container
|
|
Container(
|
|
height: 300,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: ThemeColors.shadowColor(context),
|
|
blurRadius: 15,
|
|
offset: Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(20),
|
|
child: MapPicker(
|
|
mapPickerController: mapPickerController,
|
|
child: FlutterMap(
|
|
mapController: mapController,
|
|
options: MapOptions(
|
|
initialCenter: cameraPosition,
|
|
initialZoom: cameraZoom,
|
|
onTap: (_, point) {
|
|
setState(() {
|
|
cameraPosition = point;
|
|
_latitude = point.latitude;
|
|
_longitude = point.longitude;
|
|
_locationController.text = 'Lat: $_latitude, Lng: $_longitude';
|
|
});
|
|
},
|
|
),
|
|
children: [
|
|
TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
|
|
),
|
|
MarkerLayer(
|
|
markers: [
|
|
Marker(
|
|
width: 40,
|
|
height: 40,
|
|
point: cameraPosition,
|
|
child: Icon(
|
|
Icons.location_on,
|
|
color: ThemeColors.error,
|
|
size: 40,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 20),
|
|
|
|
// Form Container
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: ThemeColors.background(context),
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: ThemeColors.shadowColor(context),
|
|
blurRadius: 15,
|
|
offset: Offset(0, 5),
|
|
),
|
|
],
|
|
),
|
|
padding: EdgeInsets.all(20),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Location Field
|
|
TextFormField(
|
|
controller: _locationController,
|
|
readOnly: true,
|
|
decoration: InputDecoration(
|
|
hintText: 'Lokasi',
|
|
hintStyle: TextStyle(color: ThemeColors.textGrey(context)),
|
|
prefixIcon: Icon(Icons.location_on, color: ThemeColors.inputIcon(context)),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: ThemeColors.inputFill(context),
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
|
|
// Get Location Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _getCurrentLocation,
|
|
icon: Icon(Icons.my_location),
|
|
label: Text('Gunakan Lokasi Saat Ini'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ThemeColors.accent(context),
|
|
foregroundColor: ThemeColors.buttonText(context),
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
|
|
// Dropdown Jenis Kerusakan
|
|
DropdownButtonFormField<String>(
|
|
value: _selectedDisasterType,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
_selectedDisasterType = newValue;
|
|
});
|
|
},
|
|
decoration: InputDecoration(
|
|
// labelText: 'Jenis Kerusakan',
|
|
hintText: 'Pilih Jenis Kerusakan',
|
|
prefixIcon: Icon(Icons.warning_amber, color: ThemeColors.inputIcon(context)),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: ThemeColors.inputFill(context),
|
|
),
|
|
items: [
|
|
DropdownMenuItem(
|
|
value: 'jalan rusak ringan',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.warning, color: Colors.orange),
|
|
SizedBox(width: 8.0),
|
|
Text('Kerusakan Ringan'),
|
|
],
|
|
),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'jalan rusak berat',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.dangerous, color: Colors.red),
|
|
SizedBox(width: 8.0),
|
|
Text('Kerusakan Berat'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Mohon pilih jenis kerusakan';
|
|
}
|
|
return null;
|
|
},
|
|
hint: Text('Pilih Jenis Kerusakan'),
|
|
),
|
|
SizedBox(height: 16),
|
|
|
|
// Description Field
|
|
TextFormField(
|
|
controller: _descriptionController,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: 'Deskripsi Kerusakan',
|
|
hintStyle: TextStyle(color: ThemeColors.textGrey(context)),
|
|
prefixIcon: Icon(Icons.description, color: ThemeColors.inputIcon(context)),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide.none,
|
|
),
|
|
filled: true,
|
|
fillColor: ThemeColors.inputFill(context),
|
|
),
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Mohon isi deskripsi kerusakan';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
SizedBox(height: 16),
|
|
|
|
// Photo Container
|
|
Container(
|
|
width: double.infinity,
|
|
height: 200,
|
|
decoration: BoxDecoration(
|
|
color: ThemeColors.inputFill(context),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(
|
|
color: ThemeColors.inputBorder(context),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: _imageData != null
|
|
? ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.memory(
|
|
_imageData!,
|
|
fit: BoxFit.cover,
|
|
),
|
|
)
|
|
: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.camera_alt,
|
|
size: 48,
|
|
color: ThemeColors.textGrey(context),
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Tambahkan Foto',
|
|
style: TextStyle(
|
|
color: ThemeColors.textGrey(context),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
|
|
// Take Photo Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: _pickImage,
|
|
icon: Icon(Icons.camera_alt),
|
|
label: Text('Ambil Foto'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ThemeColors.accent(context),
|
|
foregroundColor: ThemeColors.buttonText(context),
|
|
padding: EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 24),
|
|
|
|
// Submit Button
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
if (_formKey.currentState!.validate()) {
|
|
_submitReport();
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ThemeColors.accent(context),
|
|
foregroundColor: ThemeColors.buttonText(context),
|
|
padding: EdgeInsets.symmetric(vertical: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
child: Text(
|
|
'KIRIM LAPORAN',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Layer 3: Bottom Bar yang mengambang
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: CustomBottomBar(currentPage: 'lapor'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDisasterTypeDropdown() {
|
|
return DropdownButtonFormField<String>(
|
|
value: _selectedDisasterType,
|
|
onChanged: (String? newValue) {
|
|
setState(() {
|
|
_selectedDisasterType = newValue;
|
|
});
|
|
},
|
|
decoration: InputDecoration(
|
|
labelText: 'Jenis Kerusakan',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: [
|
|
DropdownMenuItem(
|
|
value: 'jalan rusak ringan',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.invert_colors, color: Colors.blue),
|
|
SizedBox(width: 8.0),
|
|
Text('Kerusakan Ringan'),
|
|
],
|
|
),
|
|
),
|
|
DropdownMenuItem(
|
|
value: 'jalan rusak berat',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.terrain, color: Colors.brown),
|
|
SizedBox(width: 8.0),
|
|
Text('Kerusakan Berat'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
hint: Text('Pilih Jenis Kerusakan'),
|
|
);
|
|
}
|
|
|
|
Widget _buildLocationSelection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_pickImage();
|
|
},
|
|
icon: Icon(Icons.camera_alt, color: Colors.white),
|
|
label: Text(
|
|
'Ambil Foto dengan Kamera',
|
|
style: TextStyle(fontSize: 16.0, color: Colors.white),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: Colors.white,
|
|
backgroundColor: const Color(0xFF4CAF50), // Warna hijau
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
),
|
|
elevation: 5.0,
|
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
),
|
|
),
|
|
SizedBox(height: 16.0),
|
|
_buildImagePreview(),
|
|
SizedBox(height: 16.0),
|
|
ElevatedButton.icon(
|
|
onPressed: () {
|
|
_getCurrentLocation();
|
|
},
|
|
icon: Icon(Icons.my_location, color: const Color(0xFF020306)),
|
|
label: Text(
|
|
'Pilih Lokasi Anda Sekarang',
|
|
style: TextStyle(fontSize: 16.0, color: const Color(0xFF020306)),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFFF9C416),
|
|
elevation: 5.0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.0),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildImagePreview() {
|
|
if (_imageData != null) {
|
|
return Container(
|
|
height: 250,
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 10,
|
|
spreadRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
// Preview Foto
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Image.memory(
|
|
_imageData!,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
// Overlay gelap semi-transparan
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
Colors.black.withOpacity(0.3),
|
|
Colors.transparent,
|
|
Colors.transparent,
|
|
Colors.black.withOpacity(0.3),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Label Preview Foto
|
|
Positioned(
|
|
top: 8,
|
|
right: 8,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.black.withOpacity(0.6),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.camera_alt, color: Colors.white, size: 16),
|
|
SizedBox(width: 4),
|
|
Text(
|
|
'Preview Foto',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
// Tombol Aksi
|
|
Positioned(
|
|
bottom: 8,
|
|
left: 8,
|
|
right: 8,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
// Tombol Ambil Ulang
|
|
Container(
|
|
child: IconButton(
|
|
icon: Icon(Icons.refresh, color: Colors.white),
|
|
onPressed: _pickImage,
|
|
tooltip: 'Ambil Ulang Foto',
|
|
),
|
|
),
|
|
SizedBox(width: 8),
|
|
// Tombol Hapus
|
|
Container(
|
|
child: IconButton(
|
|
icon: Icon(Icons.delete_outline, color: Colors.white),
|
|
onPressed: () {
|
|
setState(() {
|
|
_imageData = null;
|
|
_photoFile = null;
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Foto telah dihapus'),
|
|
backgroundColor: Colors.grey,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
},
|
|
tooltip: 'Hapus Foto',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Tampilan saat tidak ada foto
|
|
return Container(
|
|
padding: EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey[300]!),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(
|
|
Icons.camera_alt_outlined,
|
|
size: 48,
|
|
color: Colors.grey[600],
|
|
),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'Belum ada foto yang diambil',
|
|
style: TextStyle(
|
|
color: Colors.grey[600],
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showConfirmationDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Konfirmasi Laporkan'),
|
|
content: Text('Data Anda akan segera dikonfirmasi oleh admin.'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
_submitReport(); // Memanggil fungsi untuk mengirim laporan
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
foregroundColor: const Color(0xFF00BFF3),
|
|
),
|
|
child: Text('Laporkan'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Bersihkan memori
|
|
_imageData = null;
|
|
_photoFile = null;
|
|
_descriptionController.dispose();
|
|
_locationController.dispose();
|
|
textController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
TileLayer get openStreetMapTileLayer => TileLayer(
|
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
|
|
);
|
|
|
|
class ValidationException implements Exception {
|
|
final String message;
|
|
ValidationException(this.message);
|
|
}
|
|
|
|
class NetworkException implements Exception {
|
|
final String message;
|
|
NetworkException(this.message);
|
|
}
|
|
|
|
class ServerException implements Exception {
|
|
final String message;
|
|
ServerException(this.message);
|
|
}
|
|
|
|
Future<void> showSuccessDialog(BuildContext context) async {
|
|
return showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
title: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.check_circle,
|
|
color: Colors.green,
|
|
size: 50,
|
|
),
|
|
SizedBox(height: 10),
|
|
Text(
|
|
'Berhasil!',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Text(
|
|
'Laporan kerusakan jalan Anda telah berhasil dikirim dan akan segera diproses oleh tim kami.',
|
|
textAlign: TextAlign.center,
|
|
),
|
|
actions: [
|
|
Center(
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: ThemeColors.accent(context),
|
|
minimumSize: Size(200, 45),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'OK, Saya Mengerti',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
actionsAlignment: MainAxisAlignment.center,
|
|
contentPadding: EdgeInsets.fromLTRB(24, 20, 24, 0),
|
|
actionsPadding: EdgeInsets.all(16),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void showErrorDialog(BuildContext context, String message) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
title: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline,
|
|
color: Colors.red,
|
|
size: 50,
|
|
),
|
|
SizedBox(height: 10),
|
|
Text(
|
|
'Terjadi Kesalahan',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Text(
|
|
message,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
actions: [
|
|
Center(
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
minimumSize: Size(200, 45),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Tutup',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
actionsAlignment: MainAxisAlignment.center,
|
|
contentPadding: EdgeInsets.fromLTRB(24, 20, 24, 0),
|
|
actionsPadding: EdgeInsets.all(16),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void showLoadingDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return WillPopScope(
|
|
onWillPop: () async => false,
|
|
child: AlertDialog(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(15),
|
|
),
|
|
content: Container(
|
|
padding: EdgeInsets.symmetric(vertical: 20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(height: 20),
|
|
Text(
|
|
'Sedang memproses...',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|