MIF_E31222656/lib/screens/calendar/schedule_list_screen.dart

926 lines
33 KiB
Dart

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<ScheduleListScreen> {
bool _isLoading = true;
List<Map<String, dynamic>> _schedules = [];
// Map untuk warna dan ikon tanaman
final Map<String, Map<String, dynamic>> _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<void> _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<Color>(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<bool> _showDeleteConfirmationDialog(
String scheduleId,
String cropName,
) async {
return await showDialog<bool>(
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: <Widget>[
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<void> _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<String, dynamic> 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()
: <Map<String, dynamic>>[];
if (!mounted) return;
final scheduleToEdit = Map<String, dynamic>.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()
: <Map<String, dynamic>>[];
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),
),
),
);
}
}
}