2686 lines
107 KiB
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': 'm²',
|
|
'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': 'm²',
|
|
'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': 'm²',
|
|
'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;
|
|
}
|