1311 lines
40 KiB
Dart
1311 lines
40 KiB
Dart
import 'dart:io';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:percent_indicator/circular_percent_indicator.dart';
|
|
import 'package:tugas_akhir_supabase/services/session_manager.dart';
|
|
import 'package:tugas_akhir_supabase/services/auth_services.dart';
|
|
import 'package:tugas_akhir_supabase/utils/session_checker_mixin.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
|
|
class ProfileScreen extends StatefulWidget {
|
|
const ProfileScreen({super.key});
|
|
|
|
@override
|
|
_ProfileScreenState createState() => _ProfileScreenState();
|
|
}
|
|
|
|
class _ProfileScreenState extends State<ProfileScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _usernameController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _phoneController = TextEditingController();
|
|
final _addressController = TextEditingController();
|
|
final _farmNameController = TextEditingController();
|
|
|
|
final SupabaseClient _supabase = Supabase.instance.client;
|
|
final ImagePicker _picker = ImagePicker();
|
|
String? _avatarUrl;
|
|
bool _isLoading = false;
|
|
User? _user;
|
|
|
|
// Statistics data
|
|
int _totalFields = 0;
|
|
int _activeSchedules = 0;
|
|
int _completedHarvests = 0;
|
|
double _averageYield = 0;
|
|
final String _mostPlantedCrop = '-';
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
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() {
|
|
_usernameController.dispose();
|
|
_emailController.dispose();
|
|
_phoneController.dispose();
|
|
_addressController.dispose();
|
|
_farmNameController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// Refresh admin status setiap kali halaman dimuat ulang
|
|
// _checkAdminStatus(); // Removed as per new_code
|
|
}
|
|
|
|
Future<void> _loadProfile() async {
|
|
if (_user == null) return;
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final response =
|
|
await _supabase
|
|
.from('profiles')
|
|
.select(
|
|
'user_id, username, email, phone, address, avatar_url, farm_name',
|
|
)
|
|
.eq('user_id', _user!.id)
|
|
.maybeSingle();
|
|
|
|
if (response == null) {
|
|
await _createProfile(); // Pastikan ini membuat semua field yang dibutuhkan
|
|
|
|
final newProfile =
|
|
await _supabase
|
|
.from('profiles')
|
|
.select(
|
|
'user_id, username, email, phone, address, avatar_url, farm_name',
|
|
)
|
|
.eq('user_id', _user!.id)
|
|
.single();
|
|
|
|
_updateControllers(newProfile);
|
|
} else {
|
|
_updateControllers(response);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error loading profile: $e');
|
|
_showErrorSnackbar('Gagal memuat profil: ${e.toString()}');
|
|
} finally {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadStatistics() async {
|
|
if (_user == null) return;
|
|
|
|
try {
|
|
// Reset values first
|
|
setState(() {
|
|
_totalFields = 0;
|
|
_activeSchedules = 0;
|
|
_completedHarvests = 0;
|
|
_averageYield = 0.0;
|
|
}); // Fetch fields count safely
|
|
final fieldsResponse = await _supabase
|
|
.from('fields')
|
|
.select('id')
|
|
.eq('user_id', _user!.id);
|
|
|
|
_totalFields = fieldsResponse.length;
|
|
|
|
// Fetch active schedules safely
|
|
final now = DateTime.now().toIso8601String();
|
|
final schedulesResponse = await _supabase
|
|
.from('crop_schedules')
|
|
.select()
|
|
.eq('user_id', _user!.id)
|
|
.gt('end_date', now);
|
|
|
|
_activeSchedules = schedulesResponse.length;
|
|
|
|
// Fetch harvest results safely
|
|
final harvestResponse = await _supabase
|
|
.from('harvest_results')
|
|
.select('productivity')
|
|
.eq('user_id', _user!.id);
|
|
|
|
if (harvestResponse.isNotEmpty) {
|
|
_completedHarvests = harvestResponse.length;
|
|
|
|
// Calculate average yield safely
|
|
double totalYield = 0;
|
|
int validRecords = 0;
|
|
|
|
for (final harvest in harvestResponse) {
|
|
final productivity = harvest['productivity'] as num?;
|
|
if (productivity != null) {
|
|
totalYield += productivity.toDouble();
|
|
validRecords++;
|
|
}
|
|
}
|
|
|
|
_averageYield = validRecords > 0 ? totalYield / validRecords : 0.0;
|
|
}
|
|
|
|
if (mounted) setState(() {});
|
|
} catch (e) {
|
|
debugPrint('Error loading statistics: $e');
|
|
if (mounted) {
|
|
setState(() {
|
|
_totalFields = 0;
|
|
_activeSchedules = 0;
|
|
_completedHarvests = 0;
|
|
_averageYield = 0.0;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _createProfile() async {
|
|
if (_user == null) return;
|
|
|
|
try {
|
|
final username =
|
|
_user!.email?.split('@').first ??
|
|
'user_${DateTime.now().millisecondsSinceEpoch}';
|
|
|
|
await _supabase.from('profiles').insert({
|
|
'user_id': _user!.id,
|
|
'username': username,
|
|
'email': _user!.email ?? '',
|
|
'created_at': DateTime.now().toUtc().toIso8601String(),
|
|
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Error creating profile: $e');
|
|
_showErrorSnackbar('Error membuat profil: ${e.toString()}');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void _updateControllers(Map<String, dynamic> data) {
|
|
_usernameController.text = data['username'] ?? '';
|
|
_emailController.text = data['email'] ?? '';
|
|
_phoneController.text = data['phone'] ?? '';
|
|
_addressController.text = data['address'] ?? '';
|
|
_farmNameController.text = data['farm_name'] ?? '';
|
|
|
|
setState(() {
|
|
_avatarUrl = data['avatar_url'] ?? '';
|
|
});
|
|
}
|
|
|
|
Future<void> _updateProfile() async {
|
|
if (!_formKey.currentState!.validate() || _user == null) return;
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final updates = {
|
|
'username': _usernameController.text.trim(),
|
|
'phone': _phoneController.text.trim(),
|
|
'address': _addressController.text.trim(),
|
|
'farm_name': _farmNameController.text.trim(),
|
|
'updated_at': DateTime.now().toUtc().toIso8601String(),
|
|
};
|
|
|
|
await _supabase
|
|
.from('profiles')
|
|
.update(updates)
|
|
.eq('user_id', _user!.id)
|
|
.select();
|
|
|
|
_showSuccessSnackbar('Profil berhasil diperbarui');
|
|
} catch (e) {
|
|
debugPrint('Error updating profile: $e');
|
|
_showErrorSnackbar('Error memperbarui profil: ${e.toString()}');
|
|
} finally {
|
|
if (mounted) setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
|
|
Future<void> _uploadAvatar() async {
|
|
final picked = await _picker.pickImage(source: ImageSource.gallery);
|
|
if (picked == null) return;
|
|
|
|
try {
|
|
final file = File(picked.path);
|
|
final fileExt = picked.path.split('.').last;
|
|
final filePath = 'avatars/${_user!.id}/avatar.$fileExt';
|
|
|
|
await _supabase.storage
|
|
.from('avatars')
|
|
.upload(
|
|
filePath,
|
|
file,
|
|
fileOptions: FileOptions(
|
|
upsert: true,
|
|
contentType: 'image/$fileExt',
|
|
),
|
|
);
|
|
|
|
// Get the public URL instead of a signed URL
|
|
final avatarUrl = _supabase.storage
|
|
.from('avatars')
|
|
.getPublicUrl(filePath);
|
|
|
|
await _supabase
|
|
.from('profiles')
|
|
.update({'avatar_url': avatarUrl})
|
|
.eq('user_id', _user!.id);
|
|
|
|
setState(() {
|
|
_avatarUrl = avatarUrl;
|
|
});
|
|
|
|
_showSuccessSnackbar('Avatar berhasil diunggah');
|
|
} catch (e) {
|
|
debugPrint('Upload error: $e');
|
|
_showErrorSnackbar('Gagal mengunggah avatar');
|
|
}
|
|
}
|
|
|
|
void _showErrorSnackbar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: Colors.red),
|
|
);
|
|
}
|
|
|
|
void _showSuccessSnackbar(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: Colors.green),
|
|
);
|
|
}
|
|
|
|
Future<void> _signOut() async {
|
|
try {
|
|
final authServices = GetIt.instance<AuthServices>();
|
|
await authServices.signOut();
|
|
|
|
if (mounted) {
|
|
Navigator.of(context).pushReplacementNamed('/login');
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error during sign out: $e');
|
|
// Fallback to direct sign out
|
|
await _supabase.auth.signOut();
|
|
if (mounted) {
|
|
Navigator.of(context).pushReplacementNamed('/login');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Simple admin check without complex session management
|
|
Future<bool> _isUserAdmin() async {
|
|
try {
|
|
if (_user == null) return false;
|
|
final authServices = GetIt.instance<AuthServices>();
|
|
return await authServices.isAdmin();
|
|
} catch (e) {
|
|
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(
|
|
child: CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(const Color(0xFF056839)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
debugPrint('ProfileScreen: Showing main profile screen');
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF8F9FA),
|
|
appBar: _buildAppBar(),
|
|
body: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
_buildProfileHeader(),
|
|
const SizedBox(height: 20),
|
|
_buildFarmStatsSummary(),
|
|
const SizedBox(height: 20),
|
|
_buildProfileForm(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
PreferredSizeWidget _buildAppBar() {
|
|
return AppBar(
|
|
elevation: 0,
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.black87,
|
|
centerTitle: false,
|
|
title: Text(
|
|
'Profil Saya',
|
|
style: GoogleFonts.poppins(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
actions: [
|
|
// Tombol admin dengan tooltip yang sesuai
|
|
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',
|
|
);
|
|
},
|
|
),
|
|
IconButton(
|
|
icon: Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(Icons.logout, size: 18, color: Colors.red[700]),
|
|
),
|
|
onPressed: _signOut,
|
|
tooltip: 'Keluar',
|
|
),
|
|
const SizedBox(width: 8),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildProfileHeader() {
|
|
return Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
offset: const Offset(0, 2),
|
|
blurRadius: 8,
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
_buildAvatarWithEditButton(),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_usernameController.text,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
Text(
|
|
_emailController.text,
|
|
style: GoogleFonts.poppins(fontSize: 14, color: Colors.grey[600]),
|
|
textAlign: TextAlign.center,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildActionButtons(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAvatarWithEditButton() {
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: const Color(0xFF056839), width: 2),
|
|
),
|
|
child: CircleAvatar(
|
|
radius: 60,
|
|
backgroundColor: Colors.grey[200],
|
|
backgroundImage:
|
|
_avatarUrl != null && _avatarUrl!.isNotEmpty
|
|
? NetworkImage(_avatarUrl!)
|
|
: null,
|
|
child:
|
|
_avatarUrl == null || _avatarUrl!.isEmpty
|
|
? const Icon(Icons.person, size: 60, color: Colors.grey)
|
|
: null,
|
|
),
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: GestureDetector(
|
|
onTap: _uploadAvatar,
|
|
child: Container(
|
|
height: 40,
|
|
width: 40,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF056839),
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.1),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.camera_alt,
|
|
color: Colors.white,
|
|
size: 20,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildActionButtons() {
|
|
return SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Tombol Kelola Role dihapus, fungsinya dipindahkan ke tombol admin di AppBar
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showRoleManagementDialog() async {
|
|
// Check admin status first
|
|
final isAdmin = await _isUserAdmin();
|
|
|
|
// Jika bukan admin, tampilkan pesan error
|
|
if (!isAdmin) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Anda tidak memiliki akses untuk mengelola role'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
|
|
// Debug: Tampilkan user ID
|
|
debugPrint('Current user ID: ${_user?.id}');
|
|
return;
|
|
}
|
|
|
|
final userId = _user!.id;
|
|
String? currentRole;
|
|
|
|
try {
|
|
// Ambil role pengguna saat ini
|
|
final roleResponse =
|
|
await Supabase.instance.client
|
|
.from('user_roles')
|
|
.select('role')
|
|
.eq('user_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (roleResponse != null) {
|
|
currentRole = roleResponse['role'] as String?;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error fetching user role: $e');
|
|
}
|
|
|
|
if (!mounted) return;
|
|
|
|
// Tampilkan dialog untuk mengelola role
|
|
showDialog(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('Kelola Role Pengguna'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('User ID: ${userId.substring(0, 8)}...'),
|
|
Text('Email: ${_emailController.text}'),
|
|
const SizedBox(height: 16),
|
|
const Text('Pilih Role:'),
|
|
const SizedBox(height: 8),
|
|
_buildRoleOption('admin', 'Admin', currentRole == 'admin'),
|
|
_buildRoleOption(
|
|
'user',
|
|
'User',
|
|
currentRole == 'user' || currentRole == null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Catatan: Admin tetap memiliki akses ke semua fitur admin dan user.',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontStyle: FontStyle.italic,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
// Simpan perubahan role
|
|
final newRole = currentRole == 'admin' ? 'user' : 'admin';
|
|
|
|
// Jika sedang mengubah dari admin ke user
|
|
final isDowngradingToUser =
|
|
currentRole == 'admin' && newRole == 'user';
|
|
final isCurrentUser = _user!.id == userId;
|
|
|
|
if (isDowngradingToUser) {
|
|
// Periksa jumlah admin yang ada
|
|
final authServices = GetIt.instance<AuthServices>();
|
|
final adminCount = await authServices.countAdmins();
|
|
debugPrint('Current admin count: $adminCount');
|
|
|
|
// Jika hanya ada 1 admin dan kita mencoba menurunkan admin terakhir
|
|
if (adminCount <= 1) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'Tidak dapat menurunkan admin terakhir. Harus ada minimal satu admin dalam sistem.',
|
|
),
|
|
backgroundColor: Colors.red,
|
|
duration: Duration(seconds: 5),
|
|
),
|
|
);
|
|
Navigator.of(context).pop();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Jika ini adalah pengguna saat ini yang menurunkan dirinya sendiri
|
|
if (isCurrentUser) {
|
|
// Tampilkan konfirmasi khusus
|
|
final confirmDowngrade = await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: const Text('Peringatan'),
|
|
content: const Text(
|
|
'Anda akan menurunkan hak akses Anda sendiri dari admin menjadi user. '
|
|
'Anda tidak akan dapat mengakses fitur admin lagi kecuali ada admin lain '
|
|
'yang mengembalikan hak akses Anda.\n\n'
|
|
'Apakah Anda yakin?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed:
|
|
() => Navigator.of(context).pop(false),
|
|
child: const Text('Batal'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed:
|
|
() => Navigator.of(context).pop(true),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
),
|
|
child: const Text('Ya, Saya Yakin'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmDowngrade != true) {
|
|
return; // Batal jika pengguna tidak mengkonfirmasi
|
|
}
|
|
}
|
|
}
|
|
|
|
await _updateUserRole(userId, newRole);
|
|
if (mounted) Navigator.of(context).pop();
|
|
},
|
|
child: Text(
|
|
currentRole == 'admin' ? 'Jadikan User' : 'Jadikan Admin',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRoleOption(String role, String label, bool isSelected) {
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(
|
|
color: isSelected ? Colors.blue : Colors.grey.shade300,
|
|
width: isSelected ? 2 : 1,
|
|
),
|
|
color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.transparent,
|
|
),
|
|
child: ListTile(
|
|
title: Text(label),
|
|
leading: Icon(
|
|
role == 'admin' ? Icons.admin_panel_settings : Icons.person,
|
|
color: isSelected ? Colors.blue : Colors.grey,
|
|
),
|
|
trailing:
|
|
isSelected
|
|
? const Icon(Icons.check_circle, color: Colors.blue)
|
|
: null,
|
|
dense: true,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _updateUserRole(String userId, String newRole) async {
|
|
try {
|
|
// Cek apakah pengguna sudah memiliki role
|
|
final existingRole =
|
|
await Supabase.instance.client
|
|
.from('user_roles')
|
|
.select()
|
|
.eq('user_id', userId)
|
|
.maybeSingle();
|
|
|
|
if (existingRole != null) {
|
|
// Update role jika sudah ada
|
|
await Supabase.instance.client
|
|
.from('user_roles')
|
|
.update({'role': newRole})
|
|
.eq('user_id', userId);
|
|
} else {
|
|
// Tambahkan role baru jika belum ada
|
|
await Supabase.instance.client.from('user_roles').insert({
|
|
'user_id': userId,
|
|
'role': newRole,
|
|
});
|
|
}
|
|
|
|
// Refresh status admin
|
|
// _checkAdminStatus(); // Removed as per new_code
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Berhasil mengubah role menjadi $newRole'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error updating user role: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error: ${e.toString()}'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildFarmStatsSummary() {
|
|
final currency = NumberFormat.currency(locale: 'id_ID', symbol: 'Rp ');
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
spreadRadius: 1,
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Statistik Pertanian',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 6,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF056839).withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
'Aktif',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF056839),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// Performance indicators
|
|
_buildPerformanceIndicators(),
|
|
|
|
const Divider(height: 32),
|
|
|
|
// Financial summary
|
|
_buildFinancialSummary(currency),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPerformanceIndicators() {
|
|
// Calculate percentage for circular indicator based on average yield
|
|
double yieldPercentage = 0.0;
|
|
if (_averageYield > 0) {
|
|
// Assuming optimal yield is 8 ton/ha, calculate percentage
|
|
yieldPercentage = (_averageYield / 8.0).clamp(0.0, 1.0);
|
|
}
|
|
|
|
return Row(
|
|
children: [
|
|
Expanded(
|
|
child: CircularPercentIndicator(
|
|
radius: 60.0,
|
|
lineWidth: 10.0,
|
|
percent: yieldPercentage,
|
|
center: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
_averageYield.toStringAsFixed(1),
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
'ton/ha',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 12,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
progressColor: const Color(0xFF056839),
|
|
backgroundColor: const Color(0xFF056839).withOpacity(0.2),
|
|
animation: true,
|
|
animationDuration: 1200,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildStatRow('Total Lahan', '$_totalFields Lahan'),
|
|
const SizedBox(height: 8),
|
|
_buildStatRow('Tanaman Aktif', '$_activeSchedules Jenis'),
|
|
const SizedBox(height: 8),
|
|
_buildStatRow('Total Panen', '$_completedHarvests Kali'),
|
|
const SizedBox(height: 8),
|
|
_buildStatRow('Tanaman Terbanyak', _mostPlantedCrop),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildFinancialSummary(NumberFormat currency) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: Column(
|
|
children: [
|
|
IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'Rata-rata Panen',
|
|
'${(_averageYield * 10).toStringAsFixed(1)} kilogram/ha',
|
|
Icons.trending_up,
|
|
const Color(0xFF056839),
|
|
'Rata-rata hasil panen per hektar',
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'Total Panen',
|
|
'$_completedHarvests Kali',
|
|
Icons.check_circle_outline,
|
|
Colors.blue.shade700,
|
|
'Jumlah panen yang telah dilakukan',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
IntrinsicHeight(
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'Musim Tanam',
|
|
'$_activeSchedules Aktif',
|
|
Icons.calendar_today,
|
|
Colors.orange.shade700,
|
|
'Jumlah tanaman yang sedang ditanam',
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: _buildMetricCard(
|
|
'Total Lahan',
|
|
'$_totalFields Lahan',
|
|
Icons.eco,
|
|
Colors.green.shade700,
|
|
'Jumlah lahan yang dimiliki',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildMetricCard(
|
|
String title,
|
|
String value,
|
|
IconData icon,
|
|
Color color,
|
|
String tooltip,
|
|
) {
|
|
return Container(
|
|
constraints: const BoxConstraints(minHeight: 100),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: color.withOpacity(0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: color.withOpacity(0.2)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 18, color: color),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
title,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13,
|
|
color: color,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
value,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatRow(String label, String value) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Expanded(
|
|
flex: 3,
|
|
child: Text(
|
|
label,
|
|
style: GoogleFonts.poppins(fontSize: 14, color: Colors.grey[700]),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
value,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
textAlign: TextAlign.end,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildProfileForm() {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
spreadRadius: 1,
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Informasi Pengguna',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildInputField(
|
|
controller: _usernameController,
|
|
label: 'Nama Pengguna',
|
|
icon: Icons.person_outlined,
|
|
validator:
|
|
(value) =>
|
|
value == null || value.isEmpty
|
|
? 'Nama pengguna wajib diisi'
|
|
: null,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInputField(
|
|
controller: _emailController,
|
|
label: 'Email',
|
|
icon: Icons.email_outlined,
|
|
readOnly: true,
|
|
),
|
|
// const SizedBox(height: 16),
|
|
// _buildInputField(
|
|
// controller: _farmNameController,
|
|
// label: 'Nama Lahan',
|
|
// icon: Icons.agriculture_outlined,
|
|
// validator: (value) =>
|
|
// value == null || value.isEmpty
|
|
// ? 'Nama lahan wajib diisi'
|
|
// : null,
|
|
// ),
|
|
const SizedBox(height: 16),
|
|
_buildInputField(
|
|
controller: _phoneController,
|
|
label: 'No. Telepon',
|
|
icon: Icons.phone_outlined,
|
|
keyboardType: TextInputType.phone,
|
|
),
|
|
const SizedBox(height: 16),
|
|
_buildInputField(
|
|
controller: _addressController,
|
|
label: 'Alamat',
|
|
icon: Icons.location_on_outlined,
|
|
maxLines: 3,
|
|
),
|
|
const SizedBox(height: 30),
|
|
_buildSaveButton(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSaveButton() {
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 55,
|
|
child: ElevatedButton(
|
|
onPressed: _updateProfile,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 2,
|
|
),
|
|
child:
|
|
_isLoading
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 3,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
)
|
|
: Text(
|
|
'Simpan Perubahan',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInputField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
required IconData icon,
|
|
String? Function(String?)? validator,
|
|
bool readOnly = false,
|
|
TextInputType? keyboardType,
|
|
int? maxLines,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4, bottom: 8),
|
|
child: Text(
|
|
label,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
color: Colors.grey[700],
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: Colors.grey[300]!),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.03),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: TextFormField(
|
|
controller: controller,
|
|
decoration: InputDecoration(
|
|
prefixIcon: Icon(icon, color: const Color(0xFF056839), size: 22),
|
|
border: InputBorder.none,
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
vertical: 16,
|
|
horizontal: 8,
|
|
),
|
|
fillColor: readOnly ? Colors.grey[50] : Colors.white,
|
|
filled: true,
|
|
),
|
|
style: GoogleFonts.poppins(fontSize: 15),
|
|
readOnly: readOnly,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines ?? 1,
|
|
validator: validator,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNoUserScreen() {
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF8F9FA),
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(30),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.person_off_outlined,
|
|
size: 70,
|
|
color: Colors.grey[400],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Silakan masuk untuk melihat profil',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Anda perlu masuk ke akun untuk mengakses fitur ini',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: Colors.grey[600],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 40),
|
|
Container(
|
|
width: 200,
|
|
height: 55,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(12),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: const Color(0xFF056839).withOpacity(0.3),
|
|
blurRadius: 12,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: ElevatedButton(
|
|
onPressed:
|
|
() =>
|
|
Navigator.of(context).pushReplacementNamed('/login'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: const Color(0xFF056839),
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 32,
|
|
vertical: 15,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: Text(
|
|
'Masuk',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|