import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/utils/date_formatter.dart'; import 'package:tugas_akhir_supabase/screens/calendar/add_schedule_dialog.dart'; class ScheduleListScreen extends StatefulWidget { const ScheduleListScreen({super.key}); @override _ScheduleListScreenState createState() => _ScheduleListScreenState(); } class _ScheduleListScreenState extends State { bool _isLoading = true; List> _schedules = []; // Map untuk warna dan ikon tanaman final Map> _cropIcons = { 'Padi': {'icon': Icons.grass, 'color': Color.fromARGB(255, 6, 75, 9)}, 'Jagung': {'icon': Icons.eco, 'color': Color.fromARGB(255, 188, 171, 16)}, 'Kedelai': {'icon': Icons.spa, 'color': Color(0xFFFFA000)}, 'Cabai': {'icon': Icons.whatshot, 'color': Color(0xFFE53935)}, 'Tomat': {'icon': Icons.circle, 'color': Color(0xFFE53935)}, 'Bawang': {'icon': Icons.layers, 'color': Color(0xFFAB47BC)}, 'Lainnya': {'icon': Icons.local_florist, 'color': Color(0xFF42A5F5)}, }; @override void initState() { super.initState(); _fetchSchedules(); // Clear any existing error messages WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).clearSnackBars(); } }); } Future _fetchSchedules() async { if (!mounted) return; setState(() => _isLoading = true); try { final user = Supabase.instance.client.auth.currentUser; if (user == null) { if (mounted) setState(() => _isLoading = false); return; } // Perbaiki query dengan relasi yang tepat final response = await Supabase.instance.client .from('crop_schedules') .select('*, fields:fields!crop_schedules_field_id_fkey(*)') .eq('user_id', user.id) .order('start_date', ascending: false); debugPrint('INFO: Raw schedules response type: ${response.runtimeType}'); debugPrint('INFO: Raw schedules response: $response'); debugPrint('INFO: Response is a List with ${response.length} items'); // Jangan lakukan filter tambahan, gunakan semua data yang diterima final schedulesList = response.map((item) => item).toList(); debugPrint('INFO: Parsed schedules count: ${schedulesList.length}'); if (schedulesList.isNotEmpty) { debugPrint('INFO: First schedule: ${schedulesList.first}'); } if (mounted) { setState(() { _schedules = schedulesList; _isLoading = false; }); } } catch (e) { debugPrint('ERROR: Error fetching schedules: $e'); if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}'))); } } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8FAFC), appBar: AppBar( title: Text( 'Jadwal Tanam', style: GoogleFonts.poppins( fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white, ), ), backgroundColor: const Color.fromARGB(255, 15, 92, 18), foregroundColor: Colors.white, elevation: 0, flexibleSpace: Container( decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color.fromARGB(255, 15, 92, 18), Color(0xFF2E7D32)], ), ), ), actions: [ Container( margin: const EdgeInsets.only(right: 8), child: IconButton( icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.refresh, size: 20), ), tooltip: 'Refresh Data', onPressed: () { setState(() => _isLoading = true); _fetchSchedules(); }, ), ), ], ), body: _isLoading ? const Center( child: CircularProgressIndicator( color: Color.fromARGB(255, 15, 92, 18), strokeWidth: 3, ), ) : _schedules.isEmpty ? _buildEmptyState() : _buildScheduleList(), floatingActionButton: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color.fromARGB(255, 15, 92, 18), Color(0xFF2E7D32)], ), boxShadow: [ BoxShadow( color: const Color.fromARGB(255, 15, 92, 18).withOpacity(0.4), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: FloatingActionButton( onPressed: () { _showAddScheduleDialog(); }, backgroundColor: Colors.transparent, elevation: 0, child: const Icon(Icons.add, color: Colors.white, size: 28), ), ), bottomNavigationBar: null, ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: const Color.fromARGB(255, 15, 92, 18).withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( Icons.agriculture, size: 64, color: const Color.fromARGB(255, 15, 92, 18), ), ), const SizedBox(height: 24), Text( 'Belum Ada Jadwal Tanam', style: GoogleFonts.poppins( fontSize: 20, fontWeight: FontWeight.w700, color: const Color(0xFF2E7D32), ), ), const SizedBox(height: 12), Text( 'Mulai perjalanan bertani Anda dengan\nmembuat jadwal tanam pertama', textAlign: TextAlign.center, style: GoogleFonts.poppins( fontSize: 14, color: Colors.grey[600], height: 1.5, ), ), const SizedBox(height: 32), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), gradient: const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color.fromARGB(255, 15, 92, 18), Color(0xFF2E7D32)], ), boxShadow: [ BoxShadow( color: const Color.fromARGB( 255, 15, 92, 18, ).withOpacity(0.3), blurRadius: 12, offset: const Offset(0, 6), ), ], ), child: ElevatedButton.icon( onPressed: () { _showAddScheduleDialog(); }, icon: const Icon(Icons.add, size: 20), label: Text( 'Buat Jadwal Tanam', style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, ), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, foregroundColor: Colors.white, elevation: 0, padding: const EdgeInsets.symmetric( horizontal: 24, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ), ], ), ), ); } Widget _buildScheduleList() { return ListView.builder( padding: const EdgeInsets.all(16), itemCount: _schedules.length, itemBuilder: (context, index) { final schedule = _schedules[index]; final scheduleId = schedule['id']; final cropName = schedule['crop_name'] ?? 'Tanaman'; final fieldName = schedule['fields']?['name'] ?? 'Lahan'; final startDate = DateTime.parse(schedule['start_date']); final endDate = DateTime.parse(schedule['end_date']); final status = schedule['status'] ?? 'active'; // Get crop icon and color final cropInfo = _cropIcons[cropName] ?? _cropIcons['Lainnya']!; final IconData cropIcon = cropInfo['icon']; final Color cropColor = cropInfo['color']; // Calculate progress final totalDuration = endDate.difference(startDate).inDays; final elapsedDuration = DateTime.now().difference(startDate).inDays; double progress = elapsedDuration / totalDuration; progress = progress.clamp(0.0, 1.0); // Status color now uses crop color Color statusColor = cropColor; String statusText; IconData statusIcon; switch (status.toLowerCase()) { case 'active': statusText = 'Aktif'; statusIcon = Icons.play_circle_filled; break; case 'completed': statusText = 'Selesai'; statusIcon = Icons.check_circle; break; case 'cancelled': statusText = 'Dibatalkan'; statusIcon = Icons.cancel; break; default: statusText = 'Pending'; statusIcon = Icons.pause_circle_filled; } return Dismissible( key: Key(scheduleId), background: _buildDismissibleBackground(true), secondaryBackground: _buildDismissibleBackground(false), confirmDismiss: (direction) async { if (direction == DismissDirection.endToStart) { return await _showDeleteConfirmationDialog(scheduleId, cropName); } else if (direction == DismissDirection.startToEnd) { _editSchedule(schedule); return false; } return false; }, onDismissed: (direction) { if (direction == DismissDirection.endToStart) { setState(() { _schedules.removeAt(index); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Jadwal $cropName telah dihapus'), backgroundColor: const Color.fromARGB(255, 15, 92, 18), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); } }, child: Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], // Add subtle border with crop color border: Border.all(color: cropColor.withOpacity(0.3), width: 1.5), ), child: InkWell( onTap: () { Navigator.pushNamed( context, '/kalender-detail', arguments: {'scheduleId': schedule['id']}, ); }, borderRadius: BorderRadius.circular(16), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Section Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: cropColor.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(cropIcon, color: cropColor, size: 24), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( cropName, style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w700, color: cropColor, ), overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), Row( children: [ Icon( Icons.location_on, size: 16, color: Colors.grey[600], ), const SizedBox(width: 4), Expanded( child: Text( fieldName, style: GoogleFonts.poppins( fontSize: 14, color: Colors.grey[600], ), overflow: TextOverflow.ellipsis, ), ), ], ), ], ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: statusColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: statusColor.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(statusIcon, size: 14, color: Colors.white), const SizedBox(width: 4), Text( statusText, style: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ], ), const SizedBox(height: 20), // Date Range Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cropColor.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: cropColor.withOpacity(0.2)), ), child: Row( children: [ Icon( Icons.calendar_today, color: cropColor, size: 20, ), const SizedBox(width: 12), Expanded( child: Text( '${formatDate(startDate)} - ${formatDate(endDate)}', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w500, color: cropColor.withOpacity(0.8), ), ), ), ], ), ), const SizedBox(height: 16), // Progress Section Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Progress Tanam', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, color: cropColor, ), ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: cropColor.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( '${(progress * 100).toInt()}%', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w700, color: cropColor, ), ), ), ], ), const SizedBox(height: 8), // Progress Bar Container( height: 8, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Colors.grey.withOpacity(0.1), ), child: ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progress, backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation(cropColor), minHeight: 8, ), ), ), const SizedBox(height: 16), // Action Buttons Row( children: [ Expanded( child: OutlinedButton.icon( onPressed: () => _editSchedule(schedule), icon: const Icon(Icons.edit, size: 16), label: Text( 'Edit', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, ), ), style: OutlinedButton.styleFrom( foregroundColor: cropColor, side: BorderSide(color: cropColor), padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), ), ), const SizedBox(width: 12), Expanded( flex: 2, child: ElevatedButton.icon( onPressed: () { Navigator.pushNamed( context, '/kalender-detail', arguments: {'scheduleId': schedule['id']}, ); }, icon: const Icon(Icons.visibility, size: 16), label: Text( 'Lihat Detail', style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, ), ), style: ElevatedButton.styleFrom( backgroundColor: cropColor, foregroundColor: Colors.white, elevation: 2, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), shadowColor: cropColor.withOpacity(0.3), ), ), ), ], ), ], ), ), ), ), ); }, ); } Widget _buildDismissibleBackground(bool isEdit) { return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: isEdit ? const Color(0xFF2196F3) : const Color(0xFFF44336), borderRadius: BorderRadius.circular(16), gradient: LinearGradient( begin: isEdit ? Alignment.centerLeft : Alignment.centerRight, end: isEdit ? Alignment.centerRight : Alignment.centerLeft, colors: isEdit ? [const Color(0xFF2196F3), const Color(0xFF1976D2)] : [const Color(0xFFF44336), const Color(0xFFD32F2F)], ), ), alignment: isEdit ? Alignment.centerLeft : Alignment.centerRight, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), shape: BoxShape.circle, ), child: Icon( isEdit ? Icons.edit : Icons.delete, color: Colors.white, size: 24, ), ), const SizedBox(height: 8), Text( isEdit ? 'Edit' : 'Hapus', style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 14, ), ), ], ), ); } Future _showDeleteConfirmationDialog( String scheduleId, String cropName, ) async { return await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: const Color(0xFFF44336).withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.warning, color: Color(0xFFF44336), size: 24, ), ), const SizedBox(width: 12), Text( 'Hapus Jadwal', style: GoogleFonts.poppins( fontWeight: FontWeight.w700, fontSize: 18, color: const Color(0xFF2E7D32), ), ), ], ), content: Text( 'Apakah Anda yakin ingin menghapus jadwal "$cropName"? Semua data terkait juga akan dihapus dan tidak dapat dikembalikan.', style: GoogleFonts.poppins( fontSize: 14, color: Colors.grey[700], height: 1.5, ), ), actions: [ OutlinedButton( onPressed: () => Navigator.of(context).pop(false), style: OutlinedButton.styleFrom( foregroundColor: Colors.grey[700], side: BorderSide(color: Colors.grey[300]!), padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( 'Batal', style: GoogleFonts.poppins(fontWeight: FontWeight.w600), ), ), const SizedBox(width: 8), ElevatedButton( onPressed: () async { Navigator.of(context).pop(true); await _deleteSchedule(scheduleId); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFFF44336), foregroundColor: Colors.white, elevation: 2, padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( 'Hapus', style: GoogleFonts.poppins(fontWeight: FontWeight.w600), ), ), ], ); }, ) ?? false; } Future _deleteSchedule(String scheduleId) async { try { await Supabase.instance.client .from('crop_schedules') .delete() .eq('id', scheduleId); } catch (e) { debugPrint('ERROR: Failed to delete schedule: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Gagal menghapus jadwal. Silakan coba lagi.'), backgroundColor: const Color(0xFFF44336), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); } } } void _editSchedule(Map schedule) async { final user = Supabase.instance.client.auth.currentUser; if (user == null) { debugPrint('ERROR: User is null in _editSchedule'); return; } try { final response = await Supabase.instance.client .from('crop_schedules') .select('id, field_id, plot, start_date, end_date') .eq('user_id', user.id) .neq('id', schedule['id']); final existingSchedules = response is List ? response.map((item) => item).toList() : >[]; if (!mounted) return; final scheduleToEdit = Map.from(schedule); final result = await showDialog( context: context, builder: (context) => Dialog( backgroundColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), insetPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 24, ), child: AddScheduleDialog( existingSchedules: existingSchedules, scheduleToEdit: scheduleToEdit, onScheduleAdded: (updatedSchedule) { debugPrint('INFO: Schedule updated: $updatedSchedule'); _fetchSchedules(); }, ), ), ); if (result == true) { await _fetchSchedules(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Jadwal berhasil diperbarui'), backgroundColor: const Color.fromARGB(255, 15, 92, 18), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); } } } catch (e) { debugPrint('ERROR: Error preparing edit schedule dialog: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Terjadi kesalahan. Silakan coba lagi.'), backgroundColor: const Color(0xFFF44336), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); } } } void _showAddScheduleDialog() async { final user = Supabase.instance.client.auth.currentUser; if (user == null) { debugPrint('ERROR: User is null in _showAddScheduleDialog'); return; } try { debugPrint('INFO: Fetching existing schedules for dialog'); final response = await Supabase.instance.client .from('crop_schedules') .select('id, field_id, plot, start_date, end_date') .eq('user_id', user.id); debugPrint('INFO: Dialog response type: ${response.runtimeType}'); debugPrint('INFO: Dialog raw response: $response'); final existingSchedules = response is List ? response.map((item) => item).toList() : >[]; debugPrint( 'INFO: Found ${existingSchedules.length} schedules for dialog', ); if (!mounted) return; final result = await showDialog( context: context, builder: (context) => Dialog( backgroundColor: Colors.transparent, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), insetPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 24, ), child: AddScheduleDialog( existingSchedules: existingSchedules, onScheduleAdded: (newSchedule) { debugPrint('INFO: New schedule added: $newSchedule'); _fetchSchedules(); }, ), ), ); if (result == true) { debugPrint('INFO: Refreshing schedules after dialog closed'); await _fetchSchedules(); } } catch (e) { debugPrint('Error preparing add schedule dialog: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Terjadi kesalahan. Silakan coba lagi.'), backgroundColor: const Color(0xFFF44336), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ); } } }