import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:tugas_akhir_supabase/utils/date_formatter.dart'; import 'package:intl/intl.dart'; import 'package:tugas_akhir_supabase/screens/calendar/add_schedule_dialog.dart'; import 'dart:async'; import 'package:tugas_akhir_supabase/utils/app_events.dart'; import 'package:tugas_akhir_supabase/screens/calendar/schedule_list_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/field_management_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/add_field_bottom_sheet.dart'; import 'package:flutter/services.dart'; class KalenderTanamScreen extends StatefulWidget { const KalenderTanamScreen({super.key}); @override _KalenderTanamScreenState createState() => _KalenderTanamScreenState(); } class _KalenderTanamScreenState extends State { CalendarFormat _calendarFormat = CalendarFormat.month; DateTime _focusedDay = DateTime.now(); DateTime? _selectedDay; Map> _events = {}; bool _isLoading = true; int _activeSchedules = 0; int _totalFields = 0; @override void initState() { super.initState(); _selectedDay = _focusedDay; // Hapus kode yang mungkin mengganggu keyboard _fetchEvents(); _fetchScheduleCount(); _fetchFieldCount(); } @override void didChangeDependencies() { super.didChangeDependencies(); // Hapus kode yang mungkin mengganggu keyboard } Future _fetchEvents() async { if (mounted) { setState(() => _isLoading = true); } try { final user = Supabase.instance.client.auth.currentUser; if (user == null) { if (mounted) { setState(() => _isLoading = false); } return; } // Perbaiki query dengan menyebutkan relasi yang spesifik 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: true); final schedules = response as List; // Convert to event map format final eventMap = >{}; for (final schedule in schedules) { final startDate = DateTime.parse(schedule['start_date']); final endDate = DateTime.parse(schedule['end_date']); // Generate a list of all dates between start and end var currentDate = startDate; while (currentDate.isBefore(endDate) || currentDate.isAtSameMomentAs(endDate)) { final day = DateTime( currentDate.year, currentDate.month, currentDate.day, ); if (eventMap[day] == null) { eventMap[day] = []; } eventMap[day]!.add(schedule); currentDate = currentDate.add(const Duration(days: 1)); } } if (mounted) { setState(() { _events = eventMap; _isLoading = false; }); } } catch (e) { if (mounted) { setState(() => _isLoading = false); ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}'))); } } } List _getEventsForDay(DateTime day) { return _events[DateTime(day.year, day.month, day.day)] ?? []; } @override Widget build(BuildContext context) { return GestureDetector( // Hapus unfocus yang menyebabkan masalah keyboard // onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( backgroundColor: const Color(0xFF056839), elevation: 0, title: Text( 'Kalender Tanam', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), automaticallyImplyLeading: false, actions: [ IconButton( icon: const Icon(Icons.refresh), tooltip: 'Refresh Data', onPressed: () { setState(() => _isLoading = true); _fetchEvents(); _fetchScheduleCount(); _fetchFieldCount(); }, ), IconButton( icon: const Icon(Icons.list), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const ScheduleListScreen()), ).then((_) { _fetchEvents(); _fetchScheduleCount(); _fetchFieldCount(); }); }, ), ], ), body: SafeArea( child: Column( children: [ _buildMonthNavigation(), Expanded( child: Stack( children: [ _buildCalendar(), Positioned( bottom: 0, left: 0, right: 0, child: _buildStatsRow(), ), ], ), ), _buildViewAllButton(), ], ), ), ), ); } Widget _buildMonthNavigation() { return Container( color: const Color(0xFF056839), padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(width: 40), Row( children: [ IconButton( icon: const Icon( Icons.chevron_left, color: Colors.white, size: 24, ), onPressed: () { setState(() { _focusedDay = DateTime( _focusedDay.year, _focusedDay.month - 1, ); }); }, ), Text( DateFormat('MMMM yyyy').format(_focusedDay), style: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), IconButton( icon: const Icon( Icons.chevron_right, color: Colors.white, size: 24, ), onPressed: () { setState(() { _focusedDay = DateTime( _focusedDay.year, _focusedDay.month + 1, ); }); }, ), ], ), ], ), ); } Widget _buildCalendar() { return Container( color: Colors.white, child: TableCalendar( firstDay: DateTime.utc(2020, 1, 1), lastDay: DateTime.utc(2030, 12, 31), focusedDay: _focusedDay, calendarFormat: _calendarFormat, eventLoader: _getEventsForDay, startingDayOfWeek: StartingDayOfWeek.monday, selectedDayPredicate: (day) { return isSameDay(_selectedDay, day); }, onDaySelected: (selectedDay, focusedDay) { setState(() { _selectedDay = selectedDay; _focusedDay = focusedDay; }); _showAddScheduleDialog(selectedDay); }, onFormatChanged: (format) { if (_calendarFormat != format) { setState(() { _calendarFormat = format; }); } }, onPageChanged: (focusedDay) { setState(() { _focusedDay = focusedDay; }); }, calendarStyle: CalendarStyle( markersMaxCount: 1, markerSize: 5, markerDecoration: const BoxDecoration( color: Color(0xFF056839), shape: BoxShape.circle, ), markersAlignment: Alignment.bottomCenter, markersAnchor: 0.85, canMarkersOverflow: false, isTodayHighlighted: true, selectedDecoration: const BoxDecoration( color: Color(0xFF056839), shape: BoxShape.circle, ), todayDecoration: const BoxDecoration( color: Color(0xFF056839), shape: BoxShape.circle, ), weekendTextStyle: const TextStyle( color: Colors.red, fontSize: 14, fontWeight: FontWeight.w500, ), defaultTextStyle: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87, ), outsideTextStyle: TextStyle( color: Colors.grey[400], fontSize: 14, fontWeight: FontWeight.w500, ), selectedTextStyle: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold, ), todayTextStyle: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.bold, ), cellMargin: EdgeInsets.zero, cellPadding: EdgeInsets.zero, tableBorder: TableBorder.all(width: 0, color: Colors.transparent), ), headerVisible: false, daysOfWeekStyle: DaysOfWeekStyle( weekdayStyle: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.black87, ), weekendStyle: GoogleFonts.poppins( fontSize: 12, fontWeight: FontWeight.w600, color: Colors.red, ), decoration: const BoxDecoration(color: Colors.white), ), daysOfWeekHeight: 32, rowHeight: 42, sixWeekMonthsEnforced: false, shouldFillViewport: false, availableCalendarFormats: const {CalendarFormat.month: 'Bulan'}, ), ); } Widget _buildStatsRow() { return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Row( children: [ Expanded( child: _buildStatCard( icon: Icons.calendar_today, iconColor: const Color(0xFF4CAF50), backgroundColor: const Color(0xFFE8F5E8), value: '$_activeSchedules', label: 'Jadwal', ), ), const SizedBox(width: 4), Expanded( child: _buildStatCard( icon: Icons.grid_view, iconColor: const Color(0xFF4CAF50), backgroundColor: const Color(0xFFE8F5E8), value: '$_totalFields', label: 'Lahan', ), ), const SizedBox(width: 4), Expanded( child: _buildStatCard( icon: Icons.trending_up, iconColor: const Color(0xFF2196F3), backgroundColor: const Color(0xFFE3F2FD), value: '0', label: 'Progress', ), ), ], ), ); } Widget _buildStatCard({ required IconData icon, required Color iconColor, required Color backgroundColor, required String value, required String label, VoidCallback? onTap, }) { return GestureDetector( onTap: onTap, child: Container( height: 125, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 2), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 3, offset: const Offset(0, 1), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: backgroundColor, shape: BoxShape.circle, ), child: Icon(icon, color: iconColor, size: 14), ), const SizedBox(height: 2), Text( value, style: GoogleFonts.poppins( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black87, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( label, style: GoogleFonts.poppins( fontSize: 9, fontWeight: FontWeight.w400, color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), ); } Widget _buildViewAllButton() { return Container( margin: const EdgeInsets.all(10), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: double.infinity, height: 42, child: ElevatedButton.icon( onPressed: () { Navigator.pushNamed(context, '/schedule-list'); }, icon: const Icon(Icons.list_alt, size: 16, color: Colors.white), label: Text( 'Lihat Semua Jadwal Tanam', style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white, ), ), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF056839), elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), padding: const EdgeInsets.symmetric(horizontal: 16), ), ), ), const SizedBox(height: 8), SizedBox( width: double.infinity, height: 42, child: OutlinedButton.icon( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const FieldManagementScreen()), ).then((_) { _fetchFieldCount(); }); }, icon: const Icon(Icons.grid_view, size: 16), label: Text( 'Kelola Lahan', style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, ), ), style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF056839), side: const BorderSide(color: Color(0xFF056839), width: 1.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), padding: const EdgeInsets.symmetric(horizontal: 16), ), ), ), ], ), ); } Future _showAddScheduleDialog(DateTime selectedDate) async { // Dismiss keyboard before showing dialog // Hapus unfocus yang mungkin mengganggu keyboard // Show loading indicator first showDialog( context: context, barrierDismissible: false, builder: (context) => const Center(child: CircularProgressIndicator()), ); // Fetch existing schedules to avoid conflicts List> existingSchedules = []; try { final user = Supabase.instance.client.auth.currentUser; if (user != null) { final response = await Supabase.instance.client .from('crop_schedules') .select('id, field_id, plot, start_date, end_date') .eq('user_id', user.id) .timeout(const Duration(seconds: 10)); existingSchedules = List>.from(response); } } catch (e) { debugPrint('Error fetching existing schedules: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal memuat jadwal yang ada: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } if (!mounted) return; Navigator.of(context).pop(); // Close loading indicator final result = await showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return AddScheduleDialog( existingSchedules: existingSchedules, initialStartDate: selectedDate, onScheduleAdded: (newSchedule) { AppEventBus().fireScheduleUpdated(); }, ); }, ); // Selalu refresh data setelah dialog ditutup _fetchEvents(); _fetchScheduleCount(); _fetchFieldCount(); } Future _fetchScheduleCount() async { final user = Supabase.instance.client.auth.currentUser; if (user == null) { debugPrint('ERROR: User is null in _fetchScheduleCount'); return; } try { debugPrint('INFO: Fetching active schedules count'); // Add timeout handling to prevent freezing final completer = Completer>(); // Set a timeout to prevent the app from hanging Future.delayed(const Duration(seconds: 8), () { if (!completer.isCompleted) { completer.completeError( TimeoutException('Koneksi timeout saat memuat jumlah jadwal.'), ); } }); // Hapus filter waktu yang mungkin terlalu membatasi Supabase.instance.client .from('crop_schedules') .select() .eq('user_id', user.id) .eq('status', 'active') .then((value) { if (!completer.isCompleted) completer.complete(value); }) .catchError((error) { if (!completer.isCompleted) completer.completeError(error); }); final response = await completer.future; debugPrint('INFO: Schedule count response type: ${response.runtimeType}'); debugPrint('INFO: Raw schedule count response: $response'); int count = 0; count = response.length; debugPrint('INFO: Parsed active schedules count: $count'); // Log first schedule for debugging if available if (response.isNotEmpty) { debugPrint('INFO: First active schedule: ${response.first}'); } if (mounted) { setState(() { _activeSchedules = count; }); } } catch (e) { debugPrint('ERROR: Error fetching active schedules: $e'); // Continue silently for count errors - they're not critical } } Future _fetchFieldCount() async { final user = Supabase.instance.client.auth.currentUser; if (user == null) { debugPrint('ERROR: User is null in _fetchFieldCount'); return; } try { debugPrint('INFO: Fetching fields count'); // Add timeout handling to prevent freezing final completer = Completer>(); // Set a timeout to prevent the app from hanging Future.delayed(const Duration(seconds: 8), () { if (!completer.isCompleted) { completer.completeError( TimeoutException('Koneksi timeout saat memuat jumlah lahan.'), ); } }); // Fetch all fields for this user Supabase.instance.client .from('fields') .select() .eq('user_id', user.id) .then((value) { if (!completer.isCompleted) completer.complete(value); }) .catchError((error) { if (!completer.isCompleted) completer.completeError(error); }); final response = await completer.future; debugPrint('INFO: Fields count response type: ${response.runtimeType}'); debugPrint('INFO: Raw fields count response: $response'); int count = 0; count = response.length; debugPrint('INFO: Parsed fields count: $count'); // Log first field for debugging if available if (response.isNotEmpty) { debugPrint('INFO: First field: ${response.first}'); } if (mounted) { setState(() { _totalFields = count; }); } } catch (e) { debugPrint('ERROR: Error fetching fields count: $e'); // Continue silently for count errors - they're not critical } } Future _fetchSchedules() async { if (mounted) { setState(() => _isLoading = true); } await _fetchEvents(); if (mounted) { setState(() => _isLoading = false); } } // Add this method to show exit confirmation dialog Future _showExitConfirmationDialog() async { return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: const Text('Konfirmasi'), content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'), actions: [ TextButton( child: const Text('Batal'), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( child: const Text('Keluar'), onPressed: () { Navigator.of(context).pop(); SystemNavigator.pop(); }, ), ], ); }, ); } }