588 lines
17 KiB
Dart
588 lines
17 KiB
Dart
import 'dart:ui';
|
|
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.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/calendar/schedule_detail_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/community_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/community/enhanced_community_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/panen/analisis_panen_screen.dart';
|
|
import 'package:tugas_akhir_supabase/screens/profile_screen.dart';
|
|
import 'package:tugas_akhir_supabase/models/crop_schedule.dart';
|
|
import 'package:tugas_akhir_supabase/screens/home/home_content.dart';
|
|
import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart';
|
|
import 'package:tugas_akhir_supabase/utils/date_formatter.dart';
|
|
import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart';
|
|
import 'package:tugas_akhir_supabase/services/auth_services.dart';
|
|
import 'package:tugas_akhir_supabase/services/session_manager.dart';
|
|
import 'package:tugas_akhir_supabase/utils/session_checker_mixin.dart';
|
|
import 'package:tugas_akhir_supabase/utils/fix_database_policies.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
_HomeScreenState createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> with SessionCheckerMixin {
|
|
User? _user;
|
|
int _selectedIndex = 0;
|
|
String? _profileImageUrl;
|
|
Map<String, dynamic>? _profile;
|
|
|
|
String? _scheduleId;
|
|
String? _cropName;
|
|
bool _isLoadingSchedule = true;
|
|
DateTime? _lastBackPressed;
|
|
|
|
// Variabel untuk melacak apakah perlu refresh
|
|
bool _needsHomeRefresh = false;
|
|
|
|
bool _isAdmin = false;
|
|
|
|
List<Widget> get _screens {
|
|
final userId = _user?.id ?? '';
|
|
|
|
return [
|
|
HomeContent(
|
|
userId: userId,
|
|
// Tambahkan parameter refresh key yang akan berubah saat perlu refresh
|
|
key: ValueKey(
|
|
'home_content_${_needsHomeRefresh ? 'refresh' : 'normal'}',
|
|
),
|
|
),
|
|
KalenderTanamScreen(),
|
|
PlantScannerScreen(),
|
|
_buildAnalisisScreen(userId),
|
|
EnhancedCommunityScreen(),
|
|
];
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_user = Supabase.instance.client.auth.currentUser;
|
|
|
|
// Gunakan Future.delayed untuk memastikan UI sudah dirender sebelum operasi berat
|
|
Future.delayed(Duration(milliseconds: 300), () {
|
|
if (mounted) {
|
|
_safeInitialize();
|
|
}
|
|
});
|
|
|
|
// Initialize session checking dengan delay
|
|
Future.delayed(Duration(milliseconds: 800), () {
|
|
if (mounted) {
|
|
initSessionChecking();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Clean up session checking
|
|
disposeSessionChecking();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _safeInitialize() async {
|
|
try {
|
|
// Set safety timer untuk mencegah loading yang tidak berhenti
|
|
Future.delayed(Duration(seconds: 5), () {
|
|
if (mounted && _isLoadingSchedule) {
|
|
debugPrint(
|
|
'[WARNING] Force completing schedule loading after timeout',
|
|
);
|
|
setState(() => _isLoadingSchedule = false);
|
|
}
|
|
});
|
|
|
|
// Jalankan operasi secara paralel untuk mempercepat
|
|
await Future.wait([
|
|
_loadUserProfile().timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Load user profile timed out');
|
|
},
|
|
),
|
|
_fetchScheduleIfNeeded().timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Fetch schedule timed out');
|
|
if (mounted) {
|
|
setState(() => _isLoadingSchedule = false);
|
|
}
|
|
},
|
|
),
|
|
_checkAdminStatus().timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Check admin status timed out');
|
|
},
|
|
),
|
|
_refreshUserSession().timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Refresh user session timed out');
|
|
},
|
|
),
|
|
]);
|
|
} catch (e) {
|
|
debugPrint('[ERROR] Error in safe initialize: $e');
|
|
if (mounted && _isLoadingSchedule) {
|
|
setState(() => _isLoadingSchedule = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshUserSession() async {
|
|
try {
|
|
// Update user activity timestamp
|
|
await updateUserActivity();
|
|
|
|
// Refresh Supabase session if needed
|
|
final authServices = GetIt.instance<AuthServices>();
|
|
await authServices.refreshSession();
|
|
debugPrint('Session refreshed in HomeScreen');
|
|
|
|
// Cek ulang status admin setelah refresh session
|
|
await _checkAdminStatus();
|
|
} catch (e) {
|
|
debugPrint('Error refreshing session in HomeScreen: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _loadUserProfile() async {
|
|
if (_user == null) {
|
|
debugPrint('FATAL: User is null, cannot load profile');
|
|
return;
|
|
}
|
|
|
|
debugPrint('INFO: Current user ID: ${_user!.id}');
|
|
debugPrint('INFO: Current user email: ${_user!.email}');
|
|
|
|
try {
|
|
debugPrint('INFO: Mencoba mencari profile untuk user ID: ${_user!.id}');
|
|
|
|
// Coba dengan query langsung ke tabel dengan timeout
|
|
final response = await Supabase.instance.client
|
|
.from('profiles')
|
|
.select('*')
|
|
.eq('user_id', _user!.id)
|
|
.limit(1)
|
|
.timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Profile query timed out');
|
|
throw TimeoutException('Profile query timed out');
|
|
},
|
|
);
|
|
|
|
debugPrint('QUERY RESULT: Hasil query length: ${response.length}');
|
|
debugPrint('QUERY RESULT: Response: $response');
|
|
|
|
if (response.isNotEmpty) {
|
|
final userData = response[0];
|
|
debugPrint('SUCCESS: Profile data ditemukan');
|
|
debugPrint('DATA: Full profile: $userData');
|
|
debugPrint('DATA: farm_name: ${userData['farm_name']}');
|
|
debugPrint('DATA: user_id: ${userData['user_id']}');
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_profileImageUrl = userData['avatar_url'];
|
|
_profile = userData;
|
|
});
|
|
}
|
|
} else {
|
|
debugPrint(
|
|
'FATAL: Tidak ada data profile ditemukan untuk user_id: ${_user!.id}',
|
|
);
|
|
|
|
// Fallback: Create a temporary profile for UI
|
|
if (mounted) {
|
|
setState(() {
|
|
_profile = {
|
|
'farm_name': 'pepepe', // Gunakan nama yang kita tahu ada
|
|
'user_id': _user!.id,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
} catch (e, stackTrace) {
|
|
debugPrint('ERROR: Gagal mengambil profile: $e');
|
|
debugPrint('STACKTRACE: $stackTrace');
|
|
|
|
// Fallback untuk UI
|
|
if (mounted) {
|
|
setState(() {
|
|
_profile = {
|
|
'farm_name': _user?.email?.split('@').first ?? 'Pengguna',
|
|
'user_id': _user!.id,
|
|
};
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<bool> _onWillPop() async {
|
|
final now = DateTime.now();
|
|
if (_lastBackPressed == null ||
|
|
now.difference(_lastBackPressed!) > Duration(seconds: 2)) {
|
|
_lastBackPressed = now;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Tekan sekali lagi untuk keluar'),
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
return false;
|
|
}
|
|
return true; // keluar aplikasi
|
|
}
|
|
|
|
Future<void> _fetchScheduleIfNeeded() async {
|
|
if (_user == null) {
|
|
setState(() => _isLoadingSchedule = false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final schedule = await fetchActiveSchedule(_user!.id).timeout(
|
|
Duration(seconds: 3),
|
|
onTimeout: () {
|
|
debugPrint('[WARNING] Fetch active schedule timed out');
|
|
return null;
|
|
},
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_scheduleId = schedule?['scheduleId'];
|
|
_cropName = schedule?['cropName'];
|
|
_isLoadingSchedule = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error saat ambil schedule: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoadingSchedule = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _onItemTapped(int index) {
|
|
// Update user activity timestamp when switching tabs
|
|
updateUserActivity();
|
|
|
|
// Jika sebelumnya berada di tab lain dan kembali ke home tab
|
|
if (_selectedIndex != 0 && index == 0 && _needsHomeRefresh) {
|
|
// Reset flag dan rebuild HomeContent dengan key baru
|
|
setState(() {
|
|
_needsHomeRefresh = false;
|
|
});
|
|
}
|
|
|
|
setState(() {
|
|
_selectedIndex = index;
|
|
});
|
|
}
|
|
|
|
// Tandai bahwa home screen perlu di-refresh
|
|
void _markHomeNeedsRefresh() {
|
|
setState(() {
|
|
_needsHomeRefresh = true;
|
|
});
|
|
}
|
|
|
|
void _navigateToProfile() {
|
|
// Update user activity timestamp when navigating to profile
|
|
updateUserActivity();
|
|
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => ProfileScreen()),
|
|
).then((_) {
|
|
// Reload profile when returning from profile screen
|
|
_loadUserProfile();
|
|
});
|
|
}
|
|
|
|
Widget _buildAnalisisScreen(String userId) {
|
|
if (_isLoadingSchedule) {
|
|
return Center(child: CircularProgressIndicator());
|
|
}
|
|
return AnalisisInputScreen(
|
|
userId: userId,
|
|
scheduleData:
|
|
_scheduleId != null
|
|
? {'id': _scheduleId, 'crop_name': _cropName}
|
|
: null,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
// Update user activity when building the screen
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
updateUserActivity();
|
|
});
|
|
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
|
|
final shouldExit = await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('Konfirmasi'),
|
|
content: const Text('Apakah Anda ingin keluar dari aplikasi?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Tidak'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: const Text('Keluar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (shouldExit == true) {
|
|
SystemNavigator.pop();
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xFFFAFAFA),
|
|
body: SafeArea(
|
|
child: Column(
|
|
children: [
|
|
_buildHeader(),
|
|
Expanded(
|
|
child: IndexedStack(index: _selectedIndex, children: _screens),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
bottomNavigationBar: _buildBottomNavBar(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
offset: const Offset(0, 1),
|
|
blurRadius: 2,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'TaniSM4RT',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.bold,
|
|
color: const Color(0xFF056839),
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
_getUserDisplayName(),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
GestureDetector(
|
|
onTap: () {
|
|
// Update user activity when tapping profile
|
|
updateUserActivity();
|
|
_navigateToProfile();
|
|
},
|
|
child: Container(
|
|
height: 40,
|
|
width: 40,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.grey[200],
|
|
image:
|
|
_profileImageUrl != null
|
|
? DecorationImage(
|
|
image: NetworkImage(_profileImageUrl!),
|
|
fit: BoxFit.cover,
|
|
)
|
|
: null,
|
|
border: Border.all(color: Colors.white, width: 1.5),
|
|
),
|
|
child:
|
|
_profileImageUrl == null
|
|
? const Icon(
|
|
Icons.person,
|
|
color: Colors.grey,
|
|
size: 20,
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomNavBar() {
|
|
return Container(
|
|
height: 60,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, -1),
|
|
),
|
|
],
|
|
),
|
|
child: BottomNavigationBar(
|
|
currentIndex: _selectedIndex,
|
|
onTap: _onItemTapped,
|
|
backgroundColor: Colors.white,
|
|
type: BottomNavigationBarType.fixed,
|
|
selectedItemColor: const Color(0xFF056839),
|
|
unselectedItemColor: Colors.grey,
|
|
selectedFontSize: 9,
|
|
unselectedFontSize: 11,
|
|
iconSize: 20,
|
|
selectedLabelStyle: GoogleFonts.poppins(fontWeight: FontWeight.w500),
|
|
unselectedLabelStyle: GoogleFonts.poppins(fontWeight: FontWeight.w500),
|
|
elevation: 0,
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.home_rounded),
|
|
label: 'Beranda',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.calendar_today_rounded),
|
|
label: 'Kalender',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.document_scanner_rounded),
|
|
label: 'Deteksi',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.analytics_rounded),
|
|
label: 'Analisis',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.forum_rounded),
|
|
label: 'Komunitas',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _getUserDisplayName() {
|
|
debugPrint('DIAGNOSIS: Mencoba mendapatkan nama display');
|
|
debugPrint('DIAGNOSIS: Profile ada? ${_profile != null}');
|
|
|
|
if (_profile != null) {
|
|
debugPrint('DIAGNOSIS: Isi profile: $_profile');
|
|
debugPrint('DIAGNOSIS: Keys dalam profile: ${_profile!.keys.toList()}');
|
|
}
|
|
|
|
// Prioritaskan username dari database
|
|
if (_profile != null &&
|
|
_profile!['username'] != null &&
|
|
_profile!['username'].toString().isNotEmpty) {
|
|
final username = _profile!['username'].toString();
|
|
debugPrint('DIAGNOSIS: Menggunakan username dari database: $username');
|
|
return 'Hi, $username';
|
|
}
|
|
|
|
// Fallback ke farm_name
|
|
if (_profile != null &&
|
|
_profile!['farm_name'] != null &&
|
|
_profile!['farm_name'].toString().isNotEmpty) {
|
|
final farmName = _profile!['farm_name'].toString();
|
|
debugPrint('DIAGNOSIS: Menggunakan farm_name dari database: $farmName');
|
|
return 'Hi, $farmName';
|
|
}
|
|
|
|
// Fallback ke email
|
|
if (_user != null && _user!.email != null) {
|
|
final email = _user!.email!;
|
|
final username = email.split('@').first;
|
|
debugPrint('DIAGNOSIS: Menggunakan nama dari email: $username');
|
|
return 'Hi, $username';
|
|
}
|
|
|
|
return 'Hi, Petani';
|
|
}
|
|
|
|
Future<Map<String, String>?> fetchActiveSchedule(String userId) async {
|
|
try {
|
|
final response =
|
|
await Supabase.instance.client
|
|
.from('crop_schedules')
|
|
.select('id, crop_name')
|
|
.eq('user_id', userId)
|
|
.order('created_at', ascending: false)
|
|
.limit(1)
|
|
.single();
|
|
|
|
if (response['id'] != null && response['crop_name'] != null) {
|
|
return {
|
|
'scheduleId': response['id'],
|
|
'cropName': response['crop_name'],
|
|
};
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Gagal fetch schedule: $e');
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<void> _checkAdminStatus() async {
|
|
try {
|
|
final authServices = GetIt.instance<AuthServices>();
|
|
final isAdmin = await authServices.isAdmin();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isAdmin = isAdmin;
|
|
});
|
|
}
|
|
|
|
debugPrint('Admin status checked: $_isAdmin');
|
|
} catch (e) {
|
|
debugPrint('Error checking admin status: $e');
|
|
}
|
|
}
|
|
}
|