fix
This commit is contained in:
parent
15327e0b2d
commit
9a5af453c1
|
@ -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/register_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/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/enhanced_community_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/intro/animation_splash_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/panen/analisis_chart_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/panen/analisis_hasil_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/panen/analisis_input_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/panen/analisis_panen_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/screens/image_processing/plant_scanner_screen.dart'
|
||||
as scanner;
|
||||
import 'package:tugas_akhir_supabase/screens/intro/animation_splash_screen.dart'
|
||||
as splash;
|
||||
import 'package:tugas_akhir_supabase/screens/intro/intro_page_screen.dart'
|
||||
as intro;
|
||||
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/calendar/schedule_detail_screen.dart';
|
||||
import 'package:tugas_akhir_supabase/widgets/session_guard_wrapper.dart';
|
||||
|
||||
/// Defines all routes used in the application
|
||||
class AppRoutes {
|
||||
/// Non-authenticated routes
|
||||
static final Map<String, Widget Function(BuildContext)> _publicRoutes = {
|
||||
'/': (context) => const SplashScreen(),
|
||||
'/intro': (context) => const AnimatedIntroScreen(),
|
||||
'/': (context) => const splash.SplashScreen(),
|
||||
'/intro': (context) => const intro.AnimatedIntroScreen(),
|
||||
'/login': (context) => const LoginScreen(),
|
||||
'/register': (context) => const RegisterScreen(),
|
||||
'/forgot-password': (context) => const ForgotPasswordScreen(),
|
||||
|
@ -50,26 +59,26 @@ class AppRoutes {
|
|||
_authenticatedRoutes = {
|
||||
'/home': (context) => const HomeScreen(),
|
||||
'/profile': (context) => const ProfileScreen(),
|
||||
'/calendar': (context) => const KalenderTanamScreen(),
|
||||
'/calendar': (context) => const calendar.KalenderTanamScreen(),
|
||||
'/field-management': (context) => const FieldManagementScreen(),
|
||||
'/schedule-list': (context) => const ScheduleListScreen(),
|
||||
'/plant-scanner': (context) => const PlantScannerScreen(),
|
||||
'/plant-scanner': (context) => const scanner.PlantScannerScreen(),
|
||||
'/community': (context) => const CommunityScreen(),
|
||||
'/enhanced-community': (context) => const EnhancedCommunityScreen(),
|
||||
'/analisis': (context) {
|
||||
final args =
|
||||
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
||||
return HarvestAnalysisScreen(userId: args?['userId'] ?? '');
|
||||
return panen.HarvestAnalysisScreen(userId: args?['userId'] ?? '');
|
||||
},
|
||||
'/analisis-input': (context) {
|
||||
final args =
|
||||
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
||||
return AnalisisInputScreen(userId: args?['userId'] ?? '');
|
||||
return input.AnalisisInputScreen(userId: args?['userId'] ?? '');
|
||||
},
|
||||
'/analisis-hasil': (context) {
|
||||
final args =
|
||||
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
||||
return HarvestResultScreen(
|
||||
return hasil.HarvestResultScreen(
|
||||
userId: args?['userId'] ?? '',
|
||||
harvestData: args?['harvestData'],
|
||||
scheduleData: args?['scheduleData'],
|
||||
|
@ -78,7 +87,7 @@ class AppRoutes {
|
|||
'/analisis-chart': (context) {
|
||||
final args =
|
||||
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
||||
return HarvestAnalysisChart(
|
||||
return chart.HarvestAnalysisChart(
|
||||
userId: args?['userId'] ?? '',
|
||||
harvestData: args?['harvestData'],
|
||||
scheduleData: args?['scheduleData'],
|
||||
|
@ -88,7 +97,7 @@ class AppRoutes {
|
|||
'/kalender-detail': (context) {
|
||||
final args =
|
||||
ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
|
||||
return ScheduleDetailScreen(scheduleId: args?['scheduleId'] ?? '');
|
||||
return detail.ScheduleDetailScreen(scheduleId: args?['scheduleId'] ?? '');
|
||||
},
|
||||
|
||||
/// Admin routes
|
||||
|
@ -105,10 +114,30 @@ class AppRoutes {
|
|||
/// Add public routes as-is
|
||||
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) {
|
||||
allRoutes[route] =
|
||||
(context) => SessionGuardWrapper(child: builder(context));
|
||||
// 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] =
|
||||
(context) => SessionGuardWrapper(child: builder(context));
|
||||
}
|
||||
});
|
||||
|
||||
return allRoutes;
|
||||
|
|
179
lib/main.dart
179
lib/main.dart
|
@ -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:get_it/get_it.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
|
||||
bool _hasDoneHotReloadSetup = false;
|
||||
|
@ -250,75 +252,30 @@ class _RealAppState extends State<RealApp> with WidgetsBindingObserver {
|
|||
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');
|
||||
_showSessionExpiredDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// Mulai pemeriksaan sesi berkala yang lebih agresif, tapi hanya jika sudah tidak dalam initial launch
|
||||
if (!_isInitialLaunch) {
|
||||
_startAggressiveSessionChecking();
|
||||
// Mulai pemeriksaan sesi yang lebih agresif (DISABLED)
|
||||
void 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
|
||||
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
|
||||
// Check session validity on startup and periodically (DISABLED)
|
||||
Future<void> _checkSessionValidity() async {
|
||||
// Jangan periksa session selama initial launch phase
|
||||
if (_isInitialLaunch) {
|
||||
debugPrint('App: Skipping session check during initial launch');
|
||||
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');
|
||||
}
|
||||
// Session checking DISABLED for permissive mode
|
||||
debugPrint('App: Session checking DISABLED for permissive mode');
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -375,86 +332,56 @@ class _RealAppState extends State<RealApp> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
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
|
||||
if (_isInitialLaunch) {
|
||||
debugPrint(
|
||||
'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;
|
||||
});
|
||||
// Reset expired dialog state saat login/logout
|
||||
void _resetExpiredDialogState() {
|
||||
_showingSessionExpiredDialog = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'TaniSMART',
|
||||
navigatorKey: navigatorKey,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
primaryColor: const Color(0xFF2E7D32),
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
primarySwatch: Colors.green,
|
||||
accentColor: const Color(0xFF66BB6A),
|
||||
primarySwatch: Colors.green,
|
||||
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,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
borderSide: const BorderSide(color: Colors.black, width: 2.5),
|
||||
),
|
||||
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),
|
||||
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(
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFF056839), width: 2),
|
||||
),
|
||||
),
|
||||
dividerColor: Colors.black,
|
||||
),
|
||||
routes: AppRoutes.routes,
|
||||
initialRoute: '/',
|
||||
home: const intro.AnimatedIntroScreen(),
|
||||
routes: Map.from(AppRoutes.routes)..remove('/'),
|
||||
// Add navigation observer to clear SnackBars when navigating
|
||||
navigatorObservers: [
|
||||
_SnackBarClearingNavigatorObserver(),
|
||||
|
|
|
@ -131,7 +131,9 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
Future<void> _loadDashboardStats() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
|
||||
try {
|
||||
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 length: ${response.length}');
|
||||
debugPrint('🔍 get_all_users response: $response');
|
||||
|
||||
// Convert to List<Map>
|
||||
final users = List<Map<String, dynamic>>.from(response);
|
||||
// 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);
|
||||
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) {
|
||||
setState(() {
|
||||
_totalUsers = users.length;
|
||||
_totalUsers = userCount;
|
||||
debugPrint('✅ Set _totalUsers to $userCount');
|
||||
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) {
|
||||
debugPrint('❌ Error fetching users with get_all_users: $e');
|
||||
|
@ -188,6 +224,12 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
}
|
||||
} catch (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) {
|
||||
debugPrint('❌ Error fetching guides count: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_totalGuides = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
|
@ -224,6 +271,11 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching news count: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_totalNews = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch community messages count
|
||||
|
@ -237,16 +289,23 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching community posts count: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_totalPosts = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_lastUpdated = formatter.format(now);
|
||||
debugPrint('🕒 Last updated: $_lastUpdated');
|
||||
debugPrint(
|
||||
'📊 FINAL STATS: Users: $_totalUsers, Active: $_activeUsers, Guides: $_totalGuides, News: $_totalNews, Posts: $_totalPosts',
|
||||
);
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_lastUpdated = formatter.format(now);
|
||||
debugPrint('🕒 Last updated: $_lastUpdated');
|
||||
debugPrint(
|
||||
'📊 FINAL STATS: Users: $_totalUsers, Active: $_activeUsers, Guides: $_totalGuides, News: $_totalNews, Posts: $_totalPosts',
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error loading dashboard stats: $e');
|
||||
if (mounted) {
|
||||
|
@ -256,10 +315,14 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
}
|
||||
|
||||
Future<void> _checkAdminAccess() async {
|
||||
setState(() => _isLoading = true);
|
||||
debugPrint('DEBUG: Mulai cek akses admin...');
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
|
||||
try {
|
||||
final isAdmin = await _authServices.isAdmin();
|
||||
debugPrint('DEBUG: Hasil cek isAdmin: $isAdmin');
|
||||
|
||||
if (mounted) {
|
||||
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((_) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text('Access denied. Admin privileges required.'),
|
||||
backgroundColor: Colors.red.shade400,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: const Text(
|
||||
'Access denied. Admin privileges required.',
|
||||
),
|
||||
backgroundColor: Colors.red.shade400,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
debugPrint('DEBUG: User adalah admin, melanjutkan...');
|
||||
}
|
||||
} 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) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Error: ${e.toString()}'),
|
||||
backgroundColor: Colors.red.shade400,
|
||||
content: Text('Error checking admin access: ${e.toString()}'),
|
||||
backgroundColor: Colors.orange.shade400,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
|
@ -299,6 +371,7 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
);
|
||||
}
|
||||
}
|
||||
debugPrint('DEBUG: Selesai cek akses admin.');
|
||||
}
|
||||
|
||||
Future<void> _refreshDashboardData() async {
|
||||
|
@ -515,106 +588,112 @@ class _AdminDashboardState extends State<AdminDashboard> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Define page content based on selected index
|
||||
Widget pageContent;
|
||||
switch (_currentIndex) {
|
||||
case 0:
|
||||
pageContent = _buildOverviewTab();
|
||||
break;
|
||||
case 1:
|
||||
pageContent = const UserManagement();
|
||||
break;
|
||||
case 2:
|
||||
pageContent = const GuideManagement();
|
||||
break;
|
||||
// case 3: // Removing Crops tab from admin
|
||||
// pageContent = const CropManagement();
|
||||
// break;
|
||||
case 3: // Updated index after removing Crops
|
||||
pageContent = const CommunityManagement();
|
||||
break;
|
||||
case 4: // Updated index after removing Crops
|
||||
pageContent = const NewsManagement();
|
||||
break;
|
||||
default:
|
||||
pageContent = _buildOverviewTab();
|
||||
}
|
||||
try {
|
||||
debugPrint('DEBUG: build() AdminDashboard dipanggil');
|
||||
// Define page content based on selected index
|
||||
Widget pageContent;
|
||||
switch (_currentIndex) {
|
||||
case 0:
|
||||
pageContent = _buildOverviewTab();
|
||||
break;
|
||||
case 1:
|
||||
pageContent = const UserManagement();
|
||||
break;
|
||||
case 2:
|
||||
pageContent = const GuideManagement();
|
||||
break;
|
||||
// case 3: // Removing Crops tab from admin
|
||||
// pageContent = const CropManagement();
|
||||
// break;
|
||||
case 3: // Updated index after removing Crops
|
||||
pageContent = const CommunityManagement();
|
||||
break;
|
||||
case 4: // Updated index after removing Crops
|
||||
pageContent = const NewsManagement();
|
||||
break;
|
||||
default:
|
||||
pageContent = _buildOverviewTab();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Admin Dashboard',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Admin Dashboard',
|
||||
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,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.exit_to_app),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body:
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: !_isAdmin
|
||||
? const Center(
|
||||
child: Text('Access denied. Admin privileges required.'),
|
||||
)
|
||||
: pageContent,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: AppColors.primary,
|
||||
unselectedItemColor: Colors.grey,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Overview',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.people_outline),
|
||||
activeIcon: Icon(Icons.people),
|
||||
label: 'Users',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.book_outlined),
|
||||
activeIcon: Icon(Icons.book),
|
||||
label: 'Guides',
|
||||
),
|
||||
// BottomNavigationBarItem(
|
||||
// icon: Icon(Icons.agriculture_outlined),
|
||||
// activeIcon: Icon(Icons.agriculture),
|
||||
// label: 'Crops',
|
||||
// ),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.forum_outlined),
|
||||
activeIcon: Icon(Icons.forum),
|
||||
label: 'Community',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.newspaper_outlined),
|
||||
activeIcon: Icon(Icons.newspaper),
|
||||
label: 'News',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
body:
|
||||
_isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: !_isAdmin
|
||||
? const Center(
|
||||
child: Text('Access denied. Admin privileges required.'),
|
||||
)
|
||||
: pageContent,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
_currentIndex = index;
|
||||
});
|
||||
},
|
||||
type: BottomNavigationBarType.fixed,
|
||||
backgroundColor: Colors.white,
|
||||
selectedItemColor: AppColors.primary,
|
||||
unselectedItemColor: Colors.grey,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
activeIcon: Icon(Icons.dashboard),
|
||||
label: 'Overview',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.people_outline),
|
||||
activeIcon: Icon(Icons.people),
|
||||
label: 'Users',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.book_outlined),
|
||||
activeIcon: Icon(Icons.book),
|
||||
label: 'Guides',
|
||||
),
|
||||
// BottomNavigationBarItem(
|
||||
// icon: Icon(Icons.agriculture_outlined),
|
||||
// activeIcon: Icon(Icons.agriculture),
|
||||
// label: 'Crops',
|
||||
// ),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.forum_outlined),
|
||||
activeIcon: Icon(Icons.forum),
|
||||
label: 'Community',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.newspaper_outlined),
|
||||
activeIcon: Icon(Icons.newspaper),
|
||||
label: 'News',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
debugPrint('DEBUG: Exception di build AdminDashboard: $e\n$stack');
|
||||
return Center(child: Text('Error: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOverviewTab() {
|
||||
|
|
|
@ -454,7 +454,10 @@ class _KalenderTanamScreenState extends State<KalenderTanamScreen> {
|
|||
height: 42,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pushNamed(context, '/schedule-list');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const ScheduleListScreen()),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.list_alt, size: 16, color: Colors.white),
|
||||
label: Text(
|
||||
|
@ -483,7 +486,9 @@ class _KalenderTanamScreenState extends State<KalenderTanamScreen> {
|
|||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const FieldManagementScreen()),
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const FieldManagementScreen(),
|
||||
),
|
||||
).then((_) {
|
||||
_fetchFieldCount();
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import 'package:tugas_akhir_supabase/screens/calendar/location_picker_dialog.dar
|
|||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
|
@ -149,6 +150,9 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
|
|||
'Lahan Konversi',
|
||||
];
|
||||
|
||||
// Tambahkan MapController di _FieldManagementScreenState
|
||||
final MapController _detailMapController = MapController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -714,18 +718,18 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
|
|||
),
|
||||
),
|
||||
child: TextFormField(
|
||||
controller: _locationController,
|
||||
controller: _locationController,
|
||||
readOnly: true,
|
||||
decoration: InputDecoration(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Pilih lokasi di peta',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.white.withOpacity(
|
||||
0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
prefixIcon: Icon(
|
||||
Icons.location_on,
|
||||
prefixIcon: Icon(
|
||||
Icons.location_on,
|
||||
color: Colors.white.withOpacity(
|
||||
0.9,
|
||||
),
|
||||
|
@ -734,8 +738,8 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
|
|||
const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
),
|
||||
|
@ -1959,32 +1963,118 @@ class _FieldManagementScreenState extends State<FieldManagementScreen> {
|
|||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FlutterMap(
|
||||
options: MapOptions(
|
||||
center: LatLng(field.latitude!, field.longitude!),
|
||||
zoom: 15.0,
|
||||
interactiveFlags: InteractiveFlag.all & ~InteractiveFlag.rotate,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
subdomains: ['a', 'b', 'c'],
|
||||
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,
|
||||
),
|
||||
FlutterMap(
|
||||
mapController: _detailMapController,
|
||||
options: MapOptions(
|
||||
center: LatLng(
|
||||
field.latitude!,
|
||||
field.longitude!,
|
||||
),
|
||||
zoom: 18.0,
|
||||
interactiveFlags:
|
||||
InteractiveFlag
|
||||
.all, // Aktifkan semua gesture
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -3,6 +3,9 @@ import 'package:flutter_map/flutter_map.dart';
|
|||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'dart:convert';
|
||||
|
||||
class LocationResult {
|
||||
final String address;
|
||||
|
@ -125,6 +128,37 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
|
|||
_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
|
||||
Widget build(BuildContext context) {
|
||||
final isFullscreen = widget.fullscreen;
|
||||
|
@ -235,9 +269,7 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
|
|||
center: _selectedLatLng,
|
||||
zoom: 15.0,
|
||||
interactiveFlags:
|
||||
InteractiveFlag.all &
|
||||
~InteractiveFlag.rotate, // Nonaktifkan gesture rotasi
|
||||
// rotation: _rotation, // Tidak perlu rotasi
|
||||
InteractiveFlag.all, // Aktifkan semua gesture
|
||||
onTap: (tapPosition, point) async {
|
||||
setState(() {
|
||||
_selectedLatLng = point;
|
||||
|
@ -247,13 +279,11 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
|
|||
point.longitude,
|
||||
);
|
||||
},
|
||||
// Hapus onPositionChanged
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate:
|
||||
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
subdomains: ['a', 'b', 'c'],
|
||||
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.tanismart.tugas_akhir_supabase',
|
||||
),
|
||||
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
|
||||
Positioned(
|
||||
top: 12,
|
||||
|
@ -423,4 +510,3 @@ class _LocationPickerDialogState extends State<LocationPickerDialog> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -36,6 +36,9 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
|
|||
String? _loadingGroupId; // Track grup yang sedang di-join/leave
|
||||
bool _isJoiningGroup = false;
|
||||
|
||||
// Tambahkan deklarasi channel realtime
|
||||
RealtimeChannel? _groupRealtimeChannel;
|
||||
|
||||
// Add a key for the GroupChatScreen
|
||||
final GlobalKey<GroupChatScreenState> _groupChatKey =
|
||||
GlobalKey<GroupChatScreenState>();
|
||||
|
@ -50,24 +53,16 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
|
|||
);
|
||||
_tabController.addListener(_handleTabChange);
|
||||
|
||||
// Gunakan timeout yang lebih pendek dan jangan langsung memanggil _preLoadDefaultGroup
|
||||
// Beri waktu untuk widget selesai diinisialisasi
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (mounted) {
|
||||
_safeLoadGroups();
|
||||
}
|
||||
});
|
||||
// Panggil langsung tanpa Future.delayed
|
||||
_safeLoadGroups();
|
||||
|
||||
// Make a direct database query to preload messages as early as possible
|
||||
_preloadDefaultGroupMessages();
|
||||
|
||||
// Set a super-aggressive backup timer that will force refresh the chat
|
||||
// even if all other mechanisms fail
|
||||
Future.delayed(Duration(seconds: 2), () {
|
||||
if (mounted) {
|
||||
_forceRefreshAllMessages();
|
||||
}
|
||||
});
|
||||
_setupGroupRealtimeSubscription();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -137,7 +132,8 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
|
|||
}
|
||||
|
||||
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');
|
||||
return;
|
||||
}
|
||||
|
@ -327,6 +323,8 @@ class _EnhancedCommunityScreenState extends State<EnhancedCommunityScreen>
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
// Unsubscribe channel realtime
|
||||
_groupRealtimeChannel?.unsubscribe();
|
||||
_tabController.removeListener(_handleTabChange);
|
||||
_tabController.dispose();
|
||||
// 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
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
|
@ -274,9 +274,7 @@ class GroupMessageService {
|
|||
final userEmail = currentEmail ?? _supabase.auth.currentUser?.email ?? '';
|
||||
|
||||
// Generate ID
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final messageId =
|
||||
'grp-$timestamp-${userId.substring(0, userId.length.clamp(0, 6))}';
|
||||
final messageId = _uuid.v4(); // UUID valid
|
||||
|
||||
print('[DEBUG] Sending group message: $messageId');
|
||||
print(
|
||||
|
@ -349,7 +347,7 @@ class GroupMessageService {
|
|||
|
||||
try {
|
||||
// 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');
|
||||
saveSuccess = true;
|
||||
} catch (e) {
|
||||
|
@ -367,7 +365,7 @@ class GroupMessageService {
|
|||
retryData.remove('reply_to_sender_username');
|
||||
|
||||
try {
|
||||
await _supabase.from('group_messages').insert(retryData);
|
||||
await _supabase.from('messages').insert(retryData);
|
||||
print('[DEBUG] Group message saved without reply data');
|
||||
saveSuccess = true;
|
||||
} catch (retryError) {
|
||||
|
@ -560,7 +558,10 @@ class GroupMessageService {
|
|||
await _supabase
|
||||
.rpc(
|
||||
'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(
|
||||
Duration(seconds: 2),
|
||||
|
|
|
@ -1012,7 +1012,10 @@ class MessageService {
|
|||
try {
|
||||
await _supabase.rpc(
|
||||
'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) {
|
||||
print('[ERROR] Failed to mark message $messageId as read: $e');
|
||||
|
@ -1034,8 +1037,9 @@ class MessageService {
|
|||
await _supabase.rpc(
|
||||
'add_message_read_receipt',
|
||||
params: {
|
||||
'p_message_id': messageId,
|
||||
'p_user_id': _supabase.auth.currentUser!.id,
|
||||
'p_message_id': messageId.toString(), // Ensure it's string
|
||||
'p_user_id':
|
||||
_supabase.auth.currentUser!.id.toString(), // Ensure it's string
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
@ -2266,7 +2266,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
|
|||
'Biaya Pokok Produksi:',
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 10,
|
||||
fontSize: 9,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
|
@ -2276,7 +2276,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
|
|||
'${currency.format(productionCostPerKg)}/kg',
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
fontSize: 10,
|
||||
color: Colors.blue.shade900,
|
||||
),
|
||||
),
|
||||
|
@ -2701,7 +2701,7 @@ class _HarvestResultScreenState extends State<HarvestResultScreen> {
|
|||
'Biaya Pokok Produksi',
|
||||
style: GoogleFonts.poppins(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 16,
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -17,8 +17,7 @@ class ProfileScreen extends StatefulWidget {
|
|||
_ProfileScreenState createState() => _ProfileScreenState();
|
||||
}
|
||||
|
||||
class _ProfileScreenState extends State<ProfileScreen>
|
||||
with SessionCheckerMixin {
|
||||
class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _usernameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
|
@ -39,20 +38,27 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
double _averageYield = 0;
|
||||
final String _mostPlantedCrop = '-';
|
||||
|
||||
bool _isAdmin = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initSessionChecking(); // Initialize session checking
|
||||
_initializeProfile();
|
||||
_checkAdminStatus();
|
||||
_refreshUserSession();
|
||||
debugPrint('ProfileScreen: initState called');
|
||||
|
||||
// Get user immediately and load profile
|
||||
_user = _supabase.auth.currentUser;
|
||||
debugPrint('ProfileScreen: User from Supabase: ${_user?.id}');
|
||||
|
||||
if (_user != null) {
|
||||
_loadProfile();
|
||||
_loadStatistics();
|
||||
} else {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposeSessionChecking(); // Clean up session checking
|
||||
_usernameController.dispose();
|
||||
_emailController.dispose();
|
||||
_phoneController.dispose();
|
||||
|
@ -65,34 +71,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Refresh admin status setiap kali halaman dimuat ulang
|
||||
_checkAdminStatus();
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
// _checkAdminStatus(); // Removed as per new_code
|
||||
}
|
||||
|
||||
Future<void> _loadProfile() async {
|
||||
|
@ -315,56 +294,48 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
}
|
||||
|
||||
Future<void> _signOut() async {
|
||||
await _supabase.auth.signOut();
|
||||
if (mounted) {
|
||||
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');
|
||||
await authServices.signOut();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isAdmin = isAdmin;
|
||||
});
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
} 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 {
|
||||
// Update user activity timestamp
|
||||
await updateUserActivity();
|
||||
|
||||
// Refresh Supabase session
|
||||
if (_user == null) return false;
|
||||
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();
|
||||
return await authServices.isAdmin();
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing session in ProfileScreen: $e');
|
||||
debugPrint('Error checking admin status: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint(
|
||||
'ProfileScreen: build called, _user: ${_user != null}, _isLoading: $_isLoading',
|
||||
);
|
||||
|
||||
if (_user == null) {
|
||||
debugPrint('ProfileScreen: Showing no user screen');
|
||||
return _buildNoUserScreen();
|
||||
}
|
||||
|
||||
if (_isLoading) {
|
||||
debugPrint('ProfileScreen: Showing loading screen');
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
body: Center(
|
||||
|
@ -375,6 +346,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
);
|
||||
}
|
||||
|
||||
debugPrint('ProfileScreen: Showing main profile screen');
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
appBar: _buildAppBar(),
|
||||
|
@ -407,29 +379,57 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
),
|
||||
actions: [
|
||||
// Tombol admin dengan tooltip yang sesuai
|
||||
IconButton(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: _isAdmin ? Colors.blue[100] : Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.admin_panel_settings,
|
||||
size: 18,
|
||||
color: _isAdmin ? Colors.blue[700] : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
// Jika admin, buka dashboard admin
|
||||
if (_isAdmin) {
|
||||
Navigator.of(context).pushNamed('/admin');
|
||||
} else {
|
||||
// Jika bukan admin, tampilkan dialog untuk mengelola role
|
||||
_showRoleManagementDialog();
|
||||
}
|
||||
FutureBuilder<bool>(
|
||||
future: _isUserAdmin(),
|
||||
builder: (context, snapshot) {
|
||||
final isAdmin = snapshot.data ?? false;
|
||||
return IconButton(
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: isAdmin ? Colors.blue[100] : Colors.grey[100],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.admin_panel_settings,
|
||||
size: 18,
|
||||
color: isAdmin ? Colors.blue[700] : Colors.grey[700],
|
||||
),
|
||||
),
|
||||
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
|
||||
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(
|
||||
icon: Container(
|
||||
|
@ -553,13 +553,12 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _showRoleManagementDialog() async {
|
||||
if (_user == null) return;
|
||||
|
||||
final authServices = GetIt.instance<AuthServices>();
|
||||
|
||||
void _showRoleManagementDialog() async {
|
||||
// Check admin status first
|
||||
final isAdmin = await _isUserAdmin();
|
||||
|
||||
// Jika bukan admin, tampilkan pesan error
|
||||
if (!_isAdmin) {
|
||||
if (!isAdmin) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Anda tidak memiliki akses untuk mengelola role'),
|
||||
|
@ -569,9 +568,6 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
|
||||
// Debug: Tampilkan user ID
|
||||
debugPrint('Current user ID: ${_user?.id}');
|
||||
|
||||
// Coba refresh status admin
|
||||
_checkAdminStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -645,6 +641,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
|
||||
if (isDowngradingToUser) {
|
||||
// Periksa jumlah admin yang ada
|
||||
final authServices = GetIt.instance<AuthServices>();
|
||||
final adminCount = await authServices.countAdmins();
|
||||
debugPrint('Current admin count: $adminCount');
|
||||
|
||||
|
@ -766,7 +763,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||
}
|
||||
|
||||
// Refresh status admin
|
||||
await _checkAdminStatus();
|
||||
// _checkAdminStatus(); // Removed as per new_code
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
|
|
@ -213,28 +213,9 @@ class AuthServices {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Gunakan fungsi is_admin_no_rls untuk menghindari infinite recursion
|
||||
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
|
||||
// First try: cek langsung dari tabel tanpa RLS
|
||||
try {
|
||||
final userId = getCurrentUserId();
|
||||
if (userId == null) return false;
|
||||
|
||||
// Query sederhana tanpa menggunakan RPC
|
||||
debugPrint('Trying direct table query for admin check...');
|
||||
final response =
|
||||
await _supabase
|
||||
.from('user_roles')
|
||||
|
@ -244,12 +225,53 @@ class AuthServices {
|
|||
.maybeSingle();
|
||||
|
||||
final isAdmin = response != null;
|
||||
debugPrint('Fallback admin check result: $isAdmin');
|
||||
return isAdmin;
|
||||
} catch (fallbackError) {
|
||||
debugPrint('Fallback error: $fallbackError');
|
||||
return false;
|
||||
debugPrint('Direct table query result: $isAdmin');
|
||||
if (isAdmin) return true;
|
||||
} catch (directError) {
|
||||
debugPrint('Direct table query failed: $directError');
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,8 @@ class SessionManager {
|
|||
static const String _lastActiveTimeKey = 'last_active_time';
|
||||
static const String _lastUserInteractionKey = 'last_user_interaction';
|
||||
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? _presenceUpdateTimer;
|
||||
|
@ -103,16 +104,27 @@ class SessionManager {
|
|||
debugPrint('Session: User login status set to: $isLoggedIn');
|
||||
|
||||
if (isLoggedIn) {
|
||||
_setSessionExpired(false);
|
||||
_setSessionExpired(false); // Pastikan expired direset!
|
||||
_isSessionExpired = false;
|
||||
_sessionExpiredController.add(false); // Kirim event expired false
|
||||
updateLastUserInteraction();
|
||||
_startSessionMonitoring();
|
||||
_startPresenceUpdates();
|
||||
} else {
|
||||
_stopSessionMonitoring();
|
||||
_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
|
||||
static void _startPresenceUpdates() {
|
||||
_stopPresenceUpdates(); // Stop any existing timer
|
||||
|
@ -139,162 +151,22 @@ class SessionManager {
|
|||
_presenceUpdateTimer = null;
|
||||
}
|
||||
|
||||
// Check if session is valid with improved logic
|
||||
// Check if session is valid with extremely permissive logic
|
||||
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
|
||||
if (_isAppJustLaunched) {
|
||||
debugPrint('Session: App just launched, assuming session valid');
|
||||
return true;
|
||||
}
|
||||
// Check if user is actually logged in
|
||||
final currentUser = Supabase.instance.client.auth.currentUser;
|
||||
final currentSession = Supabase.instance.client.auth.currentSession;
|
||||
|
||||
// If session is already marked as expired, return false immediately
|
||||
if (_isSessionExpired) {
|
||||
debugPrint('Session: Session already marked as expired');
|
||||
if (currentUser == null || currentSession == null) {
|
||||
debugPrint('Session: No user or session found, returning false');
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
// Always return true for permissive mode if user is logged in
|
||||
debugPrint('Session: PERMISSIVE MODE - User logged in, returning true');
|
||||
return true;
|
||||
}
|
||||
|
||||
// BARU: Update timestamp interaksi pengguna terakhir
|
||||
|
@ -425,42 +297,10 @@ class SessionManager {
|
|||
'Session: Checking session validity after returning to foreground',
|
||||
);
|
||||
|
||||
SharedPreferences? prefs = await _getSafeSharedPreferences();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use permissive mode - don't check timeout
|
||||
final isValid = await isSessionValid();
|
||||
if (!isValid) {
|
||||
debugPrint('Session: Expired while in background');
|
||||
debugPrint('Session: Session invalid after background');
|
||||
await clearSession();
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
_setSessionExpired(true);
|
||||
|
@ -477,6 +317,11 @@ class SessionManager {
|
|||
|
||||
// Set session expired state and notify listeners
|
||||
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) {
|
||||
_isSessionExpired = value;
|
||||
debugPrint('Session: Setting expired state to $value');
|
||||
|
@ -510,12 +355,21 @@ class SessionManager {
|
|||
final isValid =
|
||||
currentUser != null && currentSession != null && !_isSessionExpired;
|
||||
|
||||
// Only update state if there's a significant change, and be more conservative
|
||||
if (!isValid && _hasLoggedInUser) {
|
||||
_hasLoggedInUser = false;
|
||||
_setSessionExpired(true);
|
||||
debugPrint(
|
||||
'Session: Updated login status to false based on authentication check',
|
||||
);
|
||||
// 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;
|
||||
_setSessionExpired(true);
|
||||
} else {
|
||||
debugPrint('Session: User still exists, keeping authentication state');
|
||||
}
|
||||
}
|
||||
|
||||
return isValid;
|
||||
|
@ -527,6 +381,8 @@ class SessionManager {
|
|||
_stopSessionMonitoring();
|
||||
_setSessionExpired(true);
|
||||
_hasLoggedInUser = false;
|
||||
_isSessionExpired = false;
|
||||
_sessionExpiredController.add(false);
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_lastActiveTimeKey);
|
||||
|
@ -540,6 +396,8 @@ class SessionManager {
|
|||
debugPrint('Session: Error during cleanup - $e');
|
||||
_setSessionExpired(true);
|
||||
_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() {
|
||||
_stopSessionMonitoring();
|
||||
|
||||
debugPrint(
|
||||
'Session: Starting monitoring with timeout: $_sessionTimeoutMinutes minutes',
|
||||
);
|
||||
debugPrint('Session: Session monitoring DISABLED for permissive mode');
|
||||
|
||||
_sessionCheckTimer = Timer.periodic(const Duration(seconds: 15), (
|
||||
timer,
|
||||
) 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();
|
||||
});
|
||||
// No timer setup - completely disabled
|
||||
debugPrint('Session: No periodic checks will run');
|
||||
}
|
||||
|
||||
// Stop session monitoring
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,17 +1,65 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
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 SessionCheckerMixin<T extends StatefulWidget> on State<T> {
|
||||
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
|
||||
Future<void> checkSessionStatus() async {
|
||||
if (_isCheckingSession) return;
|
||||
_isCheckingSession = true;
|
||||
|
||||
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
|
||||
final isValid = await SessionManager.isSessionValid().timeout(
|
||||
const Duration(seconds: 3),
|
||||
|
@ -21,52 +69,54 @@ mixin SessionCheckerMixin<T extends StatefulWidget> on State<T> {
|
|||
},
|
||||
);
|
||||
|
||||
if (!isValid && mounted) {
|
||||
// Session tidak valid, update timestamp untuk mencegah pemanggilan berulang
|
||||
await SessionManager.updateLastActiveTime().timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
debugPrint('SessionChecker: Update activity timed out');
|
||||
return;
|
||||
},
|
||||
debugPrint('SessionChecker: Session validity result: $isValid');
|
||||
|
||||
// Update user interaction timestamp
|
||||
await SessionManager.updateLastUserInteraction();
|
||||
|
||||
if (!isValid) {
|
||||
debugPrint(
|
||||
'SessionChecker: Session invalid, but continuing in permissive mode',
|
||||
);
|
||||
// In permissive mode, don't force logout
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('SessionChecker: Session is valid');
|
||||
} catch (e) {
|
||||
debugPrint('SessionChecker: Error checking session - $e');
|
||||
debugPrint('SessionChecker: Error checking session: $e');
|
||||
// In permissive mode, don't force logout on error
|
||||
} finally {
|
||||
_isCheckingSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Perbarui timestamp aktivitas pengguna
|
||||
Future<void> updateUserActivity() async {
|
||||
try {
|
||||
// Tambahkan timeout untuk mencegah blocking
|
||||
await SessionManager.updateLastActiveTime().timeout(
|
||||
const Duration(seconds: 2),
|
||||
onTimeout: () {
|
||||
debugPrint('SessionChecker: Update activity timed out');
|
||||
return;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('SessionChecker: Error updating activity - $e');
|
||||
}
|
||||
/// Periksa session secara berkala (DISABLED untuk permissive mode)
|
||||
void startPeriodicSessionCheck() {
|
||||
debugPrint(
|
||||
'SessionChecker: Periodic session check DISABLED for permissive mode',
|
||||
);
|
||||
// No periodic checks in permissive mode
|
||||
}
|
||||
|
||||
/// Panggil di initState untuk memeriksa session
|
||||
void initSessionChecking() {
|
||||
// Periksa session setelah widget dibangun dengan delay
|
||||
// untuk mencegah terlalu banyak operasi di startup
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
checkSessionStatus();
|
||||
}
|
||||
/// Stop periodic session check
|
||||
void stopPeriodicSessionCheck() {
|
||||
debugPrint('SessionChecker: Stopping periodic session check');
|
||||
// Nothing to stop in permissive mode
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Check session on init
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
checkSessionStatus();
|
||||
});
|
||||
}
|
||||
|
||||
/// Panggil di dispose untuk membersihkan resource
|
||||
void disposeSessionChecking() {
|
||||
// Tidak ada resource yang perlu dibersihkan
|
||||
@override
|
||||
void dispose() {
|
||||
stopPeriodicSessionCheck();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'package:tugas_akhir_supabase/services/session_manager.dart';
|
|||
import 'package:tugas_akhir_supabase/widgets/session_expired_dialog.dart';
|
||||
|
||||
/// A widget that enforces session validation for authenticated routes.
|
||||
///
|
||||
///
|
||||
/// This widget should wrap any screen that requires authentication.
|
||||
/// It will automatically check if the session is valid and redirect to login
|
||||
/// if the session has expired.
|
||||
|
@ -13,10 +13,10 @@ class SessionGuardWrapper extends StatefulWidget {
|
|||
final bool enforceAuthentication;
|
||||
|
||||
const SessionGuardWrapper({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.child,
|
||||
this.enforceAuthentication = true,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
State<SessionGuardWrapper> createState() => _SessionGuardWrapperState();
|
||||
|
@ -26,81 +26,101 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
|
|||
bool _isCheckingSession = false;
|
||||
bool _sessionExpired = false;
|
||||
bool _showingDialog = false;
|
||||
bool _hasInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Check session on widget initialization
|
||||
if (widget.enforceAuthentication) {
|
||||
_checkSession();
|
||||
}
|
||||
|
||||
// Initialize with a delay to prevent race conditions
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
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
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
|
||||
// Check session when dependencies change (e.g., after navigation)
|
||||
if (widget.enforceAuthentication && !_isCheckingSession) {
|
||||
_checkSession();
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkSession() async {
|
||||
// Skip check if not enforcing authentication
|
||||
if (!widget.enforceAuthentication) 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;
|
||||
}
|
||||
// Session checking DISABLED for permissive mode
|
||||
debugPrint('SessionGuard: Session checking DISABLED for permissive mode');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
void _showExpiredDialog() {
|
||||
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;
|
||||
|
||||
|
||||
// Show dialog on next frame to avoid build phase issues
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) {
|
||||
_showingDialog = false;
|
||||
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(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
|
@ -113,39 +133,12 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If not enforcing authentication, just show the child
|
||||
if (!widget.enforceAuthentication) {
|
||||
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,
|
||||
debugPrint('SessionGuard: build called - PERMISSIVE MODE');
|
||||
|
||||
// PERMISSIVE MODE - Always show the child without any restrictions
|
||||
debugPrint(
|
||||
'SessionGuard: Showing child widget without session restrictions',
|
||||
);
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue