MIF_E31222656/lib/screens/home/home_content.dart

1560 lines
53 KiB
Dart

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<HomeContent> createState() => _HomeContentState();
}
class _HomeContentState extends State<HomeContent> {
// Data for dynamic content
List<Map<String, dynamic>> _scheduleData = [];
List<Map<String, dynamic>> _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<void> _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<List<dynamic>>();
// 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<void> _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<List<dynamic>>();
// 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<Widget> _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<AnimatedServiceCard> createState() => _AnimatedServiceCardState();
}
class _AnimatedServiceCardState extends State<AnimatedServiceCard> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isPressed = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 3),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(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<AnimatedTipCard> createState() => _AnimatedTipCardState();
}
class _AnimatedTipCardState extends State<AnimatedTipCard> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
bool _isActive = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 4),
vsync: this,
)..repeat(reverse: false);
_animation = Tween<double>(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,
),
),
],
),
],
),
);
},
),
);
}
}