This commit is contained in:
Your Name 2025-07-10 08:22:33 +07:00
parent 15327e0b2d
commit 9a5af453c1
16 changed files with 2239 additions and 1008 deletions

View File

@ -8,29 +8,38 @@ import 'package:tugas_akhir_supabase/screens/auth/login_screen.dart';
import 'package:tugas_akhir_supabase/screens/auth/otp_screen.dart'; import 'package:tugas_akhir_supabase/screens/auth/otp_screen.dart';
import 'package:tugas_akhir_supabase/screens/auth/register_screen.dart'; import 'package:tugas_akhir_supabase/screens/auth/register_screen.dart';
import 'package:tugas_akhir_supabase/screens/auth/reset_password_screen.dart'; import 'package:tugas_akhir_supabase/screens/auth/reset_password_screen.dart';
import 'package:tugas_akhir_supabase/screens/calendar/calendar_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/calendar_screen.dart'
as calendar;
import 'package:tugas_akhir_supabase/screens/calendar/field_management_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/field_management_screen.dart';
import 'package:tugas_akhir_supabase/screens/calendar/schedule_list_screen.dart'; import 'package:tugas_akhir_supabase/screens/calendar/schedule_list_screen.dart';
import 'package:tugas_akhir_supabase/screens/calendar/schedule_detail_screen.dart'
as detail;
import 'package:tugas_akhir_supabase/screens/community/community_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/community/enhanced_community_screen.dart';
import 'package:tugas_akhir_supabase/screens/home_screen.dart'; import 'package:tugas_akhir_supabase/screens/home_screen.dart';
import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart'; import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart'
import 'package:tugas_akhir_supabase/screens/intro/animation_splash_screen.dart'; as scanner;
import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart'; import 'package:tugas_akhir_supabase/screens/intro/animation_splash_screen.dart'
import 'package:tugas_akhir_supabase/screens/panen/analisis_chart_screen.dart'; as splash;
import 'package:tugas_akhir_supabase/screens/panen/analisis_hasil_screen.dart'; import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart'
import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart'; as intro;
import 'package:tugas_akhir_supabase/screens/panen/analisis_panen_screen.dart'; import 'package:tugas_akhir_supabase/screens/panen/analisis_chart_screen.dart'
as chart;
import 'package:tugas_akhir_supabase/screens/panen/analisis_hasil_screen.dart'
as hasil;
import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart'
as input;
import 'package:tugas_akhir_supabase/screens/panen/analisis_panen_screen.dart'
as panen;
import 'package:tugas_akhir_supabase/screens/profile_screen.dart'; import 'package:tugas_akhir_supabase/screens/profile_screen.dart';
import 'package:tugas_akhir_supabase/screens/calendar/schedule_detail_screen.dart';
import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart'; import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart';
/// Defines all routes used in the application /// Defines all routes used in the application
class AppRoutes { class AppRoutes {
/// Non-authenticated routes /// Non-authenticated routes
static final Map<String, Widget Function(BuildContext)> _publicRoutes = { static final Map<String, Widget Function(BuildContext)> _publicRoutes = {
'/': (context) => const SplashScreen(), '/': (context) => const splash.SplashScreen(),
'/intro': (context) => const AnimatedIntroScreen(), '/intro': (context) => const intro.AnimatedIntroScreen(),
'/login': (context) => const LoginScreen(), '/login': (context) => const LoginScreen(),
'/register': (context) => const RegisterScreen(), '/register': (context) => const RegisterScreen(),
'/forgot-password': (context) => const ForgotPasswordScreen(), '/forgot-password': (context) => const ForgotPasswordScreen(),
@ -50,26 +59,26 @@ class AppRoutes {
_authenticatedRoutes = { _authenticatedRoutes = {
'/home': (context) => const HomeScreen(), '/home': (context) => const HomeScreen(),
'/profile': (context) => const ProfileScreen(), '/profile': (context) => const ProfileScreen(),
'/calendar': (context) => const KalenderTanamScreen(), '/calendar': (context) => const calendar.KalenderTanamScreen(),
'/field-management': (context) => const FieldManagementScreen(), '/field-management': (context) => const FieldManagementScreen(),
'/schedule-list': (context) => const ScheduleListScreen(), '/schedule-list': (context) => const ScheduleListScreen(),
'/plant-scanner': (context) => const PlantScannerScreen(), '/plant-scanner': (context) => const scanner.PlantScannerScreen(),
'/community': (context) => const CommunityScreen(), '/community': (context) => const CommunityScreen(),
'/enhanced-community': (context) => const EnhancedCommunityScreen(), '/enhanced-community': (context) => const EnhancedCommunityScreen(),
'/analisis': (context) { '/analisis': (context) {
final args = final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?; ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
return HarvestAnalysisScreen(userId: args?['userId'] ?? ''); return panen.HarvestAnalysisScreen(userId: args?['userId'] ?? '');
}, },
'/analisis-input': (context) { '/analisis-input': (context) {
final args = final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?; ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
return AnalisisInputScreen(userId: args?['userId'] ?? ''); return input.AnalisisInputScreen(userId: args?['userId'] ?? '');
}, },
'/analisis-hasil': (context) { '/analisis-hasil': (context) {
final args = final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?; ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
return HarvestResultScreen( return hasil.HarvestResultScreen(
userId: args?['userId'] ?? '', userId: args?['userId'] ?? '',
harvestData: args?['harvestData'], harvestData: args?['harvestData'],
scheduleData: args?['scheduleData'], scheduleData: args?['scheduleData'],
@ -78,7 +87,7 @@ class AppRoutes {
'/analisis-chart': (context) { '/analisis-chart': (context) {
final args = final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?; ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
return HarvestAnalysisChart( return chart.HarvestAnalysisChart(
userId: args?['userId'] ?? '', userId: args?['userId'] ?? '',
harvestData: args?['harvestData'], harvestData: args?['harvestData'],
scheduleData: args?['scheduleData'], scheduleData: args?['scheduleData'],
@ -88,7 +97,7 @@ class AppRoutes {
'/kalender-detail': (context) { '/kalender-detail': (context) {
final args = final args =
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?; ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
return ScheduleDetailScreen(scheduleId: args?['scheduleId'] ?? ''); return detail.ScheduleDetailScreen(scheduleId: args?['scheduleId'] ?? '');
}, },
/// Admin routes /// Admin routes
@ -105,10 +114,30 @@ class AppRoutes {
/// Add public routes as-is /// Add public routes as-is
allRoutes.addAll(_publicRoutes); allRoutes.addAll(_publicRoutes);
/// Add authenticated routes wrapped with SessionGuardWrapper /// Add profile route without SessionGuardWrapper for easier access
allRoutes['/profile'] = (context) => const ProfileScreen();
/// Add admin routes without SessionGuardWrapper to prevent blocking
allRoutes['/admin'] = (context) => const AdminDashboard();
allRoutes['/admin/users'] = (context) => const UserManagement();
allRoutes['/admin/crops'] = (context) => const CropManagement();
allRoutes['/admin/community'] = (context) => const CommunityManagement();
/// Add field management route without SessionGuardWrapper
allRoutes['/field-management'] = (context) => const FieldManagementScreen();
/// Add other authenticated routes wrapped with SessionGuardWrapper
_authenticatedRoutes.forEach((route, builder) { _authenticatedRoutes.forEach((route, builder) {
allRoutes[route] = // Skip routes that are already added above
(context) => SessionGuardWrapper(child: builder(context)); if (route != '/profile' &&
route != '/admin' &&
route != '/admin/users' &&
route != '/admin/crops' &&
route != '/admin/community' &&
route != '/field-management') {
allRoutes[route] =
(context) => SessionGuardWrapper(child: builder(context));
}
}); });
return allRoutes; return allRoutes;

View File

@ -12,6 +12,8 @@ import 'package:tugas_akhir_supabase/services/user_presence_service.dart';
import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart'; import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart'; import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart';
import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart'
as intro;
// Tambahkan listener untuk hot reload // Tambahkan listener untuk hot reload
bool _hasDoneHotReloadSetup = false; bool _hasDoneHotReloadSetup = false;
@ -250,75 +252,30 @@ class _RealAppState extends State<RealApp> with WidgetsBindingObserver {
expired, expired,
) { ) {
debugPrint('App: Session expired event received: $expired'); debugPrint('App: Session expired event received: $expired');
if (expired && !_showingSessionExpiredDialog) { final currentUser = Supabase.instance.client.auth.currentUser;
if (expired && !_showingSessionExpiredDialog && currentUser != null) {
debugPrint('App: Showing session expired dialog from stream'); debugPrint('App: Showing session expired dialog from stream');
_showSessionExpiredDialog(); _showSessionExpiredDialog();
} }
}); });
// Mulai pemeriksaan sesi berkala yang lebih agresif, tapi hanya jika sudah tidak dalam initial launch // Mulai pemeriksaan sesi yang lebih agresif (DISABLED)
if (!_isInitialLaunch) { void startAggressiveSessionChecking() {
_startAggressiveSessionChecking(); debugPrint('App: Session monitoring DISABLED for permissive mode');
// Cancel any existing timer
_sessionCheckTimer?.cancel();
// No timer setup - completely disabled
debugPrint('App: No periodic session checks will run');
} }
} }
// Mulai pemeriksaan sesi yang lebih agresif // Check session validity on startup and periodically (DISABLED)
void _startAggressiveSessionChecking() {
debugPrint('App: Starting aggressive session checking');
// Batalkan timer yang ada jika ada
_sessionCheckTimer?.cancel();
// Periksa sesi setiap 15 detik
_sessionCheckTimer = Timer.periodic(Duration(seconds: 15), (timer) {
// Skip jika masih dalam fase initial launch
if (_isInitialLaunch) {
debugPrint('App: Skipping aggressive check during initial launch');
return;
}
// Periksa apakah pengguna sudah login terlebih dahulu
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint(
'App: No authenticated user, skipping aggressive session check',
);
return;
}
debugPrint('App: Running aggressive session check');
_checkSessionValidity();
});
}
// Check session validity on startup and periodically
Future<void> _checkSessionValidity() async { Future<void> _checkSessionValidity() async {
// Jangan periksa session selama initial launch phase // Session checking DISABLED for permissive mode
if (_isInitialLaunch) { debugPrint('App: Session checking DISABLED for permissive mode');
debugPrint('App: Skipping session check during initial launch'); return;
return;
}
try {
debugPrint('App: Checking session validity...');
// Periksa apakah pengguna sudah login terlebih dahulu
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint('App: No authenticated user found, skipping session check');
return;
}
final isValid = await SessionManager.isSessionValid();
debugPrint('App: Session validity check result: $isValid');
if (!isValid && !_showingSessionExpiredDialog) {
debugPrint('App: Session is invalid, showing expired dialog');
_showSessionExpiredDialog();
}
} catch (e) {
debugPrint('App: Error checking session validity: $e');
}
} }
@override @override
@ -375,86 +332,56 @@ class _RealAppState extends State<RealApp> with WidgetsBindingObserver {
} }
void _showSessionExpiredDialog() { void _showSessionExpiredDialog() {
debugPrint('App: Attempting to show session expired dialog'); // Session expired dialog DISABLED for permissive mode
debugPrint('App: Session expired dialog DISABLED for permissive mode');
return;
}
// Jangan tampilkan dialog jika masih dalam fase initial launch // Reset expired dialog state saat login/logout
if (_isInitialLaunch) { void _resetExpiredDialogState() {
debugPrint( _showingSessionExpiredDialog = false;
'App: Still in initial launch phase, skipping session expired dialog',
);
return;
}
if (_showingSessionExpiredDialog) {
debugPrint('App: Dialog already showing, skipping');
return;
}
_showingSessionExpiredDialog = true;
// Pastikan context tersedia
if (navigatorKey.currentContext == null) {
debugPrint('App: Navigator context not available, using delayed dialog');
// Coba lagi setelah beberapa saat
Future.delayed(Duration(milliseconds: 500), () {
if (mounted) _showSessionExpiredDialog();
});
_showingSessionExpiredDialog = false;
return;
}
debugPrint('App: Showing session expired dialog now');
showDialog(
context: navigatorKey.currentContext!,
barrierDismissible: false,
builder: (context) => const SessionExpiredDialog(),
).then((_) {
debugPrint('App: Session expired dialog closed');
_showingSessionExpiredDialog = false;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'TaniSMART', title: 'TaniSMART',
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
primaryColor: const Color(0xFF2E7D32), primarySwatch: Colors.green,
colorScheme: ColorScheme.fromSwatch( primaryColor: const Color(0xFF056839),
primarySwatch: Colors.green, colorScheme: ColorScheme.fromSeed(
accentColor: const Color(0xFF66BB6A), seedColor: const Color(0xFF056839),
brightness: Brightness.light,
), ),
scaffoldBackgroundColor: const Color.fromARGB(255, 255, 255, 255),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true, useMaterial3: true,
inputDecorationTheme: InputDecorationTheme( fontFamily: 'Poppins',
border: OutlineInputBorder( appBarTheme: const AppBarTheme(
borderRadius: BorderRadius.circular(12.0), backgroundColor: Color(0xFF056839),
borderSide: const BorderSide(color: Colors.black26, width: 1.5), foregroundColor: Colors.white,
), elevation: 0,
enabledBorder: OutlineInputBorder( centerTitle: true,
borderRadius: BorderRadius.circular(12.0), ),
borderSide: const BorderSide(color: Colors.black26, width: 1.5), elevatedButtonTheme: ElevatedButtonThemeData(
), style: ElevatedButton.styleFrom(
focusedBorder: OutlineInputBorder( backgroundColor: const Color(0xFF056839),
borderRadius: BorderRadius.circular(12.0), foregroundColor: Colors.white,
borderSide: const BorderSide(color: Colors.black, width: 2.5), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(12),
errorBorder: OutlineInputBorder( ),
borderRadius: BorderRadius.circular(12.0), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
borderSide: const BorderSide(color: Colors.red, width: 1.5), ),
), ),
focusedErrorBorder: OutlineInputBorder( inputDecorationTheme: InputDecorationTheme(
borderRadius: BorderRadius.circular(12.0), border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
borderSide: const BorderSide(color: Colors.red, width: 2.5), focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF056839), width: 2),
), ),
), ),
dividerColor: Colors.black,
), ),
routes: AppRoutes.routes, home: const intro.AnimatedIntroScreen(),
initialRoute: '/', routes: Map.from(AppRoutes.routes)..remove('/'),
// Add navigation observer to clear SnackBars when navigating // Add navigation observer to clear SnackBars when navigating
navigatorObservers: [ navigatorObservers: [
_SnackBarClearingNavigatorObserver(), _SnackBarClearingNavigatorObserver(),

View File

@ -131,7 +131,9 @@ class _AdminDashboardState extends State<AdminDashboard> {
Future<void> _loadDashboardStats() async { Future<void> _loadDashboardStats() async {
if (!mounted) return; if (!mounted) return;
setState(() => _isLoading = true); if (mounted) {
setState(() => _isLoading = true);
}
try { try {
debugPrint('🔍 Starting to load dashboard stats...'); debugPrint('🔍 Starting to load dashboard stats...');
@ -148,17 +150,51 @@ class _AdminDashboardState extends State<AdminDashboard> {
debugPrint('🔍 get_all_users response type: ${response.runtimeType}'); debugPrint('🔍 get_all_users response type: ${response.runtimeType}');
debugPrint('🔍 get_all_users response length: ${response.length}'); debugPrint('🔍 get_all_users response length: ${response.length}');
debugPrint('🔍 get_all_users response: $response');
// Convert to List<Map> // Handle different response types
final users = List<Map<String, dynamic>>.from(response); int userCount = 0;
if (response is List) {
userCount = response.length;
debugPrint('✅ Parsed ${userCount} users from List response');
} else if (response is Map) {
userCount = 1;
debugPrint('✅ Parsed 1 user from Map response');
} else {
debugPrint('⚠️ Unknown response type, trying to convert...');
try {
final users = List<Map<String, dynamic>>.from(response);
userCount = users.length;
debugPrint('✅ Converted to ${userCount} users');
} catch (e) {
debugPrint('❌ Failed to convert response: $e');
// Try to get length directly
try {
final length = response.length;
userCount = length ?? 0;
debugPrint('✅ Got length directly: $userCount');
} catch (e2) {
debugPrint('❌ Failed to get length directly: $e2');
userCount = 0;
}
}
}
if (mounted) { if (mounted) {
setState(() { setState(() {
_totalUsers = users.length; _totalUsers = userCount;
debugPrint('✅ Set _totalUsers to $userCount');
debugPrint( debugPrint(
'✅ Fetched ${users.length} users from get_all_users RPC', '🔍 User count verification: userCount=$userCount, _totalUsers=$_totalUsers',
); );
}); });
debugPrint('✅ setState called for userCount: $userCount');
// Verify the value was set
debugPrint(
'✅ Verification: _totalUsers after setState = $_totalUsers',
);
} else {
debugPrint('❌ Widget not mounted, cannot setState');
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error fetching users with get_all_users: $e'); debugPrint('❌ Error fetching users with get_all_users: $e');
@ -188,6 +224,12 @@ class _AdminDashboardState extends State<AdminDashboard> {
} }
} catch (fallbackError) { } catch (fallbackError) {
debugPrint('❌ Even fallback query failed: $fallbackError'); debugPrint('❌ Even fallback query failed: $fallbackError');
// Set a default value to prevent issues
if (mounted) {
setState(() {
_totalUsers = 0;
});
}
} }
} }
@ -207,6 +249,11 @@ class _AdminDashboardState extends State<AdminDashboard> {
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error fetching guides count: $e'); debugPrint('❌ Error fetching guides count: $e');
if (mounted) {
setState(() {
_totalGuides = 0;
});
}
} }
debugPrint( debugPrint(
@ -224,6 +271,11 @@ class _AdminDashboardState extends State<AdminDashboard> {
} }
} catch (e) { } catch (e) {
debugPrint('Error fetching news count: $e'); debugPrint('Error fetching news count: $e');
if (mounted) {
setState(() {
_totalNews = 0;
});
}
} }
// Fetch community messages count // Fetch community messages count
@ -237,16 +289,23 @@ class _AdminDashboardState extends State<AdminDashboard> {
} }
} catch (e) { } catch (e) {
debugPrint('Error fetching community posts count: $e'); debugPrint('Error fetching community posts count: $e');
if (mounted) {
setState(() {
_totalPosts = 0;
});
}
} }
setState(() { if (mounted) {
_isLoading = false; setState(() {
_lastUpdated = formatter.format(now); _isLoading = false;
debugPrint('🕒 Last updated: $_lastUpdated'); _lastUpdated = formatter.format(now);
debugPrint( debugPrint('🕒 Last updated: $_lastUpdated');
'📊 FINAL STATS: Users: $_totalUsers, Active: $_activeUsers, Guides: $_totalGuides, News: $_totalNews, Posts: $_totalPosts', debugPrint(
); '📊 FINAL STATS: Users: $_totalUsers, Active: $_activeUsers, Guides: $_totalGuides, News: $_totalNews, Posts: $_totalPosts',
}); );
});
}
} catch (e) { } catch (e) {
debugPrint('❌ Error loading dashboard stats: $e'); debugPrint('❌ Error loading dashboard stats: $e');
if (mounted) { if (mounted) {
@ -256,10 +315,14 @@ class _AdminDashboardState extends State<AdminDashboard> {
} }
Future<void> _checkAdminAccess() async { Future<void> _checkAdminAccess() async {
setState(() => _isLoading = true); debugPrint('DEBUG: Mulai cek akses admin...');
if (mounted) {
setState(() => _isLoading = true);
}
try { try {
final isAdmin = await _authServices.isAdmin(); final isAdmin = await _authServices.isAdmin();
debugPrint('DEBUG: Hasil cek isAdmin: $isAdmin');
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -268,29 +331,38 @@ class _AdminDashboardState extends State<AdminDashboard> {
}); });
} }
if (!isAdmin) { // Only kick out if definitely not admin and we're sure about it
if (isAdmin == false) {
debugPrint('DEBUG: User bukan admin, akan pop dan tampilkan snackbar');
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(); if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( Navigator.of(context).pop();
SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: const Text('Access denied. Admin privileges required.'), SnackBar(
backgroundColor: Colors.red.shade400, content: const Text(
behavior: SnackBarBehavior.floating, 'Access denied. Admin privileges required.',
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(8), backgroundColor: Colors.red.shade400,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
), ),
), );
); }
}); });
} else {
debugPrint('DEBUG: User adalah admin, melanjutkan...');
} }
} catch (e) { } catch (e) {
debugPrint('Error checking admin access: $e'); debugPrint('DEBUG: Error checking admin access: $e');
// Don't kick out on error, just show error message
if (mounted) { if (mounted) {
setState(() => _isLoading = false); setState(() => _isLoading = false);
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Error: ${e.toString()}'), content: Text('Error checking admin access: ${e.toString()}'),
backgroundColor: Colors.red.shade400, backgroundColor: Colors.orange.shade400,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
@ -299,6 +371,7 @@ class _AdminDashboardState extends State<AdminDashboard> {
); );
} }
} }
debugPrint('DEBUG: Selesai cek akses admin.');
} }
Future<void> _refreshDashboardData() async { Future<void> _refreshDashboardData() async {
@ -515,106 +588,112 @@ class _AdminDashboardState extends State<AdminDashboard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Define page content based on selected index try {
Widget pageContent; debugPrint('DEBUG: build() AdminDashboard dipanggil');
switch (_currentIndex) { // Define page content based on selected index
case 0: Widget pageContent;
pageContent = _buildOverviewTab(); switch (_currentIndex) {
break; case 0:
case 1: pageContent = _buildOverviewTab();
pageContent = const UserManagement(); break;
break; case 1:
case 2: pageContent = const UserManagement();
pageContent = const GuideManagement(); break;
break; case 2:
// case 3: // Removing Crops tab from admin pageContent = const GuideManagement();
// pageContent = const CropManagement(); break;
// break; // case 3: // Removing Crops tab from admin
case 3: // Updated index after removing Crops // pageContent = const CropManagement();
pageContent = const CommunityManagement(); // break;
break; case 3: // Updated index after removing Crops
case 4: // Updated index after removing Crops pageContent = const CommunityManagement();
pageContent = const NewsManagement(); break;
break; case 4: // Updated index after removing Crops
default: pageContent = const NewsManagement();
pageContent = _buildOverviewTab(); break;
} default:
pageContent = _buildOverviewTab();
}
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF5F7FA), backgroundColor: const Color(0xFFF5F7FA),
appBar: AppBar( appBar: AppBar(
title: const Text( title: const Text(
'Admin Dashboard', 'Admin Dashboard',
style: TextStyle(fontWeight: FontWeight.w600), style: TextStyle(fontWeight: FontWeight.w600),
),
centerTitle: true,
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
elevation: 2,
actions: [
IconButton(
icon: const Icon(Icons.exit_to_app),
onPressed: () {
Navigator.pop(context);
},
),
],
), ),
centerTitle: true, body:
backgroundColor: AppColors.primary, _isLoading
foregroundColor: Colors.white, ? const Center(child: CircularProgressIndicator())
elevation: 2, : !_isAdmin
actions: [ ? const Center(
IconButton( child: Text('Access denied. Admin privileges required.'),
icon: const Icon(Icons.exit_to_app), )
onPressed: () { : pageContent,
Navigator.pop(context); bottomNavigationBar: BottomNavigationBar(
}, currentIndex: _currentIndex,
), onTap: (index) {
], setState(() {
), _currentIndex = index;
body: });
_isLoading },
? const Center(child: CircularProgressIndicator()) type: BottomNavigationBarType.fixed,
: !_isAdmin backgroundColor: Colors.white,
? const Center( selectedItemColor: AppColors.primary,
child: Text('Access denied. Admin privileges required.'), unselectedItemColor: Colors.grey,
) selectedFontSize: 12,
: pageContent, unselectedFontSize: 12,
bottomNavigationBar: BottomNavigationBar( items: const [
currentIndex: _currentIndex, BottomNavigationBarItem(
onTap: (index) { icon: Icon(Icons.dashboard_outlined),
setState(() { activeIcon: Icon(Icons.dashboard),
_currentIndex = index; label: 'Overview',
}); ),
}, BottomNavigationBarItem(
type: BottomNavigationBarType.fixed, icon: Icon(Icons.people_outline),
backgroundColor: Colors.white, activeIcon: Icon(Icons.people),
selectedItemColor: AppColors.primary, label: 'Users',
unselectedItemColor: Colors.grey, ),
selectedFontSize: 12, BottomNavigationBarItem(
unselectedFontSize: 12, icon: Icon(Icons.book_outlined),
items: const [ activeIcon: Icon(Icons.book),
BottomNavigationBarItem( label: 'Guides',
icon: Icon(Icons.dashboard_outlined), ),
activeIcon: Icon(Icons.dashboard), // BottomNavigationBarItem(
label: 'Overview', // icon: Icon(Icons.agriculture_outlined),
), // activeIcon: Icon(Icons.agriculture),
BottomNavigationBarItem( // label: 'Crops',
icon: Icon(Icons.people_outline), // ),
activeIcon: Icon(Icons.people), BottomNavigationBarItem(
label: 'Users', icon: Icon(Icons.forum_outlined),
), activeIcon: Icon(Icons.forum),
BottomNavigationBarItem( label: 'Community',
icon: Icon(Icons.book_outlined), ),
activeIcon: Icon(Icons.book), BottomNavigationBarItem(
label: 'Guides', icon: Icon(Icons.newspaper_outlined),
), activeIcon: Icon(Icons.newspaper),
// BottomNavigationBarItem( label: 'News',
// icon: Icon(Icons.agriculture_outlined), ),
// activeIcon: Icon(Icons.agriculture), ],
// label: 'Crops', ),
// ), );
BottomNavigationBarItem( } catch (e, stack) {
icon: Icon(Icons.forum_outlined), debugPrint('DEBUG: Exception di build AdminDashboard: $e\n$stack');
activeIcon: Icon(Icons.forum), return Center(child: Text('Error: $e'));
label: 'Community', }
),
BottomNavigationBarItem(
icon: Icon(Icons.newspaper_outlined),
activeIcon: Icon(Icons.newspaper),
label: 'News',
),
],
),
);
} }
Widget _buildOverviewTab() { Widget _buildOverviewTab() {

View File

@ -454,7 +454,10 @@ class _KalenderTanamScreenState extends State<KalenderTanamScreen> {
height: 42, height: 42,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
Navigator.pushNamed(context, '/schedule-list'); Navigator.push(
context,
MaterialPageRoute(builder: (_) => const ScheduleListScreen()),
);
}, },
icon: const Icon(Icons.list_alt, size: 16, color: Colors.white), icon: const Icon(Icons.list_alt, size: 16, color: Colors.white),
label: Text( label: Text(
@ -483,7 +486,9 @@ class _KalenderTanamScreenState extends State<KalenderTanamScreen> {
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (_) => const FieldManagementScreen()), MaterialPageRoute(
builder: (_) => const FieldManagementScreen(),
),
).then((_) { ).then((_) {
_fetchFieldCount(); _fetchFieldCount();
}); });

View File

@ -10,6 +10,7 @@ import 'package:tugas_akhir_supabase/screens/calendar/location_picker_dialog.dar
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
@ -149,6 +150,9 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
'Lahan Konversi', 'Lahan Konversi',
]; ];
// Tambahkan MapController di _FieldManagementScreenState
final MapController _detailMapController = MapController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -714,18 +718,18 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
), ),
), ),
child: TextFormField( child: TextFormField(
controller: _locationController, controller: _locationController,
readOnly: true, readOnly: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Pilih lokasi di peta', hintText: 'Pilih lokasi di peta',
hintStyle: TextStyle( hintStyle: TextStyle(
color: Colors.white.withOpacity( color: Colors.white.withOpacity(
0.7, 0.7,
), ),
), ),
border: InputBorder.none, border: InputBorder.none,
prefixIcon: Icon( prefixIcon: Icon(
Icons.location_on, Icons.location_on,
color: Colors.white.withOpacity( color: Colors.white.withOpacity(
0.9, 0.9,
), ),
@ -734,8 +738,8 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
const EdgeInsets.symmetric( const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 12, vertical: 12,
), ),
), ),
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
), ),
@ -1959,32 +1963,118 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
], ],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: FlutterMap( child: Stack(
options: MapOptions(
center: LatLng(field.latitude!, field.longitude!),
zoom: 15.0,
interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
children: [ children: [
TileLayer( FlutterMap(
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', mapController: _detailMapController,
subdomains: ['a', 'b', 'c'], options: MapOptions(
userAgentPackageName: 'com.tanismart.tugas_akhir_supabase', center: LatLng(
), field.latitude!,
MarkerLayer( field.longitude!,
markers: [ ),
Marker( zoom: 18.0,
width: 40.0, interactiveFlags:
height: 40.0, InteractiveFlag
point: LatLng(field.latitude!, field.longitude!), .all, // Aktifkan semua gesture
child: Icon( ),
Icons.location_on, children: [
color: Colors.red, TileLayer(
size: 40, urlTemplate:
), 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName:
'com.tanismart.tugas_akhir_supabase',
),
MarkerLayer(
markers: [
Marker(
width: 40.0,
height: 40.0,
point: LatLng(
field.latitude!,
field.longitude!,
),
child: Icon(
Icons.location_on,
color: Colors.red,
size: 40,
),
),
],
), ),
], ],
), ),
FutureBuilder<String?>(
future: _getMapillaryThumbnail(
field.latitude!,
field.longitude!,
),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return Positioned(
bottom: 16,
left: 16,
child: Container(
width: 120,
height: 70,
color: Colors.grey[300],
child: const Center(
child:
CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
if (snapshot.hasData &&
snapshot.data != null) {
final thumbUrl = snapshot.data!;
return Positioned(
bottom: 16,
left: 16,
child: GestureDetector(
onTap: () async {
final url =
await _getMapillaryViewerUrl(
field.latitude!,
field.longitude!,
);
if (await canLaunch(url)) {
await launch(url);
}
},
child: ClipRRect(
borderRadius:
BorderRadius.circular(8),
child: Image.network(
thumbUrl,
width: 120,
height: 70,
fit: BoxFit.cover,
errorBuilder:
(
context,
error,
stack,
) => Container(
color:
Colors.grey[300],
width: 120,
height: 70,
child: Icon(
Icons
.image_not_supported,
),
),
),
),
),
);
}
return SizedBox.shrink();
},
),
], ],
), ),
), ),
@ -2526,6 +2616,39 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
throw Exception('Tidak dapat membuka URL: $url'); throw Exception('Tidak dapat membuka URL: $url');
} }
} }
Future<String?> _getMapillaryThumbnail(double lat, double lng) async {
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id,thumb_256_url&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
return data['data'][0]['thumb_256_url'] as String?;
}
}
return null;
}
Future<String> _getMapillaryViewerUrl(double lat, double lng) async {
// Cari id foto terdekat
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
final id = data['data'][0]['id'];
return 'https://www.mapillary.com/app/?pKey=$id&focus=photo';
}
}
// Fallback: buka mapillary di koordinat
return 'https://www.mapillary.com/app/?lat=$lat&lng=$lng&z=17.5';
}
} }
// Kelas untuk membuat pattern visual pada background hero card // Kelas untuk membuat pattern visual pada background hero card

