import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/core/theme/app_colors.dart'; import 'package:tugas_akhir_supabase/screens/community/models/farming_guide_model.dart'; import 'package:tugas_akhir_supabase/screens/community/utils/plant_categorizer.dart'; import 'package:tugas_akhir_supabase/screens/community/services/guide_service.dart'; import 'dart:io'; import 'package:image_picker/image_picker.dart'; import 'package:uuid/uuid.dart'; import 'package:path/path.dart' as path; import 'package:permission_handler/permission_handler.dart'; class GuideManagement extends StatefulWidget { const GuideManagement({super.key}); @override State createState() => _GuideManagementState(); } class _GuideManagementState extends State { final _supabase = Supabase.instance.client; final _guideService = GuideService(); bool _isLoading = true; List _guides = []; String _searchQuery = ''; // Detected storage bucket String _storageBucket = 'images'; // Controller untuk form tambah/edit final _titleController = TextEditingController(); final _contentController = TextEditingController(); final _categoryController = TextEditingController(); String? _selectedImagePath; String? _currentGuideId; bool _isFormLoading = false; // Modern Color Scheme static const Color primaryGreen = Color(0xFF0F6848); static const Color lightGreen = Color(0xFF4CAF50); static const Color surfaceGreen = Color(0xFFF1F8E9); static const Color cardWhite = Colors.white; static const Color textPrimary = Color(0xFF1B5E20); static const Color textSecondary = Color(0xFF757575); @override void initState() { super.initState(); _detectStorageBucket(); _loadGuides(); } @override void dispose() { _titleController.dispose(); _contentController.dispose(); _categoryController.dispose(); super.dispose(); } Future _detectStorageBucket() async { try { // Get bucket names final buckets = await _supabase.storage.listBuckets(); final bucketNames = buckets.map((b) => b.name).toList(); debugPrint('Available buckets: ${bucketNames.join(', ')}'); // Same logic as GuideService if (bucketNames.contains('images')) { _storageBucket = 'images'; } else if (bucketNames.contains('guide_images')) { _storageBucket = 'guide_images'; } else if (bucketNames.contains('avatars')) { _storageBucket = 'avatars'; } else if (bucketNames.isNotEmpty) { _storageBucket = bucketNames.first; } debugPrint('Selected bucket for guide images: $_storageBucket'); } catch (e) { debugPrint('Error detecting buckets: $e'); // Keep default bucket } } Future _loadGuides() async { setState(() => _isLoading = true); try { // Fetch all guides from the database final response = await _supabase .from('farming_guides') .select('*') .order('created_at', ascending: false); debugPrint('Guides loaded: ${response.length}'); // Convert to List final guides = List>.from( response, ).map((map) => FarmingGuideModel.fromMap(map)).toList(); if (mounted) { setState(() { _guides = guides; _isLoading = false; }); } } catch (e) { debugPrint('Error loading guides: $e'); if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: Unable to load guides. ${e.toString()}'), backgroundColor: Colors.red.shade400, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ); } } } void _filterGuides(String query) { setState(() { _searchQuery = query; if (query.isEmpty) { // No need to reload, just display all guides } else { // Filter locally for better performance // In a real app with many guides, you might want to do this on the server } }); } List get _filteredGuides { if (_searchQuery.isEmpty) { return _guides; } final query = _searchQuery.toLowerCase(); return _guides.where((guide) { final title = guide.title.toLowerCase(); final category = guide.category.toLowerCase(); final content = guide.content.toLowerCase(); return title.contains(query) || category.contains(query) || content.contains(query); }).toList(); } Future _pickImage() async { try { debugPrint('Memulai proses pemilihan gambar...'); // Pendekatan yang lebih sederhana tanpa permission handler final ImagePicker picker = ImagePicker(); final XFile? image = await picker.pickImage( source: ImageSource.gallery, // Tanpa pembatasan ukuran ); if (image != null) { debugPrint('Gambar dipilih: ${image.path}'); if (mounted) { setState(() { _selectedImagePath = image.path; }); } } else { debugPrint('Pemilihan gambar dibatalkan'); } } catch (e) { debugPrint('Error saat memilih gambar: $e'); // Jangan tampilkan error ke user untuk menghindari crash lanjutan } } Future _uploadImage() async { if (_selectedImagePath == null) return null; try { debugPrint('Mulai upload gambar sederhana dari $_selectedImagePath'); final file = File(_selectedImagePath!); if (!await file.exists()) { debugPrint('File tidak ditemukan: $_selectedImagePath'); return null; } final fileExtension = path.extension(file.path).toLowerCase(); final fileName = 'guide_${DateTime.now().millisecondsSinceEpoch}$fileExtension'; debugPrint( 'Mencoba upload dengan nama file: $fileName ke bucket $_storageBucket', ); // Baca file sebagai bytes untuk menghindari masalah path final bytes = await file.readAsBytes(); // Upload sebagai binary ke bucket yang terdeteksi await _supabase.storage .from(_storageBucket) .uploadBinary( fileName, bytes, fileOptions: const FileOptions(contentType: 'image/jpeg'), ); // Get public URL - Gunakan metode yang benar untuk mendapatkan URL publik final imageUrl = _supabase.storage .from(_storageBucket) .getPublicUrl(fileName); debugPrint('Gambar berhasil diupload: $imageUrl'); // Tambahkan pengecekan URL if (!imageUrl.startsWith('http')) { debugPrint('Warning: URL tidak dimulai dengan http: $imageUrl'); } return imageUrl; } catch (e) { debugPrint('Error uploading image: $e'); // Jangan tampilkan error ke user untuk menghindari crash return null; } } void _showAddEditGuideDialog({FarmingGuideModel? guide}) { // Reset form or fill with guide data if editing _currentGuideId = guide?.id; _titleController.text = guide?.title ?? ''; _contentController.text = guide?.content ?? ''; _categoryController.text = guide?.category ?? ''; _selectedImagePath = null; // Reset selected image // Pilihan kategori untuk dropdown final List categoryOptions = [ PlantCategorizer.TANAMAN_PANGAN, PlantCategorizer.SAYURAN, PlantCategorizer.BUAH_BUAHAN, PlantCategorizer.REMPAH, 'Kalender Tanam', PlantCategorizer.UMUM, ]; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => AlertDialog( title: Text( guide == null ? 'Tambah Panduan Baru' : 'Edit Panduan', ), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Judul', border: OutlineInputBorder(), ), onChanged: (value) { // Auto-kategorisasi berdasarkan judul if (_categoryController.text.isEmpty || _categoryController.text == PlantCategorizer.UMUM) { final suggestedCategory = PlantCategorizer.categorize( value, description: _contentController.text, ); if (suggestedCategory != PlantCategorizer.UMUM) { setDialogState(() { _categoryController.text = suggestedCategory; }); } } }, ), const SizedBox(height: 16), // Kategori dropdown daripada text field DropdownButtonFormField( value: categoryOptions.contains(_categoryController.text) ? _categoryController.text : PlantCategorizer.UMUM, decoration: const InputDecoration( labelText: 'Kategori', border: OutlineInputBorder(), ), items: categoryOptions.map((String category) { return DropdownMenuItem( value: category, child: Text(category), ); }).toList(), onChanged: (String? newValue) { if (newValue != null) { setDialogState(() { _categoryController.text = newValue; }); } }, ), const SizedBox(height: 16), TextField( controller: _contentController, maxLines: 8, decoration: const InputDecoration( labelText: 'Konten Panduan', border: OutlineInputBorder(), alignLabelWithHint: true, ), onChanged: (value) { // Auto-kategorisasi berdasarkan konten if (_categoryController.text.isEmpty || _categoryController.text == PlantCategorizer.UMUM) { final suggestedCategory = PlantCategorizer.categorize( _titleController.text, description: value, ); if (suggestedCategory != PlantCategorizer.UMUM) { setDialogState(() { _categoryController.text = suggestedCategory; }); } } }, ), const SizedBox(height: 16), Row( children: [ Expanded( child: Text( _selectedImagePath != null ? 'Gambar dipilih: ${path.basename(_selectedImagePath!)}' : guide?.imageUrl != null ? 'Gambar saat ini akan dipertahankan' : 'Belum ada gambar dipilih', style: TextStyle(color: Colors.grey[600]), ), ), TextButton.icon( onPressed: () async { await _pickImage(); setDialogState(() {}); // Update dialog state }, icon: const Icon(Icons.image), label: const Text('Pilih Gambar'), ), ], ), if (_selectedImagePath != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Container( height: 100, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(8), ), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image, color: Colors.grey[700]), const SizedBox(width: 8), Flexible( child: Text( 'Gambar dipilih: ${path.basename(_selectedImagePath!)}', style: TextStyle( color: Colors.grey[700], ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ), if (guide?.imageUrl != null && _selectedImagePath == null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Container( height: 100, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(8), ), child: Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.image, color: Colors.grey[700]), const SizedBox(width: 8), const Flexible( child: Text( 'Gambar sudah tersimpan', style: TextStyle(color: Colors.grey), overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ), if (_isFormLoading) const Padding( padding: EdgeInsets.only(top: 16.0), child: Center(child: CircularProgressIndicator()), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Batal'), ), ElevatedButton( onPressed: _isFormLoading ? null : () async { // Validate form if (_titleController.text.trim().isEmpty || _contentController.text.trim().isEmpty || _categoryController.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Semua field harus diisi'), backgroundColor: Colors.red, ), ); return; } setDialogState(() => _isFormLoading = true); try { // Upload image if selected String? imageUrl; if (_selectedImagePath != null) { imageUrl = await _uploadImage(); // Jika upload gagal, lanjutkan tanpa gambar if (imageUrl == null) { debugPrint( 'Upload gambar gagal, melanjutkan tanpa gambar', ); } else { debugPrint( 'Image URL setelah upload: $imageUrl', ); } } // Persiapkan data guide final guideData = { 'title': _titleController.text.trim(), 'content': _contentController.text.trim(), 'category': _categoryController.text.trim(), }; // Tambahkan image_url jika ada gambar baru yang berhasil diupload if (imageUrl != null) { guideData['image_url'] = imageUrl; debugPrint( 'Menambahkan image_url ke data: $imageUrl', ); } else if (guide != null && guide.imageUrl != null) { // Pertahankan image_url yang sudah ada jika tidak ada gambar baru guideData['image_url'] = guide.imageUrl!; debugPrint( 'Mempertahankan image_url yang ada: ${guide.imageUrl}', ); } if (_currentGuideId != null) { // Update existing guide await _supabase .from('farming_guides') .update(guideData) .eq('id', _currentGuideId!); debugPrint( 'Berhasil update guide dengan ID: $_currentGuideId', ); } else { // Add new guide final response = await _supabase .from('farming_guides') .insert(guideData) .select('id'); if (response.isNotEmpty) { debugPrint( 'Berhasil insert guide dengan ID: ${response[0]['id']}', ); } } // Reload guides await _loadGuides(); if (mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( _currentGuideId != null ? 'Panduan berhasil diperbarui' : 'Panduan baru berhasil ditambahkan', ), backgroundColor: Colors.green, ), ); } } catch (e) { debugPrint('Error saving guide: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setDialogState( () => _isFormLoading = false, ); } } }, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, ), child: Text( _currentGuideId != null ? 'Perbarui' : 'Simpan', ), ), ], ), ), ); } Future _deleteGuide(String id) async { // Show confirmation dialog final shouldDelete = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Konfirmasi Hapus'), content: const Text( 'Apakah Anda yakin ingin menghapus panduan ini? ' 'Tindakan ini tidak dapat dibatalkan.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Batal'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, ), child: const Text('Hapus'), ), ], ), ); if (shouldDelete != true) return; setState(() => _isLoading = true); try { // Delete guide from database await _supabase.from('farming_guides').delete().eq('id', id); // Reload guides await _loadGuides(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Panduan berhasil dihapus'), backgroundColor: Colors.green, ), ); } } catch (e) { debugPrint('Error deleting guide: $e'); if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } @override Widget build(BuildContext context) { return Scaffold( body: Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Expanded( child: Text( 'Manajemen Panduan Pertanian', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: primaryGreen, ), ), ), ElevatedButton.icon( onPressed: () => _showAddEditGuideDialog(), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), ), icon: const Icon(Icons.add), label: const Text('Tambah Panduan'), ), ], ), const SizedBox(height: 16), TextField( onChanged: _filterGuides, decoration: InputDecoration( hintText: 'Cari panduan...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(vertical: 12), ), ), const SizedBox(height: 16), Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _filteredGuides.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.menu_book, size: 64, color: Colors.grey[400], ), const SizedBox(height: 16), Text( _searchQuery.isEmpty ? 'Belum ada panduan' : 'Tidak ada panduan yang sesuai', style: TextStyle( fontSize: 18, color: Colors.grey[600], ), ), if (_searchQuery.isNotEmpty) TextButton( onPressed: () { setState(() => _searchQuery = ''); }, child: const Text('Tampilkan semua panduan'), ), ], ), ) : ListView.builder( itemCount: _filteredGuides.length, itemBuilder: (context, index) { final guide = _filteredGuides[index]; return Card( margin: const EdgeInsets.only(bottom: 16), elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (guide.imageUrl != null) Builder( builder: (context) { // Perbaiki URL gambar menggunakan GuideService final imageUrl = _guideService.fixImageUrl( guide.imageUrl, ) ?? ''; debugPrint('Guide image URL: $imageUrl'); return Container( height: 150, width: double.infinity, decoration: const BoxDecoration( borderRadius: BorderRadius.vertical( top: Radius.circular(8), ), ), child: ClipRRect( borderRadius: const BorderRadius.vertical( top: Radius.circular(8), ), child: imageUrl.isEmpty ? Container( color: Colors.grey[200], child: const Center( child: Row( mainAxisAlignment: MainAxisAlignment .center, children: [ Icon( Icons .image_not_supported, ), SizedBox(width: 8), Text( 'URL gambar tidak valid', ), ], ), ), ) : Image.network( imageUrl, fit: BoxFit.cover, errorBuilder: ( context, error, stackTrace, ) { // Log error untuk debugging debugPrint( 'Error loading image: $error for URL $imageUrl', ); return Container( color: Colors.grey[200], child: const Center( child: Row( mainAxisAlignment: MainAxisAlignment .center, children: [ Icon( Icons .error_outline, ), SizedBox( width: 8, ), Text( 'Gagal memuat gambar', ), ], ), ), ); }, loadingBuilder: ( context, child, loadingProgress, ) { if (loadingProgress == null) return child; return Container( color: Colors.grey[200], child: Center( child: CircularProgressIndicator( value: loadingProgress .expectedTotalBytes != null ? loadingProgress .cumulativeBytesLoaded / loadingProgress .expectedTotalBytes! : null, ), ), ); }, ), ), ); }, ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( guide.title, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: surfaceGreen, borderRadius: BorderRadius.circular(4), ), child: Text( guide.category, style: const TextStyle( color: primaryGreen, fontWeight: FontWeight.w500, ), ), ), ], ), const SizedBox(height: 8), Text( guide.content, maxLines: 3, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.grey[600], ), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Dibuat: ${guide.getFormattedDate()}', style: TextStyle( fontSize: 12, color: Colors.grey[500], ), ), Row( children: [ IconButton( onPressed: () => _showAddEditGuideDialog( guide: guide, ), icon: const Icon( Icons.edit, color: primaryGreen, ), tooltip: 'Edit', ), IconButton( onPressed: () => _deleteGuide(guide.id), icon: const Icon( Icons.delete, color: Colors.red, ), tooltip: 'Hapus', ), ], ), ], ), ], ), ), ], ), ); }, ), ), ], ), ), ); } }