MIF_E31222656/lib/screens/calendar/calendar_screen.dart

733 lines
22 KiB
Dart

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<KalenderTanamScreen> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
Map<DateTime, List<dynamic>> _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<void> _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<dynamic>;
// Convert to event map format
final eventMap = <DateTime, List<dynamic>>{};
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<dynamic> _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<void> _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<Map<String, dynamic>> 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<Map<String, dynamic>>.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<void> _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<List<dynamic>>();
// 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<void> _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<List<dynamic>>();
// 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<void> _fetchSchedules() async {
if (mounted) {
setState(() => _isLoading = true);
}
await _fetchEvents();
if (mounted) {
setState(() => _isLoading = false);
}
}
// Add this method to show exit confirmation dialog
Future<void> _showExitConfirmationDialog() async {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Konfirmasi'),
content: const Text('Apakah Anda yakin ingin keluar dari aplikasi?'),
actions: <Widget>[
TextButton(
child: const Text('Batal'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Keluar'),
onPressed: () {
Navigator.of(context).pop();
SystemNavigator.pop();
},
),
],
);
},
);
}
}