MIF_E31222656/lib/screens/calendar/field_management_screen.dart

2686 lines
107 KiB
Dart

import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
import 'package:tugas_akhir_supabase/domain/entities/field.dart'; // Import the Field entity
import 'package:tugas_akhir_supabase/screens/calendar/add_field_bottom_sheet.dart';
import 'package:tugas_akhir_supabase/screens/calendar/field_converter.dart';
import 'package:tugas_akhir_supabase/screens/calendar/fix_fields_table.dart';
import 'package:tugas_akhir_supabase/screens/calendar/location_picker_dialog.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:math' as math;
// final supabase = Supabase.instance.client; // Sebaiknya akses via Supabase.instance.client di dalam method
class FieldManagementScreen extends StatefulWidget {
const FieldManagementScreen({super.key});
@override
State<FieldManagementScreen> createState() => _FieldManagementScreenState();
}
class _FieldManagementScreenState extends State<FieldManagementScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _locationController = TextEditingController();
final _areaSizeController = TextEditingController();
final _ownerNameController = TextEditingController();
final _customRegionController = TextEditingController();
final _customSystemTypeController = TextEditingController();
final _customLandFeatureController = TextEditingController();
int _plotCount = 1; // Default plot count
List<Field> _fields = [];
Field? _editingField;
bool _isLoadingFields = true;
bool _isSaving = false;
// Koordinat lokasi
double? _latitude;
double? _longitude;
// Region selection
String _selectedRegion = 'Jawa';
final List<String> _regions = [
'Jawa',
'Sumatera',
'Kalimantan',
'Sulawesi',
'Bali & Nusa Tenggara',
'Maluku & Papua',
'Lainnya',
];
// Ownership type selection
String _selectedOwnershipType = 'Milik Sendiri';
final List<String> _ownershipTypes = [
'Milik Sendiri',
'Sewa',
'Bagi Hasil',
'Lainnya',
];
// Region-specific form fields
final Map<String, dynamic> _regionSpecificData = {};
// Jawa specific fields
String _selectedPetakSystem = 'Petak Sawah';
final List<String> _petakSystems = [
'Petak Sawah',
'Tegal',
'Kebun',
'Campuran',
];
String _selectedIrigationType = 'Teknis';
final List<String> _irigationTypes = [
'Teknis',
'Semi Teknis',
'Sederhana',
'Tadah Hujan',
];
// Sumatera specific fields
String _selectedBlokSystem = 'Ladang';
final List<String> _blokSystems = ['Ladang', 'Kebun', 'Plasma', 'Campuran'];
String _selectedSoilType = 'Mineral';
final List<String> _soilTypes = [
'Mineral',
'Gambut',
'Pasir',
'Liat',
'Campuran',
];
// Kalimantan specific fields
String _selectedLadangSystem = 'Tetap';
final List<String> _ladangSystems = ['Tetap', 'Berpindah', 'Semi Permanen'];
String _riverDistance = '';
// Sulawesi specific fields
String _selectedKebunSystem = 'Permanen';
final List<String> _kebunSystems = ['Permanen', 'Rotasi', 'Campuran'];
String _selectedTerrainType = 'Datar';
final List<String> _terrainTypes = [
'Datar',
'Berbukit',
'Terasering',
'Lereng',
];
// Bali & Nusa Tenggara specific fields
String _selectedSubakSystem = 'Subak Tradisional';
final List<String> _subakSystems = [
'Subak Tradisional',
'Subak Modern',
'Non-Subak',
'Campuran',
];
String _selectedWaterSourceType = 'Mata Air';
final List<String> _waterSourceTypes = [
'Mata Air',
'Sungai',
'Bendungan',
'Air Tanah',
'Tadah Hujan',
];
// Maluku & Papua specific fields
String _selectedGardenSystem = 'Kebun Sagu';
final List<String> _gardenSystems = [
'Kebun Sagu',
'Perkebunan Kelapa',
'Agroforestri',
'Ladang',
];
String _selectedForestType = 'Hutan Primer';
final List<String> _forestTypes = [
'Hutan Primer',
'Hutan Sekunder',
'Bekas Tebangan',
'Lahan Konversi',
];
// Tambahkan MapController di _FieldManagementScreenState
final MapController _detailMapController = MapController();
@override
void initState() {
super.initState();
_loadFields();
}
@override
void dispose() {
// Hapus kode yang mungkin mengganggu keyboard
_nameController.dispose();
_locationController.dispose();
_areaSizeController.dispose();
_ownerNameController.dispose();
_customRegionController.dispose();
_customSystemTypeController.dispose();
_customLandFeatureController.dispose();
super.dispose();
}
// Update region-specific data based on selected region
void _updateRegionSpecificData() {
// Pastikan _regionSpecificData tidak null dengan aman
switch (_selectedRegion) {
case 'Jawa':
_regionSpecificData.clear();
_regionSpecificData['sistem_petak'] = _selectedPetakSystem;
_regionSpecificData['jenis_irigasi'] = _selectedIrigationType;
break;
case 'Sumatera':
_regionSpecificData.clear();
_regionSpecificData['sistem_blok'] = _selectedBlokSystem;
_regionSpecificData['jenis_tanah'] = _selectedSoilType;
break;
case 'Kalimantan':
_regionSpecificData.clear();
_regionSpecificData['sistem_ladang'] = _selectedLadangSystem;
_regionSpecificData['jarak_sungai'] = _riverDistance;
break;
case 'Sulawesi':
_regionSpecificData.clear();
_regionSpecificData['sistem_kebun'] = _selectedKebunSystem;
_regionSpecificData['kontur_lahan'] = _selectedTerrainType;
break;
case 'Bali & Nusa Tenggara':
_regionSpecificData.clear();
_regionSpecificData['sistem_subak'] = _selectedSubakSystem;
_regionSpecificData['sumber_air'] = _selectedWaterSourceType;
break;
case 'Maluku & Papua':
_regionSpecificData.clear();
_regionSpecificData['sistem_kebun'] = _selectedGardenSystem;
_regionSpecificData['tipe_hutan'] = _selectedForestType;
break;
case 'Lainnya':
_regionSpecificData.clear();
_regionSpecificData['nama_wilayah'] = _customRegionController.text;
_regionSpecificData['sistem_lahan'] = _customSystemTypeController.text;
_regionSpecificData['fitur_lahan'] = _customLandFeatureController.text;
break;
default:
_regionSpecificData.clear();
}
// Tambahkan log untuk debugging
debugPrint('Updated region specific data: $_regionSpecificData');
}
Future<void> _loadFields() async {
if (!mounted) return;
setState(() => _isLoadingFields = true);
try {
final userId = Supabase.instance.client.auth.currentUser?.id;
if (userId == null) {
_showError('User ID is null');
return;
}
// Batasi jumlah field yang diambil untuk mengurangi beban
final response = await Supabase.instance.client
.from('fields')
.select(
'id, name, user_id, plot_count, region, location, area_size, area_unit, ownership_type, owner_name, latitude, longitude, created_at',
)
.eq('user_id', userId)
.order('created_at', ascending: false)
.limit(20) // Batasi jumlah data
.timeout(const Duration(seconds: 5));
// Batasi jumlah field yang ditampilkan
final limitedFields = (response as List).take(10).toList();
if (mounted) {
setState(() {
_fields = List<Field>.from(
limitedFields.map((field) => FieldConverter.fromJson(field)),
);
_isLoadingFields = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _isLoadingFields = false);
String errorMessage = 'Gagal memuat data lahan';
if (e.toString().contains('timeout')) {
errorMessage = 'Koneksi timeout. Coba lagi nanti.';
} else if (e.toString().contains('network')) {
errorMessage =
'Masalah koneksi jaringan. Periksa koneksi internet Anda.';
} else if (e.toString().contains('permission')) {
errorMessage =
'Masalah izin akses database. Coba klik tombol Perbaiki Database.';
} else {
errorMessage =
'Terjadi kesalahan: ${e.toString().substring(0, math.min(100, e.toString().length))}';
}
_showError(errorMessage);
}
}
}
Future<void> _saveField() async {
if (!_formKey.currentState!.validate()) return;
// Dismiss keyboard immediately when saving
FocusScope.of(context).unfocus();
final userId = Supabase.instance.client.auth.currentUser?.id;
if (userId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('User tidak ditemukan, silakan login ulang.'),
backgroundColor: Colors.red,
),
);
return;
}
try {
// Update region-specific data
_updateRegionSpecificData();
// Log data yang akan disimpan untuk debugging
debugPrint('Saving field with name: ${_nameController.text}');
debugPrint('Region: $_selectedRegion');
debugPrint('Plot count: $_plotCount');
debugPrint('Region specific data: $_regionSpecificData');
debugPrint('Coordinates: $_latitude, $_longitude');
if (_editingField == null) {
// Create
final id = const Uuid().v4();
await Supabase.instance.client
.from('fields')
.insert({
'id': id,
'user_id': userId,
'name': _nameController.text,
'plot_count': _plotCount,
'region':
_selectedRegion == 'Lainnya'
? _customRegionController.text
: _selectedRegion,
'location': _locationController.text,
'latitude': _latitude,
'longitude': _longitude,
'area_size':
_areaSizeController.text.isNotEmpty
? double.parse(_areaSizeController.text)
: null,
'area_unit': '',
'ownership_type': _selectedOwnershipType,
'owner_name':
_selectedOwnershipType != 'Milik Sendiri'
? _ownerNameController.text
: null,
'region_specific_data': _regionSpecificData,
})
.timeout(const Duration(seconds: 10));
} else {
// Update - Perbaikan untuk masalah trigger
// Hapus field updated_at dari data yang dikirim
final updateData = {
'name': _nameController.text,
'plot_count': _plotCount,
'region':
_selectedRegion == 'Lainnya'
? _customRegionController.text
: _selectedRegion,
'location': _locationController.text,
'latitude': _latitude,
'longitude': _longitude,
'area_size':
_areaSizeController.text.isNotEmpty
? double.parse(_areaSizeController.text)
: null,
'area_unit': '',
'ownership_type': _selectedOwnershipType,
'owner_name':
_selectedOwnershipType != 'Milik Sendiri'
? _ownerNameController.text
: null,
'region_specific_data': _regionSpecificData,
};
// Log data yang akan diupdate
debugPrint('Updating field with ID: ${_editingField!.id}');
debugPrint('Update data: $updateData');
try {
final response = await Supabase.instance.client
.from('fields')
.update(updateData)
.eq('id', _editingField!.id)
.timeout(const Duration(seconds: 10));
debugPrint('Update response: $response');
} catch (updateError) {
// Jika update gagal karena trigger, coba dengan pendekatan delete dan insert ulang
if (updateError.toString().contains('updated_at')) {
debugPrint(
'Update error due to updated_at field, trying delete and reinsert approach',
);
// Simpan ID lama
final oldId = _editingField!.id;
// Hapus data lama
await Supabase.instance.client
.from('fields')
.delete()
.eq('id', oldId)
.timeout(const Duration(seconds: 10));
// Insert data baru dengan ID yang sama
await Supabase.instance.client
.from('fields')
.insert({
'id': oldId,
'user_id': userId,
'name': _nameController.text,
'plot_count': _plotCount,
'region':
_selectedRegion == 'Lainnya'
? _customRegionController.text
: _selectedRegion,
'location': _locationController.text,
'latitude': _latitude,
'longitude': _longitude,
'area_size':
_areaSizeController.text.isNotEmpty
? double.parse(_areaSizeController.text)
: null,
'area_unit': '',
'ownership_type': _selectedOwnershipType,
'owner_name':
_selectedOwnershipType != 'Milik Sendiri'
? _ownerNameController.text
: null,
'region_specific_data': _regionSpecificData,
})
.timeout(const Duration(seconds: 10));
} else {
// Jika bukan masalah updated_at, rethrow error
rethrow;
}
}
}
// Reset form tanpa menggunakan setState
_resetForm();
} catch (e) {
debugPrint('Error saving field: $e');
rethrow; // Rethrow exception untuk ditangkap oleh caller
}
}
Future<void> _deleteField(Field field) async {
final confirm = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Konfirmasi Hapus'),
content: Text('Yakin ingin menghapus lahan "${field.name}"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Batal'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Hapus'),
),
],
),
);
if (confirm != true) return;
setState(() => _isSaving = true);
try {
await Supabase.instance.client
.from('fields')
.delete()
.eq('id', field.id)
.timeout(const Duration(seconds: 10));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Lahan berhasil dihapus'),
backgroundColor: Colors.green,
),
);
}
await _loadFields();
} catch (e) {
if (mounted) {
setState(() => _isSaving = false);
_showError('Gagal menghapus lahan: ${e.toString()}');
}
}
}
void _resetForm() {
// Dismiss keyboard when resetting the form
FocusScope.of(context).unfocus();
_formKey.currentState?.reset(); // Ini akan mereset state validator
_nameController.clear();
_locationController.clear();
_areaSizeController.clear();
_ownerNameController.clear();
_customRegionController.clear();
_customSystemTypeController.clear();
_customLandFeatureController.clear();
_plotCount = 1; // Reset ke nilai default
_editingField = null;
_latitude = null; // Reset koordinat
_longitude = null; // Reset koordinat
if (mounted) {
setState(() {
_selectedRegion = 'Jawa';
_selectedOwnershipType = 'Milik Sendiri';
_regionSpecificData.clear();
}); // Update UI untuk membersihkan form dan kembali ke mode "Tambah"
}
}
void _startEdit(Field field) {
// Dismiss keyboard when starting an edit
FocusScope.of(context).unfocus();
// Set field values
_nameController.text = field.name;
_plotCount = field.plotCount;
// Set region-specific fields
if (field.region != null && _regions.contains(field.region)) {
_selectedRegion = field.region!;
} else {
_selectedRegion = 'Lainnya';
_customRegionController.text = field.region ?? '';
}
_locationController.text = field.location ?? '';
_latitude = field.latitude; // Set latitude
_longitude = field.longitude; // Set longitude
_areaSizeController.text = field.areaSize?.toString() ?? '';
_selectedOwnershipType = field.ownershipType ?? 'Milik Sendiri';
_ownerNameController.text = field.ownerName ?? '';
// Set region-specific data
_regionSpecificData.clear(); // Pastikan clear dulu
if (field.regionSpecificData != null) {
_regionSpecificData.addAll(field.regionSpecificData!);
// Set specific region values
switch (_selectedRegion) {
case 'Jawa':
_selectedPetakSystem =
field.regionSpecificData!['sistem_petak'] ?? _petakSystems[0];
_selectedIrigationType =
field.regionSpecificData!['jenis_irigasi'] ?? _irigationTypes[0];
break;
case 'Sumatera':
_selectedBlokSystem =
field.regionSpecificData!['sistem_blok'] ?? _blokSystems[0];
_selectedSoilType =
field.regionSpecificData!['jenis_tanah'] ?? _soilTypes[0];
break;
case 'Kalimantan':
_selectedLadangSystem =
field.regionSpecificData!['sistem_ladang'] ?? _ladangSystems[0];
_riverDistance = field.regionSpecificData!['jarak_sungai'] ?? '';
break;
case 'Sulawesi':
_selectedKebunSystem =
field.regionSpecificData!['sistem_kebun'] ?? _kebunSystems[0];
_selectedTerrainType =
field.regionSpecificData!['kontur_lahan'] ?? _terrainTypes[0];
break;
case 'Bali & Nusa Tenggara':
_selectedSubakSystem =
field.regionSpecificData!['sistem_subak'] ?? _subakSystems[0];
_selectedWaterSourceType =
field.regionSpecificData!['sumber_air'] ?? _waterSourceTypes[0];
break;
case 'Maluku & Papua':
_selectedGardenSystem =
field.regionSpecificData!['sistem_kebun'] ?? _gardenSystems[0];
_selectedForestType =
field.regionSpecificData!['tipe_hutan'] ?? _forestTypes[0];
break;
}
} else {
// Inisialisasi dengan data default sesuai region
_updateRegionSpecificData();
}
_editingField = field;
// Reset saving state
setState(() {
_isSaving = false;
});
// Tampilkan bottom sheet untuk edit
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder:
(BuildContext bottomSheetContext) => StatefulBuilder(
builder: (BuildContext context, StateSetter setModalState) {
// Fungsi untuk menyimpan data
void saveData() {
// Update region-specific data
_updateRegionSpecificData();
// Set loading state
setModalState(() {
_isSaving = true;
});
// Save field
_saveField()
.then((_) {
setModalState(() {
_isSaving = false;
});
Navigator.pop(bottomSheetContext);
})
.catchError((error) {
setModalState(() {
_isSaving = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Gagal menyimpan: ${error.toString()}'),
backgroundColor: Colors.red,
),
);
});
}
return DraggableScrollableSheet(
initialChildSize: 0.9,
minChildSize: 0.7,
maxChildSize: 0.95,
builder:
(_, scrollController) => Container(
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: ListView(
controller: scrollController,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Edit Lahan',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
),
onPressed:
() => Navigator.pop(bottomSheetContext),
),
],
),
const SizedBox(height: 16),
// Nama lahan
TextFormField(
controller: _nameController,
decoration: InputDecoration(
labelText: 'Nama Lahan',
labelStyle: TextStyle(
color: Colors.white.withOpacity(0.9),
),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.eco,
color: Colors.white.withOpacity(0.9),
),
),
style: const TextStyle(color: Colors.white),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama lahan harus diisi';
}
return null;
},
),
const SizedBox(height: 16),
// Lokasi - Diganti dengan input dan tombol pilih lokasi
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Lokasi',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(
12,
),
),
child: TextFormField(
controller: _locationController,
readOnly: true,
decoration: InputDecoration(
hintText: 'Pilih lokasi di peta',
hintStyle: TextStyle(
color: Colors.white.withOpacity(
0.7,
),
),
border: InputBorder.none,
prefixIcon: Icon(
Icons.location_on,
color: Colors.white.withOpacity(
0.9,
),
),
contentPadding:
const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
style: const TextStyle(
color: Colors.white,
),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
_showLocationPicker();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
12,
),
),
padding: const EdgeInsets.all(12),
),
child: const Icon(Icons.map),
),
],
),
if (_latitude != null && _longitude != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'Koordinat: ${_latitude!.toStringAsFixed(6)}, ${_longitude!.toStringAsFixed(6)}',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 16),
// Jumlah petak
Row(
children: [
Text(
'Jumlah Petak:',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 16,
),
),
const SizedBox(width: 16),
IconButton(
icon: const Icon(
Icons.remove_circle,
color: Colors.white,
),
onPressed:
_plotCount > 1
? () =>
setModalState(() => _plotCount--)
: null,
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_plotCount',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
IconButton(
icon: const Icon(
Icons.add_circle,
color: Colors.white,
),
onPressed:
() => setModalState(() => _plotCount++),
),
],
),
const SizedBox(height: 16),
// Wilayah
DropdownButtonFormField<String>(
value: _selectedRegion,
decoration: InputDecoration(
labelText: 'Wilayah',
labelStyle: TextStyle(
color: Colors.white.withOpacity(0.9),
),
prefixIcon: Icon(
Icons.map,
color: Colors.white.withOpacity(0.9),
),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
dropdownColor: AppColors.primary,
style: const TextStyle(color: Colors.white),
items:
_regions.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setModalState(() {
_selectedRegion = newValue;
});
}
},
),
const SizedBox(height: 16),
// Region-specific fields based on selected region
Builder(
builder: (context) {
switch (_selectedRegion) {
case 'Jawa':
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Text(
'Informasi Khusus Jawa',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _selectedPetakSystem,
decoration: InputDecoration(
labelText: 'Sistem Petak',
labelStyle: TextStyle(
color: Colors.white.withOpacity(
0.9,
),
),
prefixIcon: Icon(
Icons.grid_on,
color: Colors.white.withOpacity(
0.9,
),
),
filled: true,
fillColor: Colors.white.withOpacity(
0.2,
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
dropdownColor: AppColors.primary,
style: const TextStyle(
color: Colors.white,
),
items:
_petakSystems.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setModalState(() {
_selectedPetakSystem = newValue;
});
}
},
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedIrigationType,
decoration: InputDecoration(
labelText: 'Jenis Irigasi',
labelStyle: TextStyle(
color: Colors.white.withOpacity(
0.9,
),
),
prefixIcon: Icon(
Icons.water,
color: Colors.white.withOpacity(
0.9,
),
),
filled: true,
fillColor: Colors.white.withOpacity(
0.2,
),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
dropdownColor: AppColors.primary,
style: const TextStyle(
color: Colors.white,
),
items:
_irigationTypes.map((
String item,
) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setModalState(() {
_selectedIrigationType =
newValue;
});
}
},
),
],
);
// Tambahkan case untuk wilayah lain jika diperlukan
default:
return Container();
}
},
),
const SizedBox(height: 16),
// Luas lahan
TextFormField(
controller: _areaSizeController,
decoration: InputDecoration(
labelText: 'Luas Lahan (m²)',
labelStyle: TextStyle(
color: Colors.white.withOpacity(0.9),
),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.straighten,
color: Colors.white.withOpacity(0.9),
),
),
style: const TextStyle(color: Colors.white),
keyboardType: TextInputType.number,
validator: (value) {
if (value != null && value.isNotEmpty) {
if (double.tryParse(value) == null) {
return 'Masukkan angka yang valid';
}
}
return null;
},
),
const SizedBox(height: 16),
// Jenis kepemilikan
DropdownButtonFormField<String>(
value: _selectedOwnershipType,
decoration: InputDecoration(
labelText: 'Jenis Kepemilikan',
labelStyle: TextStyle(
color: Colors.white.withOpacity(0.9),
),
prefixIcon: Icon(
Icons.person,
color: Colors.white.withOpacity(0.9),
),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
dropdownColor: AppColors.primary,
style: const TextStyle(color: Colors.white),
items:
_ownershipTypes.map((String item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
onChanged: (String? newValue) {
if (newValue != null) {
setModalState(() {
_selectedOwnershipType = newValue;
});
}
},
),
const SizedBox(height: 16),
// Nama pemilik (jika bukan milik sendiri)
if (_selectedOwnershipType != 'Milik Sendiri')
TextFormField(
controller: _ownerNameController,
decoration: InputDecoration(
labelText: 'Nama Pemilik',
labelStyle: TextStyle(
color: Colors.white.withOpacity(0.9),
),
filled: true,
fillColor: Colors.white.withOpacity(0.2),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
prefixIcon: Icon(
Icons.person_outline,
color: Colors.white.withOpacity(0.9),
),
),
style: const TextStyle(color: Colors.white),
),
const SizedBox(height: 32),
// Tombol aksi
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed:
() => Navigator.pop(bottomSheetContext),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(
0.2,
),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('Batal'),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed:
_isSaving
? null
: () {
if (_formKey.currentState!
.validate()) {
// Update region-specific data
_updateRegionSpecificData();
// Log data yang akan disimpan
debugPrint(
'Saving data for field: ${_nameController.text}',
);
debugPrint(
'Region specific data: $_regionSpecificData',
);
// Set loading state
setModalState(() {
_isSaving = true;
});
// Save field
_saveField()
.then((_) {
// Reset loading state
setModalState(() {
_isSaving = false;
});
// Tutup form
Navigator.pop(
bottomSheetContext,
);
// Reload data setelah form ditutup
setState(() {
_isLoadingFields = true;
});
_loadFields().then((_) {
if (mounted) {
setState(() {
_isLoadingFields =
false;
});
}
});
// Tampilkan pesan sukses
ScaffoldMessenger.of(
context,
).showSnackBar(
const SnackBar(
content: Text(
'Lahan berhasil diperbarui',
),
backgroundColor:
Colors.green,
),
);
})
.catchError((error) {
debugPrint(
'Error saving field: $error',
);
setModalState(() {
_isSaving = false;
});
ScaffoldMessenger.of(
context,
).showSnackBar(
SnackBar(
content: Text(
'Gagal menyimpan: ${error.toString()}',
),
backgroundColor:
Colors.red,
),
);
});
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColors.primary,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child:
_isSaving
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primary,
),
)
: const Text('Simpan'),
),
),
],
),
],
),
),
),
);
},
),
);
}
@override
Widget build(BuildContext context) {
final isEditing = _editingField != null;
return Scaffold(
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text(
'Manajemen Lahan',
style: TextStyle(
fontWeight: FontWeight.w600,
fontSize: 20,
letterSpacing: 0.5,
),
),
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(16)),
),
bottom: PreferredSize(
preferredSize: Size.fromHeight(6),
child: _buildAppHeader(),
),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
tooltip: 'Refresh Data',
onPressed: () {
_loadFields();
},
),
],
),
body:
_isLoadingFields
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: AppColors.primary,
strokeWidth: 3,
),
const SizedBox(height: 16),
Text(
'Memuat data lahan...',
style: TextStyle(color: Colors.grey[700], fontSize: 16),
),
],
),
)
: _buildOptimizedContent(),
floatingActionButton: FloatingActionButton.extended(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
onPressed: () {
showAddFieldBottomSheet(
context: context,
onFieldAdded: () {
_loadFields();
},
);
},
icon: const Icon(Icons.add),
label: const Text('Add'),
elevation: 4,
),
);
}
Widget _buildOptimizedContent() {
if (_fields.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.landscape_outlined,
size: 60,
color: AppColors.primary.withOpacity(0.7),
),
),
const SizedBox(height: 24),
Text(
'Belum ada lahan tersimpan',
style: TextStyle(
color: Colors.grey[800],
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
'Tambahkan lahan baru untuk mulai mengelola tanaman Anda dengan lebih efektif.',
style: TextStyle(color: Colors.grey[600], fontSize: 16),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: () {
showAddFieldBottomSheet(
context: context,
onFieldAdded: () {
_loadFields();
},
);
},
icon: const Icon(Icons.add),
label: const Text('Tambah Lahan Baru'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
],
),
);
}
return _buildFieldsGrid();
}
Widget _buildFieldsGrid() {
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _fields.length,
itemBuilder: (context, index) {
final field = _fields[index];
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Material(
borderRadius: BorderRadius.circular(16),
elevation: 0,
color: Colors.white,
clipBehavior: Clip.antiAlias,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: () => _showFieldDetails(field),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.white,
_getRegionColor(field.region).withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan gradient sesuai region
Container(
height: 8,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary,
_getRegionColor(field.region),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Avatar lahan dengan efek glassmorphism
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary,
_getRegionColor(field.region),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: _getRegionColor(
field.region,
).withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Center(
child: Icon(
_getFieldIcon(field.region),
color: Colors.white,
size: 32,
),
),
),
const SizedBox(width: 16),
// Informasi lahan
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
field.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textSecondary,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
if (field.location != null &&
field.location!.isNotEmpty)
Row(
children: [
Icon(
Icons.location_on,
size: 14,
color: _getRegionColor(
field.region,
).withOpacity(0.7),
),
const SizedBox(width: 4),
Expanded(
child: Text(
field.location!,
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// Badges untuk informasi tambahan
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildInfoBadge(
Icons.grid_4x4,
'${field.plotCount} petak',
_getRegionColor(
field.region,
).withOpacity(0.15),
_getRegionColor(field.region),
),
_buildInfoBadge(
Icons.map,
field.region ?? 'Wilayah tidak diketahui',
_getRegionColor(
field.region,
).withOpacity(0.15),
_getRegionColor(field.region),
),
],
),
],
),
),
],
),
),
// Footer dengan tombol aksi
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Colors.grey[50]!,
_getRegionColor(field.region).withOpacity(0.07),
],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(16),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => _startEdit(field),
icon: Icon(
Icons.edit,
size: 18,
color: _getRegionColor(field.region),
),
label: Text(
'Edit',
style: TextStyle(
color: _getRegionColor(field.region),
fontWeight: FontWeight.w500,
),
),
),
TextButton.icon(
onPressed: () => _deleteField(field),
icon: const Icon(
Icons.delete,
size: 18,
color: AppColors.error,
),
label: const Text(
'Hapus',
style: TextStyle(
color: AppColors.error,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
],
),
),
],
),
),
),
),
);
},
);
}
Widget _buildInfoBadge(
IconData icon,
String text,
Color bgColor,
Color iconColor,
) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: iconColor.withOpacity(0.2), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: iconColor),
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: iconColor,
),
),
],
),
);
}
IconData _getFieldIcon(String? region) {
if (region == null) return Icons.landscape;
switch (region) {
case 'Jawa':
return Icons.grass;
case 'Sumatera':
return Icons.terrain;
case 'Kalimantan':
return Icons.forest;
case 'Sulawesi':
return Icons.agriculture;
case 'Bali & Nusa Tenggara':
return Icons.water;
case 'Maluku & Papua':
return Icons.park;
default:
return Icons.landscape;
}
}
Color _getRegionColor(String? region) {
if (region == null) return Colors.grey;
switch (region) {
case 'Jawa':
return Color(0xFF2E7D32); // Dark Green
case 'Sumatera':
return Color(0xFF1565C0); // Deep Blue
case 'Kalimantan':
return Color(0xFFEF6C00); // Deep Orange
case 'Sulawesi':
return Color(0xFF6A1B9A); // Deep Purple
case 'Bali & Nusa Tenggara':
return Color(0xFFC62828); // Deep Red
case 'Maluku & Papua':
return Color(0xFF00695C); // Deep Teal
default:
return Colors.grey[700]!;
}
}
// Header gradasi yang lebih kontras untuk banner aplikasi
Widget _buildAppHeader() {
return Container(
height: 6,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xFF1B5E20), // Dark Green
Color(0xFF1565C0), // Blue
Color(0xFFEF6C00), // Orange
Color(0xFF6A1B9A), // Purple
Color(0xFFC62828), // Red
Color(0xFF00695C), // Teal
],
stops: [0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
),
),
);
}
void _showFieldDetails(Field field) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder:
(context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
builder:
(_, scrollController) => Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white,
_getRegionColor(field.region).withOpacity(0.07),
],
),
),
child: Column(
children: [
// Header dengan gradient region
Container(
height: 8,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary,
_getRegionColor(field.region),
],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
borderRadius: const BorderRadius.vertical(
top: Radius.circular(20),
),
),
),
// Content
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.all(20),
children: [
// Header with field name and close button
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
field.name,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: _getRegionColor(field.region),
),
overflow: TextOverflow.ellipsis,
),
),
IconButton(
icon: Icon(
Icons.close,
color: _getRegionColor(
field.region,
).withOpacity(0.7),
),
onPressed: () => Navigator.pop(context),
),
],
),
// Hero section with location and visual
Container(
margin: const EdgeInsets.only(
top: 16,
bottom: 24,
),
child: Stack(
children: [
// Background card with gradient
Container(
height: 120,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary,
_getRegionColor(field.region),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: _getRegionColor(
field.region,
).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
// Tambahkan pattern overlay untuk efek visual
child: CustomPaint(
painter: PatternPainter(
color: Colors.white.withOpacity(0.1),
),
),
),
// Content overlay
Positioned.fill(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
// Icon
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: Colors.white.withOpacity(
0.2,
),
borderRadius:
BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withOpacity(
0.3,
),
width: 1,
),
),
child: Center(
child: Icon(
_getFieldIcon(field.region),
color: Colors.white,
size: 36,
),
),
),
const SizedBox(width: 16),
// Info
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
if (field.location != null &&
field.location!.isNotEmpty)
Row(
children: [
const Icon(
Icons.location_on,
size: 16,
color: Colors.white,
),
const SizedBox(width: 4),
Expanded(
child: Text(
field.location!,
style:
const TextStyle(
color:
Colors
.white,
fontSize: 16,
fontWeight:
FontWeight
.w500,
),
maxLines: 1,
overflow:
TextOverflow
.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
_buildDetailBadge(
Icons.grid_4x4,
'${field.plotCount} petak',
),
const SizedBox(width: 8),
_buildDetailBadge(
Icons.map,
field.region ??
'Wilayah tidak diketahui',
),
],
),
],
),
),
],
),
),
),
],
),
),
// Field information
_buildInfoSection('Informasi Umum', [
_buildInfoItem(
Icons.straighten,
'Luas Lahan',
field.areaSize != null
? '${field.areaSize} ${field.areaUnit}'
: 'Tidak diketahui',
field.region,
),
_buildInfoItem(
Icons.person,
'Kepemilikan',
field.ownershipType ?? 'Milik Sendiri',
field.region,
),
if (field.ownerName != null &&
field.ownerName!.isNotEmpty)
_buildInfoItem(
Icons.person_outline,
'Nama Pemilik',
field.ownerName!,
field.region,
),
]),
// Tampilkan peta jika ada koordinat
if (field.latitude != null &&
field.longitude != null) ...[
const SizedBox(height: 24),
_buildInfoSection('Lokasi Lahan', [
Container(
height: 200,
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.grey[300]!,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
clipBehavior: Clip.antiAlias,
child: Stack(
children: [
FlutterMap(
mapController: _detailMapController,
options: MapOptions(
center: LatLng(
field.latitude!,
field.longitude!,
),
zoom: 18.0,
interactiveFlags:
InteractiveFlag
.all, // Aktifkan semua gesture
),
children: [
TileLayer(
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName:
'com.tanismart.tugas_akhir_supabase',
),
MarkerLayer(
markers: [
Marker(
width: 40.0,
height: 40.0,
point: LatLng(
field.latitude!,
field.longitude!,
),
child: Icon(
Icons.location_on,
color: Colors.red,
size: 40,
),
),
],
),
],
),
FutureBuilder<String?>(
future: _getMapillaryThumbnail(
field.latitude!,
field.longitude!,
),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return Positioned(
bottom: 16,
left: 16,
child: Container(
width: 120,
height: 70,
color: Colors.grey[300],
child: const Center(
child:
CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
if (snapshot.hasData &&
snapshot.data != null) {
final thumbUrl = snapshot.data!;
return Positioned(
bottom: 16,
left: 16,
child: GestureDetector(
onTap: () async {
final url =
await _getMapillaryViewerUrl(
field.latitude!,
field.longitude!,
);
if (await canLaunch(url)) {
await launch(url);
}
},
child: ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
thumbUrl,
width: 120,
height: 70,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stack,
) => Container(
color:
Colors.grey[300],
width: 120,
height: 70,
child: Icon(
Icons
.image_not_supported,
),
),
),
),
),
);
}
return SizedBox.shrink();
},
),
],
),
),
if (field.location != null &&
field.location!.isNotEmpty)
_buildInfoItem(
Icons.location_on,
'Alamat',
field.location!,
field.region,
),
_buildInfoItem(
Icons.gps_fixed,
'Koordinat',
'${field.latitude!.toStringAsFixed(6)}, ${field.longitude!.toStringAsFixed(6)}',
field.region,
),
]),
],
// Region specific data
if (field.regionSpecificData != null) ...[
const SizedBox(height: 16),
_buildRegionSpecificSection(field),
],
// Action buttons
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_startEdit(field);
},
icon: const Icon(Icons.edit),
label: const Text('Edit Lahan'),
style: ElevatedButton.styleFrom(
backgroundColor: _getRegionColor(
field.region,
),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.pop(context);
_deleteField(field);
},
icon: const Icon(Icons.delete),
label: const Text('Hapus'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: AppColors.error,
padding: const EdgeInsets.symmetric(
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: AppColors.error,
),
),
elevation: 0,
),
),
),
],
),
],
),
),
],
),
),
),
);
}
Widget _buildDetailBadge(IconData icon, String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withOpacity(0.3), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: Colors.white),
const SizedBox(width: 6),
Text(
text,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
Widget _buildInfoSection(String title, List<Widget> items) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.only(bottom: 16),
child: Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
...items,
],
),
);
}
Widget _buildInfoItem(
IconData icon,
String label,
String value, [
String? region,
]) {
final Color iconColor =
region != null ? _getRegionColor(region) : AppColors.primary;
return Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[200]!),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 22, color: iconColor),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
),
],
),
),
],
),
);
}
Widget _buildRegionSpecificSection(Field field) {
final data = field.regionSpecificData!;
final List<Widget> items = [];
// Common data
if (data['topografi'] != null) {
items.add(
_buildInfoItem(
Icons.terrain,
'Topografi',
data['topografi'],
field.region,
),
);
}
if (data['kemiringan_lahan'] != null) {
items.add(
_buildInfoItem(
Icons.line_axis,
'Kemiringan Lahan',
data['kemiringan_lahan'],
field.region,
),
);
}
if (data['jenis_tanah'] != null) {
items.add(
_buildInfoItem(
Icons.layers,
'Jenis Tanah',
data['jenis_tanah'],
field.region,
),
);
}
if (data['sumber_air'] != null) {
items.add(
_buildInfoItem(
Icons.water,
'Sumber Air',
data['sumber_air'],
field.region,
),
);
}
if (data['jenis_irigasi'] != null) {
items.add(
_buildInfoItem(
Icons.water_drop,
'Jenis Irigasi',
data['jenis_irigasi'],
field.region,
),
);
}
if (data['tanaman_sebelumnya'] != null &&
data['tanaman_sebelumnya'].isNotEmpty) {
items.add(
_buildInfoItem(
Icons.history,
'Tanaman Sebelumnya',
data['tanaman_sebelumnya'],
field.region,
),
);
}
// Region specific data
switch (field.region) {
case 'Jawa':
if (data['sistem_petak'] != null) {
items.add(
_buildInfoItem(
Icons.grid_on,
'Sistem Petak',
data['sistem_petak'],
field.region,
),
);
}
break;
case 'Sumatera':
if (data['sistem_blok'] != null) {
items.add(
_buildInfoItem(
Icons.dashboard,
'Sistem Blok',
data['sistem_blok'],
field.region,
),
);
}
break;
case 'Kalimantan':
if (data['sistem_ladang'] != null) {
items.add(
_buildInfoItem(
Icons.agriculture,
'Sistem Ladang',
data['sistem_ladang'],
field.region,
),
);
}
if (data['jarak_sungai'] != null && data['jarak_sungai'].isNotEmpty) {
items.add(
_buildInfoItem(
Icons.waves,
'Jarak dari Sungai',
'${data['jarak_sungai']} meter',
field.region,
),
);
}
break;
case 'Sulawesi':
if (data['sistem_kebun'] != null) {
items.add(
_buildInfoItem(
Icons.eco,
'Sistem Kebun',
data['sistem_kebun'],
field.region,
),
);
}
if (data['kontur_lahan'] != null) {
items.add(
_buildInfoItem(
Icons.terrain,
'Kontur Lahan',
data['kontur_lahan'],
field.region,
),
);
}
break;
case 'Bali & Nusa Tenggara':
if (data['sistem_subak'] != null) {
items.add(
_buildInfoItem(
Icons.water_damage,
'Sistem Subak',
data['sistem_subak'],
field.region,
),
);
}
break;
case 'Maluku & Papua':
if (data['sistem_kebun'] != null) {
items.add(
_buildInfoItem(
Icons.forest,
'Sistem Kebun',
data['sistem_kebun'],
field.region,
),
);
}
if (data['tipe_hutan'] != null) {
items.add(
_buildInfoItem(
Icons.park,
'Tipe Hutan',
data['tipe_hutan'],
field.region,
),
);
}
break;
case 'Lainnya':
if (data['nama_wilayah'] != null) {
items.add(
_buildInfoItem(
Icons.location_city,
'Nama Wilayah',
data['nama_wilayah'],
field.region,
),
);
}
if (data['sistem_lahan'] != null) {
items.add(
_buildInfoItem(
Icons.agriculture,
'Sistem Lahan',
data['sistem_lahan'],
field.region,
),
);
}
if (data['fitur_lahan'] != null) {
items.add(
_buildInfoItem(
Icons.landscape,
'Fitur Lahan',
data['fitur_lahan'],
field.region,
),
);
}
break;
}
return _buildInfoSection('Karakteristik Lahan', items);
}
String _getRegionSpecificInfo(Field field) {
if (field.regionSpecificData == null) return '';
final data = field.regionSpecificData!;
final List<String> info = [];
// Tambahkan informasi umum
if (data['jenis_tanah'] != null) {
info.add('Tanah: ${data['jenis_tanah']}');
}
if (data['sumber_air'] != null) {
info.add('Air: ${data['sumber_air']}');
}
// Tambahkan informasi spesifik wilayah
switch (field.region) {
case 'Jawa':
if (data['sistem_petak'] != null) {
info.add('Sistem: ${data['sistem_petak']}');
}
break;
case 'Sumatera':
if (data['sistem_blok'] != null) {
info.add('Sistem: ${data['sistem_blok']}');
}
break;
case 'Kalimantan':
if (data['sistem_ladang'] != null) {
info.add('Sistem: ${data['sistem_ladang']}');
}
break;
case 'Sulawesi':
if (data['sistem_kebun'] != null) {
info.add('Sistem: ${data['sistem_kebun']}');
}
break;
case 'Bali & Nusa Tenggara':
if (data['sistem_subak'] != null) {
info.add('Sistem: ${data['sistem_subak']}');
}
break;
case 'Maluku & Papua':
if (data['sistem_kebun'] != null) {
info.add('Sistem: ${data['sistem_kebun']}');
}
break;
}
return info.join('');
}
void _showAddFieldBottomSheet() async {
final result = await showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (context) => AddFieldBottomSheet(onFieldAdded: _loadFields),
);
if (result == true) {
_loadFields();
}
}
void _showError(String message) {
// Dismiss any existing snackbars
ScaffoldMessenger.of(context).hideCurrentSnackBar();
// Show error in snackbar
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red,
duration: const Duration(seconds: 5),
action: SnackBarAction(
label: 'Refresh',
textColor: Colors.white,
onPressed: () {
_loadFields();
},
),
),
);
}
// Tambahkan metode untuk memilih lokasi
Future<void> _showLocationPicker() async {
final result = await showDialog<LocationResult>(
context: context,
builder:
(context) => LocationPickerDialog(
initialAddress: _locationController.text,
initialLatitude: _latitude,
initialLongitude: _longitude,
),
);
if (result != null) {
setState(() {
_locationController.text = result.address;
_latitude = result.latitude;
_longitude = result.longitude;
});
}
}
// Fungsi untuk membuka URL
Future<void> _launchUrl(String url) async {
final Uri uri = Uri.parse(url);
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
throw Exception('Tidak dapat membuka URL: $url');
}
}
Future<String?> _getMapillaryThumbnail(double lat, double lng) async {
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id,thumb_256_url&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
return data['data'][0]['thumb_256_url'] as String?;
}
}
return null;
}
Future<String> _getMapillaryViewerUrl(double lat, double lng) async {
// Cari id foto terdekat
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
final id = data['data'][0]['id'];
return 'https://www.mapillary.com/app/?pKey=$id&focus=photo';
}
}
// Fallback: buka mapillary di koordinat
return 'https://www.mapillary.com/app/?lat=$lat&lng=$lng&z=17.5';
}
}
// Kelas untuk membuat pattern visual pada background hero card
class PatternPainter extends CustomPainter {
final Color color;
PatternPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint =
Paint()
..color = color
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
final spacing = 15.0;
// Gambar pola garis diagonal
for (double i = 0; i < size.width + size.height; i += spacing) {
canvas.drawLine(
Offset(i < size.width ? i : 0, i < size.width ? 0 : i - size.width),
Offset(
i < size.height ? 0 : i - size.height,
i < size.height ? i : size.height,
),
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}