View File

@ -3,6 +3,9 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:geocoding/geocoding.dart'; import 'package:geocoding/geocoding.dart';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:url_launcher/url_launcher.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class LocationResult { class LocationResult {
final String address; final String address;
@ -125,6 +128,37 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
_mapController.rotate(0.0); _mapController.rotate(0.0);
} }
Future<String?> _getMapillaryThumbnail(double lat, double lng) async {
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id,thumb_256_url&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
return data['data'][0]['thumb_256_url'] as String?;
}
}
return null;
}
Future<String> _getMapillaryViewerUrl(double lat, double lng) async {
const accessToken =
'MLY|24193737726928907|ec8688dd90eee0a7bd6231d281010d5c';
final url =
'https://graph.mapillary.com/images?access_token=$accessToken&fields=id&closeto=$lng,$lat&limit=1';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['data'] != null && data['data'].isNotEmpty) {
final id = data['data'][0]['id'];
return 'https://www.mapillary.com/app/?pKey=$id&focus=photo';
}
}
return 'https://www.mapillary.com/app/?lat=$lat&lng=$lng&z=17.5';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isFullscreen = widget.fullscreen; final isFullscreen = widget.fullscreen;
@ -235,9 +269,7 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
center: _selectedLatLng, center: _selectedLatLng,
zoom: 15.0, zoom: 15.0,
interactiveFlags: interactiveFlags:
InteractiveFlag.all & InteractiveFlag.all, // Aktifkan semua gesture
~InteractiveFlag.rotate, // Nonaktifkan gesture rotasi
// rotation: _rotation, // Tidak perlu rotasi
onTap: (tapPosition, point) async { onTap: (tapPosition, point) async {
setState(() { setState(() {
_selectedLatLng = point; _selectedLatLng = point;
@ -247,13 +279,11 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
point.longitude, point.longitude,
); );
}, },
// Hapus onPositionChanged
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: urlTemplate:
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: ['a', 'b', 'c'],
userAgentPackageName: 'com.tanismart.tugas_akhir_supabase', userAgentPackageName: 'com.tanismart.tugas_akhir_supabase',
), ),
MarkerLayer( MarkerLayer(
@ -272,6 +302,63 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
), ),
], ],
), ),
FutureBuilder<String?>(
future: _getMapillaryThumbnail(
_selectedLatLng.latitude,
_selectedLatLng.longitude,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Positioned(
bottom: 16,
left: 16,
child: Container(
width: 120,
height: 70,
color: Colors.grey[300],
child: const Center(
child: CircularProgressIndicator(strokeWidth: 2),
),
),
);
}
if (snapshot.hasData && snapshot.data != null) {
final thumbUrl = snapshot.data!;
return Positioned(
bottom: 16,
left: 16,
child: GestureDetector(
onTap: () async {
final url = await _getMapillaryViewerUrl(
_selectedLatLng.latitude,
_selectedLatLng.longitude,
);
if (await canLaunch(url)) {
await launch(url);
}
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
thumbUrl,
width: 120,
height: 70,
fit: BoxFit.cover,
errorBuilder:
(context, error, stack) => Container(
color: Colors.grey[300],
width: 120,
height: 70,
child: Icon(Icons.image_not_supported),
),
),
),
),
);
}
return SizedBox.shrink();
},
),
// Koordinat & Kompas di kanan atas // Koordinat & Kompas di kanan atas
Positioned( Positioned(
top: 12, top: 12,
@ -423,4 +510,3 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
); );
} }
} }

