import 'package:flutter/material.dart'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/screens/calendar/calendar_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/schedule_list_screen.dart'; import 'package:tugas_akhir_supabase/screens/community/community_screen.dart'; import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart'; import 'package:intl/intl.dart'; import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/schedule_detail_screen.dart'; import 'package:tugas_akhir_supabase/utils/app_events.dart'; import 'dart:async'; import 'dart:io'; class HomeContent extends StatefulWidget { final String userId; const HomeContent({Key? key, required this.userId}) : super(key: key); @override State createState() => _HomeContentState(); } class _HomeContentState extends State { // Data for dynamic content List> _scheduleData = []; List> _analysisData = []; bool _isLoadingSchedules = true; bool _isLoadingAnalysis = true; // Stream subscription untuk AppEventBus StreamSubscription? _scheduleUpdatedSubscription; @override void initState() { super.initState(); _fetchRecentAnalysis(); _fetchSchedules(); // Dengarkan event jadwal diperbarui _scheduleUpdatedSubscription = AppEventBus().onScheduleUpdated.listen((event) { debugPrint('INFO: HomeContent menerima event jadwal diperbarui'); // Refresh data jadwal dan analisis _fetchSchedules(); _fetchRecentAnalysis(); // Tampilkan snackbar jika tidak sedang dalam proses refresh if (!_isLoadingSchedules && !_isLoadingAnalysis && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Data jadwal berhasil diperbarui'), duration: Duration(seconds: 2), backgroundColor: Color(0xFF056839), ), ); } }); } @override void dispose() { // Batalkan subscription saat widget dihapus _scheduleUpdatedSubscription?.cancel(); super.dispose(); } Future _fetchRecentAnalysis() async { setState(() => _isLoadingAnalysis = true); try { final user = Supabase.instance.client.auth.currentUser; if (user == null) { setState(() => _isLoadingAnalysis = false); return; } // Tambahkan timeout untuk mencegah permintaan menggantung final completer = Completer>(); // Set timeout untuk mencegah app hanging Future.delayed(const Duration(seconds: 10), () { if (!completer.isCompleted) { completer.completeError(TimeoutException('Koneksi timeout saat memuat aktivitas.')); } }); // Get the latest daily logs Supabase.instance.client .from('daily_logs') .select('*, crop_schedules!inner(id, crop_name, field_id, user_id)') .eq('crop_schedules.user_id', user.id) .order('date', ascending: false) .limit(5) .then((value) { if (!completer.isCompleted) completer.complete(value); }) .catchError((error) { if (!completer.isCompleted) completer.completeError(error); }); final response = await completer.future; debugPrint('Daily logs response: $response'); if (response is List && response.isNotEmpty) { if (mounted) { setState(() { _analysisData = response.map((item) { final cropName = item['crop_schedules']['crop_name'] ?? 'Tanaman'; final fieldId = item['crop_schedules']['field_id'] ?? 'Lahan'; final cost = item['cost'] ?? 0; final note = item['note'] ?? 'Aktivitas pertanian'; final date = item['date']; // Store the date for navigation // Clean up the location display - remove UUIDs String location = cropName; if (fieldId != null && !fieldId.contains('-')) { location = '$cropName - $fieldId'; } // Determine icon based on note content IconData icon = Icons.calendar_today; Color iconColor = const Color(0xFF00897B); Color iconBgColor = const Color(0xFFE0F2F1); Color tagColor = const Color(0xFFE0F2F1); Color tagTextColor = const Color(0xFF00897B); if (note.toLowerCase().contains('panen')) { icon = Icons.agriculture; iconColor = Colors.orange[700]!; iconBgColor = const Color(0xFFFFF3E0); tagColor = Colors.orange[100]!; tagTextColor = Colors.orange[800]!; } else if (note.toLowerCase().contains('hama') || note.toLowerCase().contains('penyakit')) { icon = Icons.bug_report; iconColor = Colors.red[700]!; iconBgColor = Colors.red[50]!; tagColor = Colors.red[100]!; tagTextColor = Colors.red[700]!; } else if (note.toLowerCase().contains('pupuk')) { icon = Icons.eco; iconColor = Colors.green[700]!; iconBgColor = Colors.green[50]!; tagColor = Colors.green[100]!; tagTextColor = Colors.green[700]!; } else if (note.toLowerCase().contains('air') || note.toLowerCase().contains('irigasi')) { icon = Icons.water_drop; iconColor = Colors.blue[700]!; iconBgColor = Colors.blue[50]!; tagColor = Colors.blue[100]!; tagTextColor = Colors.blue[700]!; } return { 'title': note, 'location': location, 'cost': 'Biaya: Rp ${NumberFormat('#,###', 'id_ID').format(cost)}', 'tag': cropName, 'tagColor': tagColor, 'tagTextColor': tagTextColor, 'icon': icon, 'iconBgColor': iconBgColor, 'iconColor': iconColor, 'crop_schedules': item['crop_schedules'], // Store the entire crop_schedules object 'date': date, // Store the date for navigation }; }).toList(); _isLoadingAnalysis = false; }); } } else { if (mounted) { setState(() => _isLoadingAnalysis = false); } } } on TimeoutException catch (e) { debugPrint('Timeout fetching analysis data: $e'); if (mounted) { setState(() => _isLoadingAnalysis = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Gagal memuat aktivitas: Koneksi timeout'), backgroundColor: Colors.orange, ), ); } } catch (e) { debugPrint('Error fetching analysis data: $e'); if (mounted) { setState(() => _isLoadingAnalysis = false); // Tampilkan pesan error yang lebih informatif String errorMessage = 'Terjadi kesalahan'; if (e.toString().contains('not found') || e.toString().contains('not exist')) { errorMessage = 'Data tidak ditemukan'; } else if (e.toString().contains('permission') || e.toString().contains('access')) { errorMessage = 'Tidak memiliki akses'; } else if (e.toString().contains('network') || e.toString().contains('connection')) { errorMessage = 'Masalah koneksi jaringan'; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal memuat aktivitas: $errorMessage'), backgroundColor: Colors.red, ), ); } } } Future _fetchSchedules() async { setState(() => _isLoadingSchedules = true); try { final user = Supabase.instance.client.auth.currentUser; if (user == null) { setState(() => _isLoadingSchedules = false); return; } // Tambahkan timeout untuk mencegah permintaan menggantung final completer = Completer>(); // Set timeout untuk mencegah app hanging Future.delayed(const Duration(seconds: 10), () { if (!completer.isCompleted) { completer.completeError(TimeoutException('Koneksi timeout saat memuat jadwal.')); } }); // Get active schedules final now = DateTime.now(); Supabase.instance.client .from('crop_schedules') .select('id, crop_name, field_id, start_date, end_date') .eq('user_id', user.id) .or('end_date.gte.${now.toIso8601String()}') .order('start_date', ascending: true) .limit(3) .then((value) { if (!completer.isCompleted) completer.complete(value); }) .catchError((error) { if (!completer.isCompleted) completer.completeError(error); }); final response = await completer.future; debugPrint('Schedules response: $response'); if (response is List && response.isNotEmpty) { if (mounted) { setState(() { _scheduleData = response.map((item) { final cropName = item['crop_name'] ?? 'Tanaman'; final fieldId = item['field_id'] ?? 'Lahan'; final startDate = DateTime.tryParse(item['start_date']) ?? DateTime.now(); final endDate = DateTime.tryParse(item['end_date']) ?? DateTime.now().add(const Duration(days: 90)); // Format dates final startFormatted = DateFormat('dd/MM', 'id_ID').format(startDate); final endFormatted = DateFormat('dd/MM', 'id_ID').format(endDate); // Determine status and colors String status = 'Belum mulai'; Color statusColor = Colors.orange[100]!; Color statusTextColor = Colors.orange[700]!; IconData icon = Icons.eco; Color iconColor = Colors.green[700]!; if (now.isAfter(startDate)) { if (now.isBefore(endDate)) { status = 'Berlangsung'; statusColor = Colors.green[100]!; statusTextColor = Colors.green[700]!; } else { status = 'Selesai'; statusColor = Colors.grey[300]!; statusTextColor = Colors.grey[700]!; } } // Set icon based on crop name if (cropName.toLowerCase().contains('cabai') || cropName.toLowerCase().contains('cabe')) { icon = Icons.local_fire_department; iconColor = Colors.red[700]!; } else if (cropName.toLowerCase().contains('padi')) { icon = Icons.grass; iconColor = Colors.green[700]!; } else if (cropName.toLowerCase().contains('jagung')) { icon = Icons.grass; iconColor = Colors.amber[700]!; } return { 'id': item['id'], 'cropName': cropName, 'location': fieldId, 'period': '$startFormatted - $endFormatted', 'status': status, 'statusColor': statusColor, 'statusTextColor': statusTextColor, 'icon': icon, 'iconColor': iconColor, 'start_date': item['start_date'], 'end_date': item['end_date'], }; }).toList(); _isLoadingSchedules = false; }); } } else { if (mounted) { setState(() => _isLoadingSchedules = false); } } } on TimeoutException catch (e) { debugPrint('Timeout fetching schedules: $e'); if (mounted) { setState(() => _isLoadingSchedules = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Gagal memuat jadwal: Koneksi timeout'), backgroundColor: Colors.orange, ), ); } } catch (e) { debugPrint('Error fetching schedules: $e'); if (mounted) { setState(() => _isLoadingSchedules = false); // Tampilkan pesan error yang lebih informatif String errorMessage = 'Terjadi kesalahan'; if (e.toString().contains('not found') || e.toString().contains('not exist')) { errorMessage = 'Data tidak ditemukan'; } else if (e.toString().contains('permission') || e.toString().contains('access')) { errorMessage = 'Tidak memiliki akses'; } else if (e.toString().contains('network') || e.toString().contains('connection')) { errorMessage = 'Masalah koneksi jaringan'; } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal memuat jadwal: $errorMessage'), backgroundColor: Colors.red, ), ); } } } @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () async { await _fetchSchedules(); await _fetchRecentAnalysis(); }, color: const Color(0xFF056839), child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), child: SafeArea( bottom: true, // Pastikan konten tidak tertutup oleh notch atau navigation bar child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildWelcomeCard(), const SizedBox(height: 16), _buildQuickActions(), const SizedBox(height: 20), _buildTipsSection(), const SizedBox(height: 20), _buildMainServicesSection(), const SizedBox(height: 20), _buildAnalysisSection(), const SizedBox(height: 20), _buildScheduleSection(), const SizedBox(height: 20), ], ), ), ), ); } Widget _buildWelcomeCard() { final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: const BoxDecoration( color: Color(0xFF056839), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: Text( 'TaniSMART', style: GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Tingkatkan Produktivitas', style: GoogleFonts.poppins( fontSize: isSmallScreen ? 18 : 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'Pertanian Anda', style: GoogleFonts.poppins( fontSize: isSmallScreen ? 18 : 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), const SizedBox(height: 6), Text( 'Solusi Pertanian Cerdas!', style: GoogleFonts.poppins( fontSize: isSmallScreen ? 12 : 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), Container( width: 38, height: 38, decoration: const BoxDecoration( color: Color(0xFFFFB74D), shape: BoxShape.circle, ), child: const Icon( Icons.wb_sunny, color: Colors.white, size: 20, ), ), ], ), ], ), ); } Widget _buildQuickActions() { final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildActionItem(Icons.eco, 'Tanaman', const Color(0xFF4CAF50), isSmallScreen), _buildActionItem(Icons.water_drop, 'Irigasi', const Color(0xFF2196F3), isSmallScreen), _buildActionItem(Icons.bug_report, 'Hama', const Color(0xFFFF5252), isSmallScreen), _buildActionItem(Icons.eco, 'Pupuk', const Color(0xFF4CAF50), isSmallScreen), ], ), ); } Widget _buildActionItem(IconData icon, String label, Color color, bool isSmallScreen) { final iconSize = isSmallScreen ? 48.0 : 60.0; final fontSize = isSmallScreen ? 11.0 : 13.0; return Expanded( child: Column( children: [ Container( height: iconSize, width: iconSize, decoration: BoxDecoration( color: color.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(icon, color: color, size: iconSize * 0.45), ), const SizedBox(height: 8), Text( label, style: GoogleFonts.poppins( fontSize: fontSize, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ); } Widget _buildTipsSection() { // Menggunakan MediaQuery untuk mendapatkan ukuran layar final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; // Deteksi layar kecil return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'Tips & Trik Bertani', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 12), SizedBox( // Tinggi yang adaptif berdasarkan ukuran layar height: isSmallScreen ? 170 : 155, child: ListView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16), children: [ _buildTipCard( 'Waktu Tanam Optimal', 'Menanam padi di awal musim hujan meningkatkan hasil panen hingga 30%', const Color(0xFFF9A825), Icons.wb_sunny, ), const SizedBox(width: 12), _buildTipCard( 'Rotasi Tanaman', 'Bergantian menanam padi dan kedelai menjaga kesehatan tanah & nutrisi', const Color(0xFF4CAF50), Icons.sync, ), const SizedBox(width: 12), _buildTipCard( 'Penggunaan Pupuk', 'Gunakan pupuk organik untuk menjaga kualitas tanah jangka panjang', const Color(0xFF7CB342), Icons.eco, ), const SizedBox(width: 12), _buildTipCard( 'Pengendalian Hama', 'Tanam tanaman pendamping seperti kemangi untuk mengusir hama alami', const Color.fromARGB(255, 193, 87, 0), Icons.bug_report, ), ], ), ), ], ); } Widget _buildTipCard(String title, String description, Color color, IconData icon) { return AnimatedTipCard( title: title, description: description, color: color, icon: icon, ); } Widget _buildMainServicesSection() { // Deteksi ukuran layar untuk responsivitas final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( 'Layanan Utama', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: GridView.count( crossAxisCount: 2, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), mainAxisSpacing: isSmallScreen ? 12 : 16, crossAxisSpacing: isSmallScreen ? 12 : 16, childAspectRatio: isSmallScreen ? 0.95 : 1.0, // Sedikit lebih tinggi pada layar kecil children: [ _buildServiceCardCompact( 'Scan Penyakit', 'assets/icons/scanner_icon.png', const Color(0xFFFFF2F2), const Color(0xFFFF9494), Icons.document_scanner_rounded, () { Navigator.push( context, MaterialPageRoute(builder: (_) => const PlantScannerScreen()), ); }, ), _buildServiceCardCompact( 'Analisis Panen', 'assets/icons/analysis_icon.png', const Color(0xFFEBFFFD), const Color(0xFF71DECE), Icons.insights_rounded, () { Navigator.push( context, MaterialPageRoute( builder: (_) => AnalisisInputScreen(userId: widget.userId, scheduleData: null), ), ); }, ), _buildServiceCardCompact( 'Kalender Tanam', 'assets/icons/calendar_icon.png', const Color(0xFFF2F8FF), const Color(0xFF5CA0FF), Icons.calendar_today_rounded, () { Navigator.push( context, MaterialPageRoute(builder: (_) => const KalenderTanamScreen()), ); }, ), _buildServiceCardCompact( 'Komunitas', 'assets/icons/community_icon.png', const Color(0xFFFFF8E8), const Color(0xFFFFBD59), Icons.forum_rounded, () { Navigator.push( context, MaterialPageRoute(builder: (_) => const CommunityScreen()), ); }, ), ], ), ), ], ); } Widget _buildServiceCardCompact( String title, String iconAssetPath, Color bgColor, Color iconColor, IconData fallbackIcon, VoidCallback onTap, ) { return AnimatedServiceCard( bgColor: bgColor, iconColor: iconColor, fallbackIcon: fallbackIcon, title: title, onTap: onTap, ); } Widget _buildAnalysisSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Analisis Terbaru', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), ], ), ), const SizedBox(height: 12), _isLoadingAnalysis ? const Center( child: Padding( padding: EdgeInsets.all(20.0), child: CircularProgressIndicator(color: Color(0xFF056839)), ), ) : _analysisData.isEmpty ? _buildEmptyState( 'Belum ada aktivitas', 'Catat aktivitas pertanian Anda untuk melihatnya di sini', Icons.calendar_today, ) : Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: _buildAnalysisItems(), ), ), ], ); } List _buildAnalysisItems() { try { // Hanya tampilkan 3 item teratas final itemsToShow = _analysisData.take(3).toList(); return itemsToShow.map((item) { // Pastikan data valid dengan nilai default final title = item['title'] ?? 'Aktivitas'; final location = item['location'] ?? 'Lokasi tidak tersedia'; final cost = item['cost'] ?? 'Biaya: Rp 0'; final tag = item['tag'] ?? 'Tag'; // Periksa apakah crop_schedules ada dan valid final hasValidSchedule = item['crop_schedules'] != null && item['crop_schedules'] is Map && item['crop_schedules']['id'] != null; // Buat handler untuk navigasi yang aman VoidCallback? onTapHandler; if (hasValidSchedule) { final scheduleId = item['crop_schedules']['id']; onTapHandler = () { try { Navigator.push( context, MaterialPageRoute( builder: (_) => ScheduleDetailScreen(scheduleId: scheduleId), ), ); } catch (e) { debugPrint('Error navigating to schedule detail: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Gagal membuka detail jadwal'), backgroundColor: Colors.red, ), ); } }; } return _buildCompactAnalysisItem( title, location, cost, tag, onTapHandler, ); }).toList(); } catch (e) { debugPrint('Error building analysis items: $e'); // Tampilkan item dummy jika terjadi error return [ _buildCompactAnalysisItem( 'panen', 'Cabai - lahan cabaii', 'Biaya: Rp 100.000', 'Cabai', null, ), ]; } } Widget _buildCompactAnalysisItem( String title, String location, String cost, String tag, VoidCallback? onTap, ) { return GestureDetector( onTap: onTap, child: Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(10), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 70, height: 70, decoration: BoxDecoration( color: const Color(0xFFE0F2F1), borderRadius: BorderRadius.circular(8), ), child: const Center( child: Icon( Icons.calendar_today, color: Color(0xFF009688), size: 18, ), ), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( location, style: GoogleFonts.poppins( fontSize: 12, color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( cost, style: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w500, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(20), ), child: Text( tag, style: GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.w500, color: const Color(0xFFF57C00), ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ), ), ); } Widget _buildScheduleSection() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Jadwal Anda', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87, ), ), TextButton.icon( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const ScheduleListScreen()), ); }, icon: Text( 'Lihat Semua', style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w500, color: const Color(0xFF056839), ), ), label: const Icon( Icons.arrow_forward, color: Color(0xFF056839), size: 14, ), style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), ], ), ), const SizedBox(height: 12), _isLoadingSchedules ? const Center( child: Padding( padding: EdgeInsets.all(20.0), child: CircularProgressIndicator(color: Color(0xFF056839)), ), ) : _scheduleData.isEmpty ? _buildEmptyState( 'Belum ada jadwal tanam', 'Tambahkan jadwal tanam untuk melihatnya di sini', Icons.calendar_today, ) : _buildScheduleHorizontalList(), ], ); } Widget _buildScheduleHorizontalList() { return SizedBox( height: 142, child: ListView.builder( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: _scheduleData.isEmpty ? 3 : _scheduleData.length, itemBuilder: (context, index) { // Jika tidak ada data, gunakan dummy data if (_scheduleData.isEmpty) { return _buildCompactScheduleCard(index); } // Jika ada data, gunakan data asli final schedule = _scheduleData[index]; final scheduleId = schedule['id']; return _buildCompactScheduleCard( index, scheduleId: scheduleId, onTap: () { try { Navigator.push( context, MaterialPageRoute( builder: (_) => ScheduleDetailScreen(scheduleId: scheduleId), ), ); } catch (e) { debugPrint('Error navigating to schedule detail: $e'); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Gagal membuka detail jadwal'), backgroundColor: Colors.red, ), ); } } ); }, ), ); } Widget _buildCompactScheduleCard(int index, {String? scheduleId, VoidCallback? onTap}) { // Data dummy hanya digunakan jika tidak ada data asli final dummyItems = [ { 'crop': 'Jagung', 'period': '03/05 - 08/09', 'status': '0%', 'statusColor': Colors.amber[100]!, 'statusTextColor': Colors.amber[800]!, }, { 'crop': 'Cabai', 'period': '10/06 - 08/09', 'status': 'Berlangsung', 'statusColor': Colors.green[100]!, 'statusTextColor': Colors.green[800]!, }, { 'crop': 'Cabai', 'period': '19/06 - 17/09', 'status': '0%', 'statusColor': Colors.grey[200]!, 'statusTextColor': Colors.grey[700]!, }, ]; // Variabel untuk menyimpan data yang akan ditampilkan String cropName = 'Tanaman'; String period = ''; String status = 'Belum mulai'; Color statusColor = Colors.grey[200]!; Color statusTextColor = Colors.grey[700]!; // Gunakan data asli jika tersedia if (_scheduleData.isNotEmpty && index < _scheduleData.length) { final schedule = _scheduleData[index]; // Ambil nama tanaman cropName = schedule['crop_name'] ?? schedule['cropName'] ?? 'Tanaman'; // Format tanggal dari data asli final startDate = DateTime.tryParse(schedule['start_date']) ?? DateTime.now(); final endDate = DateTime.tryParse(schedule['end_date']) ?? DateTime.now().add(const Duration(days: 90)); final startFormatted = DateFormat('dd/MM', 'id_ID').format(startDate); final endFormatted = DateFormat('dd/MM', 'id_ID').format(endDate); period = '$startFormatted - $endFormatted'; // Tentukan status berdasarkan tanggal final now = DateTime.now(); if (now.isAfter(startDate)) { if (now.isBefore(endDate)) { status = 'Berlangsung'; statusColor = Colors.green[100]!; statusTextColor = Colors.green[700]!; } else { status = 'Selesai'; statusColor = Colors.grey[300]!; statusTextColor = Colors.grey[700]!; } } else { status = 'Belum mulai'; statusColor = Colors.orange[100]!; statusTextColor = Colors.orange[700]!; } } else { // Gunakan data dummy jika tidak ada data asli final dummyItem = dummyItems[index % dummyItems.length]; cropName = dummyItem['crop'] as String; period = dummyItem['period'] as String; status = dummyItem['status'] as String; statusColor = dummyItem['statusColor'] as Color; statusTextColor = dummyItem['statusTextColor'] as Color; } return GestureDetector( onTap: onTap, child: Container( width: 170, margin: const EdgeInsets.only(right: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), child: Row( children: [ Container( width: 30, height: 20, decoration: BoxDecoration( color: cropName.toLowerCase().contains('cabai') ? Colors.red.shade50 : Colors.amber.shade50, shape: BoxShape.circle, ), child: Center( child: Icon( cropName.toLowerCase().contains('cabai') ? Icons.local_fire_department : Icons.grass, color: cropName.toLowerCase().contains('cabai') ? Colors.red : Colors.amber, size: 10, ), ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: statusColor, borderRadius: BorderRadius.circular(20), ), child: Text( status, style: GoogleFonts.poppins( fontSize: 9, fontWeight: FontWeight.w500, color: statusTextColor, ), ), ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( cropName, style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), const Divider(thickness: 1, height: 6), Row( children: [ Icon( Icons.calendar_today, size: 12, color: Colors.grey[600], ), const SizedBox(width: 4), Text( period, style: GoogleFonts.poppins( fontSize: 11, fontWeight: FontWeight.w500, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ], ), ), ], ), ), ); } Widget _buildEmptyState(String title, String subtitle, IconData icon) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[200]!), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, size: 40, color: Colors.grey[400], ), const SizedBox(height: 16), Text( title, style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[700], ), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( subtitle, style: GoogleFonts.poppins( fontSize: 13, color: Colors.grey[600], ), textAlign: TextAlign.center, ), ], ), ); } } class AnimatedServiceCard extends StatefulWidget { final Color bgColor; final Color iconColor; final IconData fallbackIcon; final String title; final VoidCallback onTap; const AnimatedServiceCard({ Key? key, required this.bgColor, required this.iconColor, required this.fallbackIcon, required this.title, required this.onTap, }) : super(key: key); @override State createState() => _AnimatedServiceCardState(); } class _AnimatedServiceCardState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; bool _isPressed = false; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 3), vsync: this, )..repeat(reverse: true); _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Deteksi ukuran layar untuk responsivitas final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; return InkWell( onTap: widget.onTap, onHighlightChanged: (pressed) { setState(() => _isPressed = pressed); }, borderRadius: BorderRadius.circular(16), child: AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: widget.iconColor.withOpacity(_isPressed ? 0.3 : 0.2), blurRadius: _isPressed ? 15 : 10, spreadRadius: _isPressed ? 2 + _animation.value * 3 : 1 + _animation.value * 2, ), ], ), child: Column( children: [ Expanded( flex: 7, child: Container( width: double.infinity, decoration: BoxDecoration( color: widget.bgColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), ), ), child: Center( child: AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.scale( scale: 1.0 + (_animation.value * 0.1), child: Icon( widget.fallbackIcon, color: widget.iconColor, size: isSmallScreen ? 32 : 36, ), ); }, ), ), ), ), Container( width: double.infinity, padding: EdgeInsets.symmetric( horizontal: isSmallScreen ? 8 : 10, vertical: isSmallScreen ? 8 : 10 ), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( bottomLeft: Radius.circular(16), bottomRight: Radius.circular(16), ), ), child: Text( widget.title, style: GoogleFonts.poppins( fontSize: isSmallScreen ? 12 : 14, fontWeight: FontWeight.w600, color: Colors.black87, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), ); }, ), ); } } class AnimatedTipCard extends StatefulWidget { final String title; final String description; final Color color; final IconData icon; const AnimatedTipCard({ Key? key, required this.title, required this.description, required this.color, required this.icon, }) : super(key: key); @override State createState() => _AnimatedTipCardState(); } class _AnimatedTipCardState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; bool _isActive = false; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 4), vsync: this, )..repeat(reverse: false); _animation = Tween(begin: 0.0, end: 1.0).animate(_controller); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Deteksi ukuran layar untuk responsivitas final screenWidth = MediaQuery.of(context).size.width; final isSmallScreen = screenWidth < 360; final cardWidth = screenWidth * (isSmallScreen ? 0.7 : 0.75); return GestureDetector( onTapDown: (_) => setState(() => _isActive = true), onTapUp: (_) => setState(() => _isActive = false), onTapCancel: () => setState(() => _isActive = false), child: AnimatedBuilder( animation: _animation, builder: (context, child) { return AnimatedContainer( duration: const Duration(milliseconds: 300), width: cardWidth, padding: EdgeInsets.all(isSmallScreen ? 12 : 14), decoration: BoxDecoration( color: widget.color, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: widget.color.withOpacity(_isActive ? 0.4 : 0.2), blurRadius: _isActive ? 10 : 3, spreadRadius: _isActive ? 2 : 0, offset: const Offset(0, 2), ), ], gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ widget.color, Color.lerp(widget.color, Colors.white, 0.2)!, ], stops: const [0.6, 1.0], ), ), child: Stack( children: [ // Shimmer effect Positioned.fill( child: IgnorePointer( ignoring: true, child: ShaderMask( blendMode: BlendMode.srcATop, shaderCallback: (bounds) { return LinearGradient( begin: Alignment( -1.0 + 2 * _animation.value, -0.5, ), end: Alignment( 0.0 + 2 * _animation.value, 0.5, ), colors: const [ Colors.transparent, Colors.white, Colors.transparent, ], stops: const [0.0, 0.5, 1.0], ).createShader(bounds); }, child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.white.withOpacity(0.1), ), ), ), ), ), // Content Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), padding: EdgeInsets.all(isSmallScreen ? 5 : 6), decoration: BoxDecoration( color: Colors.white.withOpacity(_isActive ? 0.4 : 0.3), shape: BoxShape.circle, boxShadow: _isActive ? [ BoxShadow( color: Colors.white.withOpacity(0.3), blurRadius: 8, spreadRadius: 2, ) ] : [], ), child: AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.rotate( angle: _isActive ? _animation.value * 0.1 : 0, child: Icon( widget.icon, color: Colors.white, size: _isActive ? (isSmallScreen ? 16 : 18) : (isSmallScreen ? 14 : 16), ), ); }, ), ), const SizedBox(width: 8), Expanded( child: Text( widget.title, style: GoogleFonts.poppins( fontSize: isSmallScreen ? 13 : 14, fontWeight: FontWeight.bold, color: Colors.white, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), ], ), const SizedBox(height: 8), Expanded( child: Text( widget.description, style: GoogleFonts.poppins( fontSize: isSmallScreen ? 11 : 12, color: Colors.white, height: 1.4, ), maxLines: 3, overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ); }, ), ); } }