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) {
// Skip routes that are already added above
if (route != '/profile' &&
route != '/admin' &&
route != '/admin/users' &&
route != '/admin/crops' &&
route != '/admin/community' &&
route != '/field-management') {
allRoutes[route] = allRoutes[route] =
(context) => SessionGuardWrapper(child: builder(context)); (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,77 +252,32 @@ 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');
}
}
// Mulai pemeriksaan sesi yang lebih agresif // Cancel any existing timer
void _startAggressiveSessionChecking() {
debugPrint('App: Starting aggressive session checking');
// Batalkan timer yang ada jika ada
_sessionCheckTimer?.cancel(); _sessionCheckTimer?.cancel();
// Periksa sesi setiap 15 detik // No timer setup - completely disabled
_sessionCheckTimer = Timer.periodic(Duration(seconds: 15), (timer) { debugPrint('App: No periodic session checks will run');
// 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 // Check session validity on startup and periodically (DISABLED)
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
void dispose() { void dispose() {
_initialLaunchTimer?.cancel(); _initialLaunchTimer?.cancel();
@ -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');
// Jangan tampilkan dialog jika masih dalam fase initial launch
if (_isInitialLaunch) {
debugPrint(
'App: Still in initial launch phase, skipping session expired dialog',
);
return; return;
} }
if (_showingSessionExpiredDialog) { // Reset expired dialog state saat login/logout
debugPrint('App: Dialog already showing, skipping'); void _resetExpiredDialogState() {
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; _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),
colorScheme: ColorScheme.fromSwatch(
primarySwatch: Colors.green, primarySwatch: Colors.green,
accentColor: const Color(0xFF66BB6A), primaryColor: const Color(0xFF056839),
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF056839),
brightness: Brightness.light,
), ),
scaffoldBackgroundColor: const Color.fromARGB(255, 255, 255, 255),
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true, useMaterial3: true,
fontFamily: 'Poppins',
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF056839),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: true,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF056839),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.black26, width: 1.5),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.black26, width: 1.5),
),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0), borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.black, width: 2.5), borderSide: const BorderSide(color: Color(0xFF056839), width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.red, width: 1.5),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.0),
borderSide: const BorderSide(color: Colors.red, width: 2.5),
), ),
), ),
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;
if (mounted) {
setState(() => _isLoading = true); 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
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); 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,8 +289,14 @@ 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;
});
}
} }
if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_lastUpdated = formatter.format(now); _lastUpdated = formatter.format(now);
@ -247,6 +305,7 @@ class _AdminDashboardState extends State<AdminDashboard> {
'📊 FINAL STATS: Users: $_totalUsers, Active: $_activeUsers, Guides: $_totalGuides, News: $_totalNews, Posts: $_totalPosts', '📊 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 {
debugPrint('DEBUG: Mulai cek akses admin...');
if (mounted) {
setState(() => _isLoading = true); 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,12 +331,17 @@ 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((_) {
if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: const Text('Access denied. Admin privileges required.'), content: const Text(
'Access denied. Admin privileges required.',
),
backgroundColor: Colors.red.shade400, backgroundColor: Colors.red.shade400,
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -281,16 +349,20 @@ class _AdminDashboardState extends State<AdminDashboard> {
), ),
), ),
); );
}
}); });
} 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,6 +588,8 @@ class _AdminDashboardState extends State<AdminDashboard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
try {
debugPrint('DEBUG: build() AdminDashboard dipanggil');
// Define page content based on selected index // Define page content based on selected index
Widget pageContent; Widget pageContent;
switch (_currentIndex) { switch (_currentIndex) {
@ -615,6 +690,10 @@ class _AdminDashboardState extends State<AdminDashboard> {
], ],
), ),
); );
} catch (e, stack) {
debugPrint('DEBUG: Exception di build AdminDashboard: $e\n$stack');
return Center(child: Text('Error: $e'));
}
} }
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();
@ -1959,24 +1963,36 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
], ],
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: FlutterMap( child: Stack(
children: [
FlutterMap(
mapController: _detailMapController,
options: MapOptions( options: MapOptions(
center: LatLng(field.latitude!, field.longitude!), center: LatLng(
zoom: 15.0, field.latitude!,
interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate, field.longitude!,
),
zoom: 18.0,
interactiveFlags:
InteractiveFlag
.all, // Aktifkan semua gesture
), ),
children: [ children: [
TileLayer( TileLayer(
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate:
subdomains: ['a', 'b', 'c'], 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.tanismart.tugas_akhir_supabase', userAgentPackageName:
'com.tanismart.tugas_akhir_supabase',
), ),
MarkerLayer( MarkerLayer(
markers: [ markers: [
Marker( Marker(
width: 40.0, width: 40.0,
height: 40.0, height: 40.0,
point: LatLng(field.latitude!, field.longitude!), point: LatLng(
field.latitude!,
field.longitude!,
),
child: Icon( child: Icon(
Icons.location_on, Icons.location_on,
color: Colors.red, color: Colors.red,
@ -1987,6 +2003,80 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
), ),
], ],
), ),
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();
},
),
],
),
), ),
if (field.location != null && if (field.location != null &&
field.location!.isNotEmpty) field.location!.isNotEmpty)
@ -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
Future.delayed(Duration(milliseconds: 100), () {
if (mounted) {
_safeLoadGroups(); _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 {
try {
final authServices = GetIt.instance<AuthServices>();
await authServices.signOut();
if (mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
} catch (e) {
debugPrint('Error during sign out: $e');
// Fallback to direct sign out
await _supabase.auth.signOut(); await _supabase.auth.signOut();
if (mounted) { if (mounted) {
Navigator.of(context).pushReplacementNamed('/login'); Navigator.of(context).pushReplacementNamed('/login');
} }
} }
Future<void> _checkAdminStatus() async {
try {
final authServices = GetIt.instance<AuthServices>();
final isAdmin = await authServices.isAdmin();
debugPrint('ProfileScreen: isAdmin check result: $isAdmin');
if (mounted) {
setState(() {
_isAdmin = isAdmin;
});
} }
// Simple admin check without complex session management
Future<bool> _isUserAdmin() async {
try {
if (_user == null) return false;
final authServices = GetIt.instance<AuthServices>();
return await authServices.isAdmin();
} catch (e) { } catch (e) {
debugPrint('Error checking admin status: $e'); debugPrint('Error checking admin status: $e');
} return false;
}
Future<void> _refreshUserSession() async {
try {
// Update user activity timestamp
await updateUserActivity();
// Refresh Supabase session
final authServices = GetIt.instance<AuthServices>();
await authServices.refreshSession();
debugPrint('Session refreshed in ProfileScreen');
// Cek ulang status admin setelah refresh session
await _checkAdminStatus();
// Check session validity
await checkSessionStatus();
} catch (e) {
debugPrint('Error refreshing session in ProfileScreen: $e');
} }
} }
@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>(
future: _isUserAdmin(),
builder: (context, snapshot) {
final isAdmin = snapshot.data ?? false;
return IconButton(
icon: Container( icon: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _isAdmin ? Colors.blue[100] : Colors.grey[100], color: isAdmin ? Colors.blue[100] : Colors.grey[100],
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon( child: Icon(
Icons.admin_panel_settings, Icons.admin_panel_settings,
size: 18, size: 18,
color: _isAdmin ? Colors.blue[700] : Colors.grey[700], color: isAdmin ? Colors.blue[700] : Colors.grey[700],
), ),
), ),
onPressed: () { onPressed: () async {
// Double check admin status before allowing access
if (_user != null) {
final authServices = GetIt.instance<AuthServices>();
final isAdmin = await authServices.isAdmin();
if (isAdmin) {
// Jika admin, buka dashboard admin // Jika admin, buka dashboard admin
if (_isAdmin) { if (mounted) {
Navigator.of(context).pushNamed('/admin'); Navigator.of(context).pushNamed('/admin');
}
} else { } else {
// Jika bukan admin, tampilkan dialog untuk mengelola role // Jika bukan admin, tampilkan dialog untuk mengelola role
if (mounted) {
_showRoleManagementDialog(); _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 (_isSessionExpired) {
debugPrint('Session: Session already marked as expired');
return false;
}
if (!_hasLoggedInUser) {
debugPrint('Session: No logged in user, skipping session validity check');
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) { if (currentUser == null || currentSession == null) {
debugPrint('Session: No valid Supabase user or session found'); debugPrint('Session: No user or session found, returning false');
_setSessionExpired(true);
_isCheckingSession = false;
return false; return false;
} }
SharedPreferences? prefs = await _getSafeSharedPreferences(); // Always return true for permissive mode if user is logged in
final sessionState = prefs?.getString(_sessionStateKey); debugPrint('Session: PERMISSIVE MODE - User logged in, returning true');
return true;
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) {
// Double check before changing state to avoid race conditions
final doubleCheckUser = Supabase.instance.client.auth.currentUser;
final doubleCheckSession = Supabase.instance.client.auth.currentSession;
if (doubleCheckUser == null || doubleCheckSession == null) {
debugPrint(
'Session: User authentication state changed, updating flags',
);
_hasLoggedInUser = false; _hasLoggedInUser = false;
_setSessionExpired(true); _setSessionExpired(true);
debugPrint( } else {
'Session: Updated login status to false based on authentication check', 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),
onTimeout: () {
debugPrint('SessionChecker: Update activity timed out');
return;
},
); );
} catch (e) { // No periodic checks in permissive mode
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

@ -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,72 +26,84 @@ 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();
// Initialize with a delay to prevent race conditions
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_hasInitialized) {
_hasInitialized = true;
// Check session on widget initialization // Check session on widget initialization
if (widget.enforceAuthentication) { if (widget.enforceAuthentication) {
_checkSession(); _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
if (widget.enforceAuthentication &&
!_isCheckingSession &&
_hasInitialized) {
// Add a small delay to prevent race conditions during navigation
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) {
_checkSession(); _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');
// 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; 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
@ -101,6 +113,14 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
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) {
// PERMISSIVE MODE - Always show the child without any restrictions
debugPrint(
'SessionGuard: Showing child widget without session restrictions',
);
return widget.child; return widget.child;
} }
// 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,
);
}
} }