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/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;

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: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(),

View File

@ -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() {

View File

@ -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();
});

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: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

View File

@ -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> {
);
}
}

View File

@ -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;
}

View File

@ -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),

View File

@ -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) {

View File

@ -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,
),
),

View File

@ -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(

View File

@ -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;
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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,15 +26,40 @@ 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
@ -42,56 +67,43 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
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
@ -101,6 +113,14 @@ class _SessionGuardWrapperState extends State<SessionGuardWrapper> {
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;
}
debugPrint('SessionGuard: build called - PERMISSIVE MODE');
// 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,
// PERMISSIVE MODE - Always show the child without any restrictions
debugPrint(
'SessionGuard: Showing child widget without session restrictions',
);
return widget.child;
}
}