View File

@ -36,6 +36,9 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
String? _loadingGroupId; // Track grup yang sedang di-join/leave String? _loadingGroupId; // Track grup yang sedang di-join/leave
bool _isJoiningGroup = false; bool _isJoiningGroup = false;
// Tambahkan deklarasi channel realtime
RealtimeChannel? _groupRealtimeChannel;
// Add a key for the GroupChatScreen // Add a key for the GroupChatScreen
final GlobalKey<GroupChatScreenState> _groupChatKey = final GlobalKey<GroupChatScreenState> _groupChatKey =
GlobalKey<GroupChatScreenState>(); GlobalKey<GroupChatScreenState>();
@ -50,24 +53,16 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
); );
_tabController.addListener(_handleTabChange); _tabController.addListener(_handleTabChange);
// Gunakan timeout yang lebih pendek dan jangan langsung memanggil _preLoadDefaultGroup // Panggil langsung tanpa Future.delayed
// Beri waktu untuk widget selesai diinisialisasi _safeLoadGroups();
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
_safeLoadGroups();
}
});
// Make a direct database query to preload messages as early as possible
_preloadDefaultGroupMessages(); _preloadDefaultGroupMessages();
// Set a super-aggressive backup timer that will force refresh the chat
// even if all other mechanisms fail
Future.delayed(Duration(seconds: 2), () { Future.delayed(Duration(seconds: 2), () {
if (mounted) { if (mounted) {
_forceRefreshAllMessages(); _forceRefreshAllMessages();
} }
}); });
_setupGroupRealtimeSubscription();
} }
@override @override
@ -137,7 +132,8 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
} }
Future<void> _loadUserGroups() async { Future<void> _loadUserGroups() async {
if (_isLoadingGroups) { // Perbaiki: hanya return jika sedang loading dan data sudah ada
if (_isLoadingGroups && _userGroups.isNotEmpty) {
print('[DEBUG] Preventing multiple loads - load already in progress'); print('[DEBUG] Preventing multiple loads - load already in progress');
return; return;
} }
@ -327,6 +323,8 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
@override @override
void dispose() { void dispose() {
// Unsubscribe channel realtime
_groupRealtimeChannel?.unsubscribe();
_tabController.removeListener(_handleTabChange); _tabController.removeListener(_handleTabChange);
_tabController.dispose(); _tabController.dispose();
// Clean up any active subscriptions // Clean up any active subscriptions
@ -1014,6 +1012,34 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
} }
} }
// Tambahkan fungsi setup subscription realtime
void _setupGroupRealtimeSubscription() {
final supabase = Supabase.instance.client;
_groupRealtimeChannel = supabase.channel('public:groups_and_members');
_groupRealtimeChannel!
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'groups',
callback: (payload) {
print('[REALTIME] Groups table changed: $payload');
_loadUserGroups();
},
)
.onPostgresChanges(
event: PostgresChangeEvent.all,
schema: 'public',
table: 'group_members',
callback: (payload) {
print('[REALTIME] Group members table changed: $payload');
_loadUserGroups();
},
);
_groupRealtimeChannel!.subscribe();
}
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }

