1371 lines
42 KiB
Dart
1371 lines
42 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
|
|
import 'package:tugas_akhir_supabase/screens/admin/user_management.dart';
|
|
import 'package:tugas_akhir_supabase/screens/admin/guide_management.dart';
|
|
import 'package:tugas_akhir_supabase/screens/admin/crop_management.dart';
|
|
import 'package:tugas_akhir_supabase/screens/admin/community_management.dart';
|
|
import 'package:tugas_akhir_supabase/screens/admin/news_management.dart';
|
|
import 'package:tugas_akhir_supabase/services/auth_services.dart';
|
|
import 'package:tugas_akhir_supabase/services/user_presence_service.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'dart:ui' as ui;
|
|
|
|
class AdminDashboard extends StatefulWidget {
|
|
const AdminDashboard({super.key});
|
|
|
|
@override
|
|
State<AdminDashboard> createState() => _AdminDashboardState();
|
|
}
|
|
|
|
class _AdminDashboardState extends State<AdminDashboard> {
|
|
final _authServices = GetIt.instance<AuthServices>();
|
|
UserPresenceService? _presenceService;
|
|
bool _isLoading = true;
|
|
bool _isAdmin = false;
|
|
int _currentIndex = 0;
|
|
|
|
// Statistik dashboard
|
|
int _totalUsers = 0;
|
|
int _activeUsers = 0;
|
|
int _totalPosts = 0;
|
|
int _totalGuides = 0;
|
|
int _totalNews = 0;
|
|
final int _totalCrops = 0;
|
|
|
|
// Statistik tambahan
|
|
final int _newUsersToday = 0;
|
|
final int _newPostsToday = 0;
|
|
final List<Map<String, dynamic>> _recentActivities = [];
|
|
|
|
// Real-time active users
|
|
int _realtimeActiveUsers = 0;
|
|
final Map<String, Map<String, dynamic>> _onlineUsers = {};
|
|
|
|
// Status sistem
|
|
final bool _systemStatus = true;
|
|
String _lastUpdated = '';
|
|
|
|
// Professional Color Scheme
|
|
static const Color primaryGreen = Color(0xFF0F6848);
|
|
static const Color lightGreen = Color(0xFF4CAF50);
|
|
static const Color accentGreen = Color(0xFF66BB6A);
|
|
static const Color surfaceGreen = Color(0xFFEDF7ED);
|
|
static const Color bgWhite = Color(0xFFF8F9FA);
|
|
static const Color cardWhite = Colors.white;
|
|
static const Color textPrimary = Color(0xFF1E293B);
|
|
static const Color textSecondary = Color(0xFF64748B);
|
|
static const Color dividerColor = Color(0xFFE2E8F0);
|
|
static const Color accentBlue = Color(0xFF2563EB);
|
|
static const Color accentOrange = Color(0xFFEA580C);
|
|
static const Color chartGreen = Color(0xFF10B981);
|
|
static const Color chartBlue = Color(0xFF3B82F6);
|
|
static const Color chartOrange = Color(0xFFF97316);
|
|
static const Color chartPurple = Color(0xFF8B5CF6);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// Safely initialize the presence service
|
|
try {
|
|
if (GetIt.instance.isRegistered<UserPresenceService>()) {
|
|
_presenceService = GetIt.instance<UserPresenceService>();
|
|
|
|
// Listen to online users count updates
|
|
_presenceService?.onlineUsersStream.listen((count) {
|
|
if (mounted) {
|
|
setState(() {
|
|
// Update the active users count
|
|
_activeUsers = count;
|
|
_realtimeActiveUsers = count;
|
|
});
|
|
}
|
|
});
|
|
} else {
|
|
debugPrint('UserPresenceService is not registered in GetIt');
|
|
|
|
// Register the service if needed
|
|
final currentUser = Supabase.instance.client.auth.currentUser;
|
|
if (currentUser != null) {
|
|
debugPrint('Registering UserPresenceService for admin dashboard');
|
|
GetIt.instance.registerSingleton<UserPresenceService>(
|
|
UserPresenceService(),
|
|
);
|
|
_presenceService = GetIt.instance<UserPresenceService>();
|
|
_presenceService?.initialize().then((_) {
|
|
_presenceService?.onlineUsersStream.listen((count) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_activeUsers = count;
|
|
_realtimeActiveUsers = count;
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error initializing UserPresenceService: $e');
|
|
}
|
|
|
|
_checkAdminAccess();
|
|
_loadDashboardStats();
|
|
|
|
// Tambahkan refresh otomatis setelah beberapa detik
|
|
// untuk memastikan data terambil dengan benar
|
|
Future.delayed(const Duration(seconds: 2), () {
|
|
if (mounted) {
|
|
_refreshDashboardData();
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
// Load dashboard statistics from Supabase
|
|
Future<void> _loadDashboardStats() async {
|
|
if (!mounted) return;
|
|
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
debugPrint('🔍 Starting to load dashboard stats...');
|
|
final now = DateTime.now();
|
|
final formatter = DateFormat('dd MMM yyyy HH:mm');
|
|
final client = Supabase.instance.client;
|
|
|
|
// Fetch user count using the same approach as UserManagement
|
|
try {
|
|
debugPrint('🔍 Attempting to fetch users with get_all_users RPC...');
|
|
|
|
// Try to execute the get_all_users function
|
|
final response = await client.rpc('get_all_users');
|
|
|
|
debugPrint('🔍 get_all_users response type: ${response.runtimeType}');
|
|
debugPrint('🔍 get_all_users response length: ${response.length}');
|
|
|
|
// Convert to List<Map>
|
|
final users = List<Map<String, dynamic>>.from(response);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalUsers = users.length;
|
|
debugPrint(
|
|
'✅ Fetched ${users.length} users from get_all_users RPC',
|
|
);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ Error fetching users with get_all_users: $e');
|
|
|
|
// Fallback to manual query if the RPC fails
|
|
try {
|
|
debugPrint('🔍 Falling back to manual profiles query...');
|
|
|
|
// Directly fetch all users from profiles table
|
|
final profilesResponse = await client
|
|
.from('profiles')
|
|
.select('*')
|
|
.order('created_at', ascending: false);
|
|
|
|
debugPrint('🔍 Profiles loaded: ${profilesResponse.length}');
|
|
|
|
// Convert to List<Map>
|
|
final profiles = List<Map<String, dynamic>>.from(profilesResponse);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalUsers = profiles.length;
|
|
debugPrint(
|
|
'✅ Fetched ${profiles.length} users from profiles table',
|
|
);
|
|
});
|
|
}
|
|
} catch (fallbackError) {
|
|
debugPrint('❌ Even fallback query failed: $fallbackError');
|
|
}
|
|
}
|
|
|
|
debugPrint(
|
|
'📊 Current stats after user fetch: Users: $_totalUsers, Active: $_activeUsers',
|
|
);
|
|
|
|
// Fetch guides count
|
|
try {
|
|
debugPrint('🔍 Fetching guides...');
|
|
final guidesResponse = await client.from('farming_guides').select();
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalGuides = guidesResponse.length;
|
|
debugPrint('✅ Fetched ${guidesResponse.length} guides');
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ Error fetching guides count: $e');
|
|
}
|
|
|
|
debugPrint(
|
|
'📊 Current stats: Users: $_totalUsers, Guides: $_totalGuides',
|
|
);
|
|
|
|
// Fetch news count
|
|
try {
|
|
final newsResponse = await client.from('saved_news').select();
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalNews = newsResponse.length;
|
|
debugPrint('Fetched ${newsResponse.length} news articles');
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching news count: $e');
|
|
}
|
|
|
|
// Fetch community messages count
|
|
try {
|
|
final postsResponse = await client.from('community_messages').select();
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalPosts = postsResponse.length;
|
|
debugPrint('Fetched ${postsResponse.length} community posts');
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching community posts count: $e');
|
|
}
|
|
|
|
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) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _checkAdminAccess() async {
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final isAdmin = await _authServices.isAdmin();
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_isAdmin = isAdmin;
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
|
|
if (!isAdmin) {
|
|
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),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error checking admin access: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: ${e.toString()}'),
|
|
backgroundColor: Colors.red.shade400,
|
|
behavior: SnackBarBehavior.floating,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshDashboardData() async {
|
|
if (mounted) {
|
|
debugPrint('🔄 Refreshing dashboard data...');
|
|
await _loadDashboardStats();
|
|
|
|
// Update active users count
|
|
try {
|
|
if (_presenceService != null) {
|
|
debugPrint('🔄 Fetching online users from presence service...');
|
|
final onlineUsers = await _presenceService!.getOnlineUsers();
|
|
if (mounted) {
|
|
setState(() {
|
|
_activeUsers = onlineUsers.length;
|
|
_realtimeActiveUsers = onlineUsers.length;
|
|
debugPrint('🔄 Updated active users count: $_activeUsers');
|
|
});
|
|
}
|
|
} else {
|
|
// Try to get the service if it was registered after initialization
|
|
try {
|
|
if (GetIt.instance.isRegistered<UserPresenceService>()) {
|
|
debugPrint('🔄 Getting UserPresenceService from GetIt...');
|
|
_presenceService = GetIt.instance<UserPresenceService>();
|
|
final onlineUsers = await _presenceService!.getOnlineUsers();
|
|
if (mounted) {
|
|
setState(() {
|
|
_activeUsers = onlineUsers.length;
|
|
_realtimeActiveUsers = onlineUsers.length;
|
|
debugPrint('🔄 Updated active users count: $_activeUsers');
|
|
});
|
|
}
|
|
} else {
|
|
debugPrint('⚠️ UserPresenceService not registered in GetIt');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ Error getting UserPresenceService: $e');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint('❌ Error getting online users: $e');
|
|
}
|
|
|
|
debugPrint(
|
|
'🔄 Refresh complete. Users: $_totalUsers, Active: $_activeUsers',
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _buildStatsGrid() {
|
|
return GridView.count(
|
|
crossAxisCount: 2,
|
|
childAspectRatio: 1.2,
|
|
padding: const EdgeInsets.all(4),
|
|
mainAxisSpacing: 12,
|
|
crossAxisSpacing: 12,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
children: [
|
|
_buildStatCard(
|
|
title: 'Total Pengguna',
|
|
value: '$_totalUsers',
|
|
subtitle: '+$_newUsersToday today',
|
|
icon: Icons.people,
|
|
color: chartBlue,
|
|
),
|
|
_buildActiveUsersCard(),
|
|
_buildStatCard(
|
|
title: 'News',
|
|
value: '$_totalNews',
|
|
subtitle: 'Saved articles',
|
|
icon: Icons.newspaper,
|
|
color: chartPurple,
|
|
),
|
|
_buildStatCard(
|
|
title: 'Panduan',
|
|
value: '$_totalGuides',
|
|
subtitle: 'Published guides',
|
|
icon: Icons.book,
|
|
color: chartOrange,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildActiveUsersCard() {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: chartGreen.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.people, color: chartGreen, size: 16),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'$_realtimeActiveUsers',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Pengguna Aktif',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: textSecondary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'Online saat ini',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: chartGreen.withOpacity(0.8),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCard({
|
|
required String title,
|
|
required String value,
|
|
required String subtitle,
|
|
required IconData icon,
|
|
required Color color,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(6),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(icon, color: color, size: 16),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: textSecondary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
color: color.withOpacity(0.8),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@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();
|
|
}
|
|
|
|
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);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
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',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOverviewTab() {
|
|
return RefreshIndicator(
|
|
onRefresh: _refreshDashboardData,
|
|
color: primaryGreen,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header Section
|
|
_buildHeaderSection(),
|
|
const SizedBox(height: 20),
|
|
|
|
// Main Statistics Grid
|
|
_buildMainStatsGrid(),
|
|
const SizedBox(height: 24),
|
|
|
|
// Quick Actions Section
|
|
_buildQuickActionsSection(),
|
|
const SizedBox(height: 24),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildHeaderSection() {
|
|
final now = DateTime.now();
|
|
final greeting =
|
|
now.hour < 12
|
|
? 'Good Morning'
|
|
: now.hour < 17
|
|
? 'Good Afternoon'
|
|
: 'Good Evening';
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
const Color(0xFF0F172A), // Slate-900
|
|
const Color(0xFF1E293B), // Slate-800
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF0F172A).withOpacity(0.2),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
spreadRadius: -2,
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Top row with timestamp and refresh button
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [_buildTimeStamp(), _buildActionButton()],
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// System health card (wider)
|
|
_buildSystemMetricsWide(),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Greeting text
|
|
Text(
|
|
greeting,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF10B981),
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 4),
|
|
|
|
// Welcome text with glow
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF10B981).withOpacity(0.2),
|
|
blurRadius: 10,
|
|
spreadRadius: 1,
|
|
),
|
|
],
|
|
),
|
|
child: const Text(
|
|
'Welcome to TaniSMART',
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
height: 1.1,
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Quick stats row
|
|
_buildQuickStats(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTimeStamp() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
|
),
|
|
child: Text(
|
|
'Last updated: $_lastUpdated',
|
|
style: TextStyle(
|
|
fontSize: 10,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white.withOpacity(0.7),
|
|
letterSpacing: 0.2,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickStats() {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: [
|
|
_buildStatPill(
|
|
label: 'Users',
|
|
value: '$_totalUsers',
|
|
icon: Icons.people_alt_outlined,
|
|
color: chartBlue,
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildStatPill(
|
|
label: 'Online',
|
|
value: '$_realtimeActiveUsers',
|
|
icon: Icons.online_prediction_outlined,
|
|
color: chartGreen,
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildStatPill(
|
|
label: 'News',
|
|
value: '$_totalNews',
|
|
icon: Icons.newspaper_outlined,
|
|
color: chartPurple,
|
|
),
|
|
const SizedBox(width: 8),
|
|
_buildStatPill(
|
|
label: 'Guides',
|
|
value: '$_totalGuides',
|
|
icon: Icons.menu_book_outlined,
|
|
color: chartOrange,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatPill({
|
|
required String label,
|
|
required String value,
|
|
required IconData icon,
|
|
required Color color,
|
|
}) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.08),
|
|
borderRadius: BorderRadius.circular(20),
|
|
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 12, color: color),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
'$label: ',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.white.withOpacity(0.7),
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSystemMetricsWide() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withOpacity(0.05),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color:
|
|
_systemStatus
|
|
? const Color(0xFF10B981).withOpacity(0.2)
|
|
: const Color(0xFFEF4444).withOpacity(0.2),
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color:
|
|
_systemStatus
|
|
? const Color(0xFF10B981).withOpacity(0.4)
|
|
: const Color(0xFFEF4444).withOpacity(0.4),
|
|
width: 1,
|
|
),
|
|
),
|
|
child: Icon(
|
|
_systemStatus ? Icons.eco_rounded : Icons.warning_rounded,
|
|
color:
|
|
_systemStatus
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFEF4444),
|
|
size: 16,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_systemStatus ? '99.8%' : '87.3%',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: Colors.white,
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
Text(
|
|
'System Health',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.white.withOpacity(0.7),
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color:
|
|
_systemStatus
|
|
? const Color(0xFF10B981).withOpacity(0.2)
|
|
: const Color(0xFFEF4444).withOpacity(0.2),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
_systemStatus ? 'Optimal' : 'Attention',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color:
|
|
_systemStatus
|
|
? const Color(0xFF10B981)
|
|
: const Color(0xFFEF4444),
|
|
letterSpacing: 0.5,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButton() {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
gradient: const LinearGradient(
|
|
colors: [Color(0xFF10B981), Color(0xFF059669)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF10B981).withOpacity(0.3),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: _refreshDashboardData,
|
|
borderRadius: BorderRadius.circular(12),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
Icons.refresh_rounded,
|
|
color: Colors.white,
|
|
size: 16,
|
|
),
|
|
const SizedBox(width: 3),
|
|
const Text(
|
|
'Refresh Data',
|
|
style: TextStyle(
|
|
fontSize: 8,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.white,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMainStatsGrid() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Platform Overview',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
TextButton.icon(
|
|
onPressed: _refreshDashboardData,
|
|
icon: Icon(
|
|
Icons.analytics_outlined,
|
|
size: 14,
|
|
color: accentBlue,
|
|
),
|
|
label: Text(
|
|
'View Analytics',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: accentBlue,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
style: TextButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildStatsGrid(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActionsSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Quick Actions',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: cardWhite,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.grey.shade100),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
_buildQuickActionItem(
|
|
title: 'Manage Users',
|
|
subtitle: '$_totalUsers total users, $_activeUsers active',
|
|
icon: Icons.people_alt_rounded,
|
|
iconColor: chartBlue,
|
|
onTap: () => setState(() => _currentIndex = 1),
|
|
),
|
|
Divider(height: 1, color: dividerColor),
|
|
_buildQuickActionItem(
|
|
title: 'Manage Guides',
|
|
subtitle: '$_totalGuides farming guides',
|
|
icon: Icons.menu_book_rounded,
|
|
iconColor: chartOrange,
|
|
onTap: () => setState(() => _currentIndex = 2),
|
|
),
|
|
Divider(height: 1, color: dividerColor),
|
|
_buildQuickActionItem(
|
|
title: 'News Management',
|
|
subtitle: '$_totalNews saved news articles',
|
|
icon: Icons.newspaper_rounded,
|
|
iconColor: chartPurple,
|
|
onTap: () => setState(() => _currentIndex = 4),
|
|
),
|
|
Divider(height: 1, color: dividerColor),
|
|
_buildQuickActionItem(
|
|
title: 'Community Management',
|
|
subtitle: '$_totalPosts posts, +$_newPostsToday today',
|
|
icon: Icons.forum_rounded,
|
|
iconColor: chartGreen,
|
|
onTap: () => setState(() => _currentIndex = 3),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickActionItem({
|
|
required String title,
|
|
required String subtitle,
|
|
required IconData icon,
|
|
required Color iconColor,
|
|
required VoidCallback onTap,
|
|
}) {
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: iconColor.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(icon, size: 18, color: iconColor),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: textPrimary,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
subtitle,
|
|
style: TextStyle(fontSize: 12, color: textSecondary),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: Colors.grey.shade400, size: 18),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentActivitiesSection() {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 16),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Recent Activities',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: textPrimary,
|
|
letterSpacing: 0.3,
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: _refreshDashboardData,
|
|
style: TextButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: Text(
|
|
'View All',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: accentBlue,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
_buildRecentActivitiesList(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecentActivitiesList() {
|
|
if (_recentActivities.isEmpty) {
|
|
return Container(
|
|
padding: const EdgeInsets.all(32),
|
|
decoration: BoxDecoration(
|
|
color: cardWhite,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.grey.shade100),
|
|
),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.inbox_rounded,
|
|
size: 48,
|
|
color: textSecondary.withOpacity(0.7),
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'No recent activities',
|
|
style: TextStyle(
|
|
color: textSecondary,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
ElevatedButton(
|
|
onPressed: _refreshDashboardData,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: primaryGreen,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 8,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
child: const Text('Refresh Data'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: cardWhite,
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
border: Border.all(color: Colors.grey.shade100),
|
|
),
|
|
child: ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: _recentActivities.length,
|
|
separatorBuilder:
|
|
(context, index) => Divider(height: 1, color: dividerColor),
|
|
itemBuilder: (context, index) {
|
|
final activity = _recentActivities[index];
|
|
final username =
|
|
activity['profiles']?['username'] ?? 'Anonymous User';
|
|
final content = activity['content'] ?? '';
|
|
final createdAt =
|
|
activity['created_at'] != null
|
|
? DateFormat(
|
|
'MMM dd, HH:mm',
|
|
).format(DateTime.parse(activity['created_at']))
|
|
: '';
|
|
|
|
// Get avatar URL if available
|
|
final avatarUrl = activity['profiles']?['avatar_url'];
|
|
|
|
return ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
leading: Container(
|
|
width: 44,
|
|
height: 44,
|
|
decoration: BoxDecoration(
|
|
color: chartBlue.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: Colors.grey.shade100),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(10),
|
|
child:
|
|
avatarUrl != null && avatarUrl.toString().isNotEmpty
|
|
? Image.network(
|
|
avatarUrl,
|
|
fit: BoxFit.cover,
|
|
errorBuilder:
|
|
(context, error, stackTrace) => Icon(
|
|
Icons.person_rounded,
|
|
color: chartBlue,
|
|
size: 22,
|
|
),
|
|
)
|
|
: Icon(
|
|
Icons.person_rounded,
|
|
color: chartBlue,
|
|
size: 22,
|
|
),
|
|
),
|
|
),
|
|
title: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
username,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: textPrimary,
|
|
fontSize: 15,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Text(
|
|
createdAt,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
color: textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
subtitle: Padding(
|
|
padding: const EdgeInsets.only(top: 6),
|
|
child: Text(
|
|
content.length > 100
|
|
? '${content.substring(0, 100)}...'
|
|
: content,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(color: textSecondary, fontSize: 13),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|