View File

@ -274,9 +274,7 @@ class GroupMessageService {
final userEmail = currentEmail ?? _supabase.auth.currentUser?.email ?? ''; final userEmail = currentEmail ?? _supabase.auth.currentUser?.email ?? '';
// Generate ID // Generate ID
final timestamp = DateTime.now().millisecondsSinceEpoch; final messageId = _uuid.v4(); // UUID valid
final messageId =
'grp-$timestamp-${userId.substring(0, userId.length.clamp(0, 6))}';
print('[DEBUG] Sending group message: $messageId'); print('[DEBUG] Sending group message: $messageId');
print( print(
@ -349,7 +347,7 @@ class GroupMessageService {
try { try {
// First try with all data including reply fields // First try with all data including reply fields
await _supabase.from('group_messages').insert(messageData); await _supabase.from('messages').insert(messageData);
print('[DEBUG] Group message saved successfully'); print('[DEBUG] Group message saved successfully');
saveSuccess = true; saveSuccess = true;
} catch (e) { } catch (e) {
@ -367,7 +365,7 @@ class GroupMessageService {
retryData.remove('reply_to_sender_username'); retryData.remove('reply_to_sender_username');
try { try {
await _supabase.from('group_messages').insert(retryData); await _supabase.from('messages').insert(retryData);
print('[DEBUG] Group message saved without reply data'); print('[DEBUG] Group message saved without reply data');
saveSuccess = true; saveSuccess = true;
} catch (retryError) { } catch (retryError) {
@ -560,7 +558,10 @@ class GroupMessageService {
await _supabase await _supabase
.rpc( .rpc(
'add_message_read_receipt', 'add_message_read_receipt',
params: {'p_message_id': messageId, 'p_user_id': currentUserId}, params: {
'p_message_id': messageId.toString(), // Ensure it's string
'p_user_id': currentUserId.toString(), // Ensure it's string
},
) )
.timeout( .timeout(
Duration(seconds: 2), Duration(seconds: 2),

View File

@ -1012,7 +1012,10 @@ class MessageService {
try { try {
await _supabase.rpc( await _supabase.rpc(
'add_message_read_receipt', 'add_message_read_receipt',
params: {'p_message_id': messageId, 'p_user_id': userId}, params: {
'p_message_id': messageId.toString(), // Ensure it's string
'p_user_id': userId.toString(), // Ensure it's string
},
); );
} catch (e) { } catch (e) {
print('[ERROR] Failed to mark message $messageId as read: $e'); print('[ERROR] Failed to mark message $messageId as read: $e');
@ -1034,8 +1037,9 @@ class MessageService {
await _supabase.rpc( await _supabase.rpc(
'add_message_read_receipt', 'add_message_read_receipt',
params: { params: {
'p_message_id': messageId, 'p_message_id': messageId.toString(), // Ensure it's string
'p_user_id': _supabase.auth.currentUser!.id, 'p_user_id':
_supabase.auth.currentUser!.id.toString(), // Ensure it's string
}, },
); );
} catch (e) { } catch (e) {

View File

@ -2266,7 +2266,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
'Biaya Pokok Produksi:', 'Biaya Pokok Produksi:',
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 10, fontSize: 9,
color: Colors.blue.shade700, color: Colors.blue.shade700,
), ),
), ),
@ -2276,7 +2276,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
'${currency.format(productionCostPerKg)}/kg', '${currency.format(productionCostPerKg)}/kg',
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 14, fontSize: 10,
color: Colors.blue.shade900, color: Colors.blue.shade900,
), ),
), ),
@ -2701,7 +2701,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
'Biaya Pokok Produksi', 'Biaya Pokok Produksi',
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 16, fontSize: 12,
color: Colors.grey.shade800, color: Colors.grey.shade800,
), ),
), ),

View File

@ -17,8 +17,7 @@ class ProfileScreen extends StatefulWidget {
_ProfileScreenState createState() => _ProfileScreenState(); _ProfileScreenState createState() => _ProfileScreenState();
} }
class _ProfileScreenState extends State<ProfileScreen> class _ProfileScreenState extends State<ProfileScreen> {
with SessionCheckerMixin {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController(); final _usernameController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
@ -39,20 +38,27 @@ class _ProfileScreenState extends State<ProfileScreen>
double _averageYield = 0; double _averageYield = 0;
final String _mostPlantedCrop = '-'; final String _mostPlantedCrop = '-';
bool _isAdmin = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initSessionChecking(); // Initialize session checking debugPrint('ProfileScreen: initState called');
_initializeProfile();
_checkAdminStatus(); // Get user immediately and load profile
_refreshUserSession(); _user = _supabase.auth.currentUser;
debugPrint('ProfileScreen: User from Supabase: ${_user?.id}');
if (_user != null) {
_loadProfile();
_loadStatistics();
} else {
setState(() {
_isLoading = false;
});
}
} }
@override @override
void dispose() { void dispose() {
disposeSessionChecking(); // Clean up session checking
_usernameController.dispose(); _usernameController.dispose();
_emailController.dispose(); _emailController.dispose();
_phoneController.dispose(); _phoneController.dispose();
@ -65,34 +71,7 @@ class _ProfileScreenState extends State<ProfileScreen>
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
// Refresh admin status setiap kali halaman dimuat ulang // Refresh admin status setiap kali halaman dimuat ulang
_checkAdminStatus(); // _checkAdminStatus(); // Removed as per new_code
}
Future<void> _initializeProfile() async {
// Update user activity
await updateUserActivity();
// Check session validity first
final isAuthenticated = SessionManager.isAuthenticated;
if (!isAuthenticated) {
debugPrint('Profile: User not authenticated or session expired');
setState(() {
_user = null;
_isLoading = false;
});
return;
}
_user = _supabase.auth.currentUser;
if (_user != null) {
await _loadProfile();
await _loadStatistics();
} else {
debugPrint('Profile: No current user found');
setState(() {
_isLoading = false;
});
}
} }
Future<void> _loadProfile() async { Future<void> _loadProfile() async {
@ -315,56 +294,48 @@ class _ProfileScreenState extends State<ProfileScreen>
} }
Future<void> _signOut() async { Future<void> _signOut() async {
await _supabase.auth.signOut();
if (mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
}
Future<void> _checkAdminStatus() async {
try { try {
final authServices = GetIt.instance<AuthServices>(); final authServices = GetIt.instance<AuthServices>();
final isAdmin = await authServices.isAdmin(); await authServices.signOut();
debugPrint('ProfileScreen: isAdmin check result: $isAdmin');
if (mounted) { if (mounted) {
setState(() { Navigator.of(context).pushReplacementNamed('/login');
_isAdmin = isAdmin;
});
} }
} catch (e) { } catch (e) {
debugPrint('Error checking admin status: $e'); debugPrint('Error during sign out: $e');
// Fallback to direct sign out
await _supabase.auth.signOut();
if (mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
} }
} }
Future<void> _refreshUserSession() async { // Simple admin check without complex session management
Future<bool> _isUserAdmin() async {
try { try {
// Update user activity timestamp if (_user == null) return false;
await updateUserActivity();
// Refresh Supabase session
final authServices = GetIt.instance<AuthServices>(); final authServices = GetIt.instance<AuthServices>();
await authServices.refreshSession(); return await authServices.isAdmin();
debugPrint('Session refreshed in ProfileScreen');
// Cek ulang status admin setelah refresh session
await _checkAdminStatus();
// Check session validity
await checkSessionStatus();
} catch (e) { } catch (e) {
debugPrint('Error refreshing session in ProfileScreen: $e'); debugPrint('Error checking admin status: $e');
return false;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint(
'ProfileScreen: build called, _user: ${_user != null}, _isLoading: $_isLoading',
);
if (_user == null) { if (_user == null) {
debugPrint('ProfileScreen: Showing no user screen');
return _buildNoUserScreen(); return _buildNoUserScreen();
} }
if (_isLoading) { if (_isLoading) {
debugPrint('ProfileScreen: Showing loading screen');
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), backgroundColor: const Color(0xFFF8F9FA),
body: Center( body: Center(
@ -375,6 +346,7 @@ class _ProfileScreenState extends State<ProfileScreen>
); );
} }
debugPrint('ProfileScreen: Showing main profile screen');
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF8F9FA), backgroundColor: const Color(0xFFF8F9FA),
appBar: _buildAppBar(), appBar: _buildAppBar(),
@ -407,29 +379,57 @@ class _ProfileScreenState extends State<ProfileScreen>
), ),
actions: [ actions: [
// Tombol admin dengan tooltip yang sesuai // Tombol admin dengan tooltip yang sesuai
IconButton( FutureBuilder<bool>(
icon: Container( future: _isUserAdmin(),
padding: const EdgeInsets.all(8), builder: (context, snapshot) {
decoration: BoxDecoration( final isAdmin = snapshot.data ?? false;
color: _isAdmin ? Colors.blue[100] : Colors.grey[100], return IconButton(
shape: BoxShape.circle, icon: Container(
), padding: const EdgeInsets.all(8),
child: Icon( decoration: BoxDecoration(
Icons.admin_panel_settings, color: isAdmin ? Colors.blue[100] : Colors.grey[100],
size: 18, shape: BoxShape.circle,
color: _isAdmin ? Colors.blue[700] : Colors.grey[700], ),
), child: Icon(
), Icons.admin_panel_settings,
onPressed: () { size: 18,
// Jika admin, buka dashboard admin color: isAdmin ? Colors.blue[700] : Colors.grey[700],
if (_isAdmin) { ),
Navigator.of(context).pushNamed('/admin'); ),
} else { onPressed: () async {
// Jika bukan admin, tampilkan dialog untuk mengelola role // Double check admin status before allowing access
_showRoleManagementDialog(); if (_user != null) {
} final authServices = GetIt.instance<AuthServices>();
final isAdmin = await authServices.isAdmin();
if (isAdmin) {
// Jika admin, buka dashboard admin
if (mounted) {
Navigator.of(context).pushNamed('/admin');
}
} else {
// Jika bukan admin, tampilkan dialog untuk mengelola role
if (mounted) {
_showRoleManagementDialog();
}
}
} else {
// User not logged in, show login prompt
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Silakan login terlebih dahulu untuk mengakses fitur admin',
),
backgroundColor: Colors.orange,
),
);
}
}
},
tooltip: isAdmin ? 'Kelola Admin' : 'Akses Admin',
);
}, },
tooltip: _isAdmin ? 'Kelola Admin' : 'Akses Admin',
), ),
IconButton( IconButton(
icon: Container( icon: Container(
@ -553,13 +553,12 @@ class _ProfileScreenState extends State<ProfileScreen>
); );
} }
Future<void> _showRoleManagementDialog() async { void _showRoleManagementDialog() async {
if (_user == null) return; // Check admin status first
final isAdmin = await _isUserAdmin();
final authServices = GetIt.instance<AuthServices>();
// Jika bukan admin, tampilkan pesan error // Jika bukan admin, tampilkan pesan error
if (!_isAdmin) { if (!isAdmin) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Anda tidak memiliki akses untuk mengelola role'), content: Text('Anda tidak memiliki akses untuk mengelola role'),
@ -569,9 +568,6 @@ class _ProfileScreenState extends State<ProfileScreen>
// Debug: Tampilkan user ID // Debug: Tampilkan user ID
debugPrint('Current user ID: ${_user?.id}'); debugPrint('Current user ID: ${_user?.id}');
// Coba refresh status admin
_checkAdminStatus();
return; return;
} }
@ -645,6 +641,7 @@ class _ProfileScreenState extends State<ProfileScreen>
if (isDowngradingToUser) { if (isDowngradingToUser) {
// Periksa jumlah admin yang ada // Periksa jumlah admin yang ada
final authServices = GetIt.instance<AuthServices>();
final adminCount = await authServices.countAdmins(); final adminCount = await authServices.countAdmins();
debugPrint('Current admin count: $adminCount'); debugPrint('Current admin count: $adminCount');
@ -766,7 +763,7 @@ class _ProfileScreenState extends State<ProfileScreen>
} }
// Refresh status admin // Refresh status admin
await _checkAdminStatus(); // _checkAdminStatus(); // Removed as per new_code
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@ -213,28 +213,9 @@ class AuthServices {
return false; return false;
} }
// Gunakan fungsi is_admin_no_rls untuk menghindari infinite recursion // First try: cek langsung dari tabel tanpa RLS
final response = await _supabase.rpc(
'is_admin_no_rls',
params: {'input_user_id': userId},
);
debugPrint('Admin check direct response: $response');
// Response dari RPC akan berupa boolean
final isAdmin = response == true;
debugPrint('Is admin: $isAdmin');
return isAdmin;
} catch (e) {
debugPrint('Error checking admin status: $e');
// Fallback: cek langsung dari tabel tanpa RLS
try { try {
final userId = getCurrentUserId(); debugPrint('Trying direct table query for admin check...');
if (userId == null) return false;
// Query sederhana tanpa menggunakan RPC
final response = final response =
await _supabase await _supabase
.from('user_roles') .from('user_roles')
@ -244,12 +225,53 @@ class AuthServices {
.maybeSingle(); .maybeSingle();
final isAdmin = response != null; final isAdmin = response != null;
debugPrint('Fallback admin check result: $isAdmin'); debugPrint('Direct table query result: $isAdmin');
return isAdmin; if (isAdmin) return true;
} catch (fallbackError) { } catch (directError) {
debugPrint('Fallback error: $fallbackError'); debugPrint('Direct table query failed: $directError');
return false;
} }
// Second try: gunakan fungsi is_admin_no_rls untuk menghindari infinite recursion
try {
debugPrint('Trying RPC function for admin check...');
final response = await _supabase.rpc(
'is_admin_no_rls',
params: {'input_user_id': userId},
);
debugPrint('Admin check RPC response: $response');
// Response dari RPC akan berupa boolean
final isAdmin = response == true;
debugPrint('RPC admin check result: $isAdmin');
return isAdmin;
} catch (rpcError) {
debugPrint('RPC function failed: $rpcError');
}
// Third try: cek dengan query yang lebih sederhana
try {
debugPrint('Trying simple query for admin check...');
final response = await _supabase
.from('user_roles')
.select('*')
.eq('user_id', userId)
.eq('role', 'admin')
.limit(1);
final isAdmin = response.isNotEmpty;
debugPrint('Simple query result: $isAdmin');
return isAdmin;
} catch (simpleError) {
debugPrint('Simple query failed: $simpleError');
}
// If all methods fail, return false
debugPrint('All admin check methods failed, returning false');
return false;
} catch (e) {
debugPrint('Error checking admin status: $e');
return false;
} }
} }

View File

@ -11,7 +11,8 @@ class SessionManager {
static const String _lastActiveTimeKey = 'last_active_time'; static const String _lastActiveTimeKey = 'last_active_time';
static const String _lastUserInteractionKey = 'last_user_interaction'; static const String _lastUserInteractionKey = 'last_user_interaction';
static const String _sessionStateKey = 'session_state'; static const String _sessionStateKey = 'session_state';
static const int _sessionTimeoutMinutes = 30; static const int _sessionTimeoutMinutes =
480; // Increased to 8 hours for permissive mode
static Timer? _sessionCheckTimer; static Timer? _sessionCheckTimer;
static Timer? _presenceUpdateTimer; static Timer? _presenceUpdateTimer;
@ -103,16 +104,27 @@ class SessionManager {
debugPrint('Session: User login status set to: $isLoggedIn'); debugPrint('Session: User login status set to: $isLoggedIn');
if (isLoggedIn) { if (isLoggedIn) {
_setSessionExpired(false); _setSessionExpired(false); // Pastikan expired direset!
_isSessionExpired = false;
_sessionExpiredController.add(false); // Kirim event expired false
updateLastUserInteraction(); updateLastUserInteraction();
_startSessionMonitoring(); _startSessionMonitoring();
_startPresenceUpdates(); _startPresenceUpdates();
} else { } else {
_stopSessionMonitoring(); _stopSessionMonitoring();
_stopPresenceUpdates(); _stopPresenceUpdates();
_setSessionExpired(false); // Reset expired saat logout juga!
_isSessionExpired = false;
_sessionExpiredController.add(false);
} }
} }
// Tambahkan fungsi untuk reset expired state secara manual
static void resetExpiredState() {
_isSessionExpired = false;
_sessionExpiredController.add(false);
}
// Start periodic presence updates // Start periodic presence updates
static void _startPresenceUpdates() { static void _startPresenceUpdates() {
_stopPresenceUpdates(); // Stop any existing timer _stopPresenceUpdates(); // Stop any existing timer
@ -139,162 +151,22 @@ class SessionManager {
_presenceUpdateTimer = null; _presenceUpdateTimer = null;
} }
// Check if session is valid with improved logic // Check if session is valid with extremely permissive logic
static Future<bool> isSessionValid() async { static Future<bool> isSessionValid() async {
debugPrint('Session: Checking session validity...'); debugPrint('Session: Checking session validity (PERMISSIVE MODE)...');
// Jika aplikasi baru saja diluncurkan, asumsikan sesi valid untuk menghindari dialog terlalu dini // Check if user is actually logged in
if (_isAppJustLaunched) { final currentUser = Supabase.instance.client.auth.currentUser;
debugPrint('Session: App just launched, assuming session valid'); final currentSession = Supabase.instance.client.auth.currentSession;
return true;
}
// If session is already marked as expired, return false immediately if (currentUser == null || currentSession == null) {
if (_isSessionExpired) { debugPrint('Session: No user or session found, returning false');
debugPrint('Session: Session already marked as expired');
return false; return false;
} }
if (!_hasLoggedInUser) { // Always return true for permissive mode if user is logged in
debugPrint('Session: No logged in user, skipping session validity check'); debugPrint('Session: PERMISSIVE MODE - User logged in, returning true');
return true; return true;
}
if (_isCheckingSession) {
debugPrint('Session: Already checking session, returning current status');
return !_isSessionExpired;
}
_isCheckingSession = true;
try {
User? currentUser;
Session? currentSession;
try {
currentUser = Supabase.instance.client.auth.currentUser;
currentSession = Supabase.instance.client.auth.currentSession;
debugPrint('Session: Supabase user: ${currentUser?.id ?? "null"}');
debugPrint(
'Session: Supabase session: ${currentSession != null ? "exists" : "null"}',
);
if (currentSession != null) {
final expiresAt = currentSession.expiresAt;
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
debugPrint(
'Session: Token expires at: $expiresAt, current time: $now',
);
}
} catch (e) {
debugPrint('Session: Error accessing Supabase auth - $e');
_isCheckingSession = false;
return !_isSessionExpired;
}
// If no user or session, session is invalid
if (currentUser == null || currentSession == null) {
debugPrint('Session: No valid Supabase user or session found');
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
SharedPreferences? prefs = await _getSafeSharedPreferences();
final sessionState = prefs?.getString(_sessionStateKey);
if (sessionState == null || sessionState == 'inactive') {
debugPrint(
'Session: No session state found or inactive, marking as expired',
);
_hasLoggedInUser = false;
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
// Check if Supabase token is expired
final sessionExpiry = currentSession.expiresAt;
if (sessionExpiry != null &&
sessionExpiry <= DateTime.now().millisecondsSinceEpoch ~/ 1000) {
debugPrint('Session: Supabase session token expired');
_hasLoggedInUser = false;
_setSessionExpired(true);
_isCheckingSession = false;
return false;
}
if (prefs == null) {
debugPrint(
'Session: Could not access SharedPreferences, assuming valid',
);
_isCheckingSession = false;
return !_isSessionExpired;
}
final lastInteractionTime = prefs.getInt(_lastUserInteractionKey);
debugPrint(
'Session: Last user interaction time from prefs: $lastInteractionTime',
);
if (lastInteractionTime == null) {
debugPrint(
'Session: No user interaction timestamp found, setting current time',
);
await updateLastUserInteraction();
_isCheckingSession = false;
return !_isSessionExpired;
}
final lastInteraction = DateTime.fromMillisecondsSinceEpoch(
lastInteractionTime,
);
final now = DateTime.now();
debugPrint('Session: Last user interaction time: $lastInteraction');
debugPrint('Session: Current time: $now');
if (lastInteraction.isAfter(now)) {
debugPrint('Session: Invalid timestamp detected (future date)');
await updateLastUserInteraction();
_isCheckingSession = false;
return !_isSessionExpired;
}
final difference = now.difference(lastInteraction);
final differenceInMinutes = difference.inMinutes;
final differenceInSeconds = difference.inSeconds;
debugPrint(
'Session: Time difference: $differenceInMinutes minutes, $differenceInSeconds seconds',
);
debugPrint(
'Session: Timeout limit: $_sessionTimeoutMinutes minutes (${_sessionTimeoutMinutes * 60} seconds)',
);
final timeoutInSeconds = _sessionTimeoutMinutes * 60;
final isValid = differenceInSeconds < timeoutInSeconds;
if (!isValid) {
debugPrint(
'Session: TIMEOUT - Sesi kedaluwarsa setelah $differenceInMinutes menit ($differenceInSeconds detik) tidak aktif. Batas waktu: $_sessionTimeoutMinutes menit',
);
_setSessionExpired(true);
} else {
_setSessionExpired(false);
debugPrint(
'Session: VALID - Terakhir aktif $differenceInMinutes menit ($differenceInSeconds detik) yang lalu. Batas waktu: $_sessionTimeoutMinutes menit',
);
}
_isCheckingSession = false;
return isValid;
} catch (e) {
debugPrint('Session: Error checking validity - $e');
_isCheckingSession = false;
return !_isSessionExpired;
}
} }
// BARU: Update timestamp interaksi pengguna terakhir // BARU: Update timestamp interaksi pengguna terakhir
@ -425,42 +297,10 @@ class SessionManager {
'Session: Checking session validity after returning to foreground', 'Session: Checking session validity after returning to foreground',
); );
SharedPreferences? prefs = await _getSafeSharedPreferences(); // Use permissive mode - don't check timeout
if (prefs != null) {
final lastInteractionTime = prefs.getInt(_lastUserInteractionKey);
if (lastInteractionTime != null) {
final lastInteraction = DateTime.fromMillisecondsSinceEpoch(
lastInteractionTime,
);
final now = DateTime.now();
final difference = now.difference(lastInteraction);
final differenceInSeconds = difference.inSeconds;
final timeoutInSeconds = _sessionTimeoutMinutes * 60;
debugPrint('Session: Last user interaction time: $lastInteraction');
debugPrint('Session: Current time: $now');
debugPrint(
'Session: Time difference: ${difference.inMinutes} minutes, $differenceInSeconds seconds',
);
debugPrint(
'Session: Timeout limit: $_sessionTimeoutMinutes minutes ($timeoutInSeconds seconds)',
);
if (differenceInSeconds >= timeoutInSeconds) {
debugPrint(
'Session: TIMEOUT after foreground - Inactive for $differenceInSeconds seconds (limit: $timeoutInSeconds)',
);
_setSessionExpired(true);
await clearSession();
return;
}
}
}
final isValid = await isSessionValid(); final isValid = await isSessionValid();
if (!isValid) { if (!isValid) {
debugPrint('Session: Expired while in background'); debugPrint('Session: Session invalid after background');
await clearSession(); await clearSession();
Future.delayed(const Duration(milliseconds: 500), () { Future.delayed(const Duration(milliseconds: 500), () {
_setSessionExpired(true); _setSessionExpired(true);
@ -477,6 +317,11 @@ class SessionManager {
// Set session expired state and notify listeners // Set session expired state and notify listeners
static void _setSessionExpired(bool value) { static void _setSessionExpired(bool value) {
// Jangan trigger expired jika user memang belum login
if (!_hasLoggedInUser) {
debugPrint('Session: Not logged in, skip setting expired state');
return;
}
if (_isSessionExpired != value) { if (_isSessionExpired != value) {
_isSessionExpired = value; _isSessionExpired = value;
debugPrint('Session: Setting expired state to $value'); debugPrint('Session: Setting expired state to $value');
@ -510,12 +355,21 @@ class SessionManager {
final isValid = final isValid =
currentUser != null && currentSession != null && !_isSessionExpired; currentUser != null && currentSession != null && !_isSessionExpired;
// Only update state if there's a significant change, and be more conservative
if (!isValid && _hasLoggedInUser) { if (!isValid && _hasLoggedInUser) {
_hasLoggedInUser = false; // Double check before changing state to avoid race conditions
_setSessionExpired(true); final doubleCheckUser = Supabase.instance.client.auth.currentUser;
debugPrint( final doubleCheckSession = Supabase.instance.client.auth.currentSession;
'Session: Updated login status to false based on authentication check',
); if (doubleCheckUser == null || doubleCheckSession == null) {
debugPrint(
'Session: User authentication state changed, updating flags',
);
_hasLoggedInUser = false;
_setSessionExpired(true);
} else {
debugPrint('Session: User still exists, keeping authentication state');
}
} }
return isValid; return isValid;
@ -527,6 +381,8 @@ class SessionManager {
_stopSessionMonitoring(); _stopSessionMonitoring();
_setSessionExpired(true); _setSessionExpired(true);
_hasLoggedInUser = false; _hasLoggedInUser = false;
_isSessionExpired = false;
_sessionExpiredController.add(false);
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.remove(_lastActiveTimeKey); await prefs.remove(_lastActiveTimeKey);
@ -540,6 +396,8 @@ class SessionManager {
debugPrint('Session: Error during cleanup - $e'); debugPrint('Session: Error during cleanup - $e');
_setSessionExpired(true); _setSessionExpired(true);
_hasLoggedInUser = false; _hasLoggedInUser = false;
_isSessionExpired = false;
_sessionExpiredController.add(false);
} }
} }
@ -573,61 +431,14 @@ class SessionManager {
} }
} }
// Start session monitoring (check every 30 seconds) // Start session monitoring (DISABLED for permissive mode)
static void _startSessionMonitoring() { static void _startSessionMonitoring() {
_stopSessionMonitoring(); _stopSessionMonitoring();
debugPrint( debugPrint('Session: Session monitoring DISABLED for permissive mode');
'Session: Starting monitoring with timeout: $_sessionTimeoutMinutes minutes',
);
_sessionCheckTimer = Timer.periodic(const Duration(seconds: 15), ( // No timer setup - completely disabled
timer, debugPrint('Session: No periodic checks will run');
) async {
debugPrint('Session: Running periodic check...');
if (_isCheckingSession) {
debugPrint('Session: Skipping periodic check (already checking)');
return;
}
_isCheckingSession = true;
try {
bool isValid = false;
try {
isValid = await isSessionValid().timeout(
const Duration(seconds: 3),
onTimeout: () {
debugPrint('Session: Validity check timed out');
return true;
},
);
} catch (e) {
debugPrint('Session: Error during periodic check - $e');
isValid = true;
}
if (!isValid) {
debugPrint('Session: Expired during periodic check');
await clearSession();
_setSessionExpired(true);
_stopSessionMonitoring();
}
} catch (e) {
debugPrint('Session: Error during periodic check - $e');
} finally {
_isCheckingSession = false;
}
});
debugPrint('Session: Monitoring started');
Future.delayed(Duration(seconds: 5), () async {
debugPrint('Session: Immediate check after monitoring start');
await isSessionValid();
});
} }
// Stop session monitoring // Stop session monitoring

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,65 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
import 'package:tugas_akhir_supabase/services/session_manager.dart'; import 'package:tugas_akhir_supabase/services/session_manager.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
/// Mixin untuk memeriksa status session di halaman-halaman utama aplikasi /// Mixin untuk memeriksa status session di halaman-halaman utama aplikasi
mixin SessionCheckerMixin<T extends StatefulWidget> on State<T> { mixin SessionCheckerMixin<T extends StatefulWidget> on State<T> {
bool _isCheckingSession = false; bool _isCheckingSession = false;
/// Initialize session checking
void initSessionChecking() {
debugPrint('SessionChecker: Initializing session checking');
// Check session on init
WidgetsBinding.instance.addPostFrameCallback((_) {
checkSessionStatus();
});
}
/// Dispose session checking
void disposeSessionChecking() {
debugPrint('SessionChecker: Disposing session checking');
stopPeriodicSessionCheck();
}
/// Update user activity
Future<void> updateUserActivity() async {
try {
await SessionManager.updateLastUserInteraction();
debugPrint('SessionChecker: User activity updated');
} catch (e) {
debugPrint('SessionChecker: Error updating user activity: $e');
}
}
/// Periksa status session saat halaman dibuka /// Periksa status session saat halaman dibuka
Future<void> checkSessionStatus() async { Future<void> checkSessionStatus() async {
if (_isCheckingSession) return; if (_isCheckingSession) return;
_isCheckingSession = true; _isCheckingSession = true;
try { try {
// Jangan cek session jika user belum login
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) {
debugPrint('SessionChecker: No current user, skipping session check');
_isCheckingSession = false;
return;
}
// Only check session if user is actually logged in
final currentSession = Supabase.instance.client.auth.currentSession;
if (currentSession == null) {
debugPrint(
'SessionChecker: No current session, skipping session check',
);
_isCheckingSession = false;
return;
}
debugPrint(
'SessionChecker: Checking session for user: ${currentUser.id}',
);
// Tambahkan timeout untuk mencegah blocking // Tambahkan timeout untuk mencegah blocking
final isValid = await SessionManager.isSessionValid().timeout( final isValid = await SessionManager.isSessionValid().timeout(
const Duration(seconds: 3), const Duration(seconds: 3),
@ -21,52 +69,54 @@ mixin SessionCheckerMixin<T extends StatefulWidget> on State<T> {
}, },
); );
if (!isValid && mounted) { debugPrint('SessionChecker: Session validity result: $isValid');
// Session tidak valid, update timestamp untuk mencegah pemanggilan berulang
await SessionManager.updateLastActiveTime().timeout( // Update user interaction timestamp
const Duration(seconds: 2), await SessionManager.updateLastUserInteraction();
onTimeout: () {
debugPrint('SessionChecker: Update activity timed out'); if (!isValid) {
return; debugPrint(
}, 'SessionChecker: Session invalid, but continuing in permissive mode',
); );
// In permissive mode, don't force logout
return;
} }
debugPrint('SessionChecker: Session is valid');
} catch (e) { } catch (e) {
debugPrint('SessionChecker: Error checking session - $e'); debugPrint('SessionChecker: Error checking session: $e');
// In permissive mode, don't force logout on error
} finally { } finally {
_isCheckingSession = false; _isCheckingSession = false;
} }
} }
/// Perbarui timestamp aktivitas pengguna /// Periksa session secara berkala (DISABLED untuk permissive mode)
Future<void> updateUserActivity() async { void startPeriodicSessionCheck() {
try { debugPrint(
// Tambahkan timeout untuk mencegah blocking 'SessionChecker: Periodic session check DISABLED for permissive mode',
await SessionManager.updateLastActiveTime().timeout( );
const Duration(seconds: 2), // No periodic checks in permissive mode
onTimeout: () {
debugPrint('SessionChecker: Update activity timed out');
return;
},
);
} catch (e) {
debugPrint('SessionChecker: Error updating activity - $e');
}
} }
/// Panggil di initState untuk memeriksa session /// Stop periodic session check
void initSessionChecking() { void stopPeriodicSessionCheck() {
// Periksa session setelah widget dibangun dengan delay debugPrint('SessionChecker: Stopping periodic session check');
// untuk mencegah terlalu banyak operasi di startup // Nothing to stop in permissive mode
Future.delayed(Duration(milliseconds: 500), () { }
if (mounted) {
checkSessionStatus(); @override
} void initState() {
super.initState();
// Check session on init
WidgetsBinding.instance.addPostFrameCallback((_) {
checkSessionStatus();
}); });
} }
/// Panggil di dispose untuk membersihkan resource @override
void disposeSessionChecking() { void dispose() {
// Tidak ada resource yang perlu dibersihkan stopPeriodicSessionCheck();
super.dispose();
} }
} }

View File

@ -4,7 +4,7 @@ import 'package:tugas_akhir_supabase/services/session_manager.dart';
import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart'; import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart';
/// A widget that enforces session validation for authenticated routes. /// A widget that enforces session validation for authenticated routes.
/// ///
/// This widget should wrap any screen that requires authentication. /// This widget should wrap any screen that requires authentication.
/// It will automatically check if the session is valid and redirect to login /// It will automatically check if the session is valid and redirect to login
/// if the session has expired. /// if the session has expired.
@ -13,10 +13,10 @@ class SessionGuardWrapper extends StatefulWidget {
final bool enforceAuthentication; final bool enforceAuthentication;
const SessionGuardWrapper({ const SessionGuardWrapper({
Key? key, super.key,
required this.child, required this.child,
this.enforceAuthentication = true, this.enforceAuthentication = true,
}) : super(key: key); });
@override @override
State<SessionGuardWrapper> createState() => _SessionGuardWrapperState(); State<SessionGuardWrapper> createState() => _SessionGuardWrapperState();
@ -26,81 +26,101 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
bool _isCheckingSession = false; bool _isCheckingSession = false;
bool _sessionExpired = false; bool _sessionExpired = false;
bool _showingDialog = false; bool _showingDialog = false;
bool _hasInitialized = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Check session on widget initialization // Initialize with a delay to prevent race conditions
if (widget.enforceAuthentication) { WidgetsBinding.instance.addPostFrameCallback((_) {
_checkSession(); if (mounted && !_hasInitialized) {
} _hasInitialized = true;
// Check session on widget initialization
if (widget.enforceAuthentication) {
_checkSession();
}
}
});
// Tambahkan listener untuk dismiss dialog jika user login/logout
Supabase.instance.client.auth.onAuthStateChange.listen((event) {
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser != null) {
setState(() {
_sessionExpired = false;
_showingDialog = false;
});
// Pop dialog jika masih terbuka
if (Navigator.canPop(context)) {
Navigator.of(
context,
rootNavigator: true,
).popUntil((route) => route.isFirst);
}
}
});
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
// Check session when dependencies change (e.g., after navigation) // Check session when dependencies change (e.g., after navigation)
if (widget.enforceAuthentication && !_isCheckingSession) { // But only if we haven't initialized yet or if there's a significant change
_checkSession(); if (widget.enforceAuthentication &&
!_isCheckingSession &&
_hasInitialized) {
// Add a small delay to prevent race conditions during navigation
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_checkSession();
}
});
} }
} }
Future<void> _checkSession() async { Future<void> _checkSession() async {
// Skip check if not enforcing authentication // Session checking DISABLED for permissive mode
if (!widget.enforceAuthentication) return; debugPrint('SessionGuard: Session checking DISABLED for permissive mode');
return;
// Skip check if already checking
if (_isCheckingSession) return;
// Skip check if no user is logged in
final currentUser = Supabase.instance.client.auth.currentUser;
if (currentUser == null) return;
_isCheckingSession = true;
try {
// Check if session is already marked as expired
if (SessionManager.isExpired) {
if (mounted && !_sessionExpired) {
setState(() {
_sessionExpired = true;
});
_showExpiredDialog();
}
_isCheckingSession = false;
return;
}
// Check session validity
final isValid = await SessionManager.isSessionValid();
if (mounted && !isValid && !_sessionExpired) {
setState(() {
_sessionExpired = true;
});
_showExpiredDialog();
}
} catch (e) {
debugPrint('SessionGuard: Error checking session - $e');
} finally {
_isCheckingSession = false;
}
} }
void _showExpiredDialog() { void _showExpiredDialog() {
if (_showingDialog) return; if (_showingDialog) return;
// Double check if user is actually logged in before showing dialog
final currentUser = Supabase.instance.client.auth.currentUser;
final currentSession = Supabase.instance.client.auth.currentSession;
if (currentUser == null || currentSession == null) {
debugPrint(
'SessionGuard: No user or session, not showing expired dialog',
);
_showingDialog = false;
return;
}
debugPrint(
'SessionGuard: Showing expired dialog for user: ${currentUser.id}',
);
_showingDialog = true; _showingDialog = true;
// Show dialog on next frame to avoid build phase issues // Show dialog on next frame to avoid build phase issues
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) { if (!mounted) {
_showingDialog = false; _showingDialog = false;
return; return;
} }
// Final check before showing dialog
final finalUser = Supabase.instance.client.auth.currentUser;
if (finalUser == null) {
debugPrint('SessionGuard: User logged out before showing dialog');
_showingDialog = false;
return;
}
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@ -113,39 +133,12 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// If not enforcing authentication, just show the child debugPrint('SessionGuard: build called - PERMISSIVE MODE');
if (!widget.enforceAuthentication) {
return widget.child; // PERMISSIVE MODE - Always show the child without any restrictions
} debugPrint(
'SessionGuard: Showing child widget without session restrictions',
// If session is expired, show a restricted UI
if (_sessionExpired) {
return Material(
child: Stack(
children: [
// Blur the background content
Opacity(
opacity: 0.3,
child: AbsorbPointer(
child: widget.child,
),
),
// Show a loading indicator if dialog is not showing yet
if (!_showingDialog)
const Center(
child: CircularProgressIndicator(),
),
],
),
);
}
// Session is valid, show the child with a gesture detector to update activity
return GestureDetector(
onTap: () => SessionManager.updateLastUserInteraction(),
onPanDown: (_) => SessionManager.updateLastUserInteraction(),
behavior: HitTestBehavior.translucent,
child: widget.child,
); );
return widget.child;
} }
} }