environment_monitoring_app/lib/screens/profile.dart
2025-08-04 15:11:24 +08:00

358 lines
13 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import 'package:environment_monitoring_app/auth_provider.dart';
import 'package:environment_monitoring_app/services/api_service.dart';
class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
final ApiService _apiService = ApiService();
bool _isLoading = false;
String _errorMessage = '';
File? _profileImageFile;
@override
void initState() {
super.initState();
// Load the image from cache first, then refresh data from the provider
_loadLocalProfileImage().then((_) {
// If no profile data is available at all, trigger a refresh
if (Provider.of<AuthProvider>(context, listen: false).profileData == null) {
_refreshProfile();
}
});
}
/// Refreshes only the profile data using the dedicated provider method.
Future<void> _refreshProfile() async {
setState(() {
_isLoading = true;
_errorMessage = '';
});
try {
final auth = Provider.of<AuthProvider>(context, listen: false);
// Call the efficient refreshProfile method instead of the full sync.
await auth.refreshProfile();
// After syncing, reload the potentially new profile image
await _loadLocalProfileImage();
} catch (e) {
if (mounted) {
setState(() {
_errorMessage = 'An unexpected error occurred during sync: ${e.toString()}';
});
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// Loads the profile image from the local cache or downloads it if not present.
Future<void> _loadLocalProfileImage() async {
final auth = Provider.of<AuthProvider>(context, listen: false);
final String? serverImagePath = auth.profileData?['profile_picture'];
if (serverImagePath != null && serverImagePath.isNotEmpty) {
final String localFileName = p.basename(serverImagePath);
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String localFilePath = p.join(appDocDir.path, 'profile_pictures', localFileName);
final File localFile = File(localFilePath);
if (await localFile.exists()) {
if (mounted) setState(() => _profileImageFile = localFile);
} else {
final String fullImageUrl = ApiService.imageBaseUrl + serverImagePath;
final downloadedFile = await _apiService.downloadProfilePicture(fullImageUrl, localFilePath);
if (downloadedFile != null && mounted) {
setState(() => _profileImageFile = downloadedFile);
}
}
} else {
if (mounted) setState(() => _profileImageFile = null);
}
}
/// Shows a modal bottom sheet for selecting an image source.
Future<void> _showImageSourceSelection() async {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('Take a photo'),
onTap: () {
Navigator.pop(context);
_pickAndUploadImage(ImageSource.camera);
},
),
ListTile(
leading: const Icon(Icons.photo_library),
title: const Text('Choose from gallery'),
onTap: () {
Navigator.pop(context);
_pickAndUploadImage(ImageSource.gallery);
},
),
],
),
);
},
);
}
/// Picks an image and initiates the upload process.
Future<void> _pickAndUploadImage(ImageSource source) async {
final ImagePicker picker = ImagePicker();
final XFile? pickedFile = await picker.pickImage(source: source, imageQuality: 85, maxWidth: 1024);
if (pickedFile != null) {
setState(() => _isLoading = true);
final File imageFile = File(pickedFile.path);
final uploadResult = await _apiService.uploadProfilePicture(imageFile);
if (mounted) {
if (uploadResult['success']) {
// After a successful upload, efficiently refresh only the profile data.
await _refreshProfile();
_showSnackBar("Profile picture updated successfully.", isError: false);
} else {
setState(() {
_errorMessage = uploadResult['message'] ?? 'Failed to upload profile picture.';
});
_showSnackBar(_errorMessage, isError: true);
}
setState(() => _isLoading = false);
}
}
}
void _showSnackBar(String message, {bool isError = false}) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Theme.of(context).colorScheme.error : Colors.green,
),
);
}
}
@override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context);
final profileData = auth.profileData;
return Scaffold(
appBar: AppBar(
title: const Text("User Profile"),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _refreshProfile,
),
],
),
body: Column(
children: [
Expanded(
child: _isLoading && profileData == null
? const Center(child: CircularProgressIndicator())
: _errorMessage.isNotEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, color: Theme.of(context).colorScheme.error, size: 50),
const SizedBox(height: 16),
Text(
_errorMessage,
textAlign: TextAlign.center,
style: TextStyle(color: Theme.of(context).colorScheme.error, fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _refreshProfile,
child: const Text('Retry'),
),
],
),
),
)
: profileData == null
? const Center(child: Text('No profile data available.'))
: RefreshIndicator(
onRefresh: _refreshProfile,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: GestureDetector(
onTap: _isLoading ? null : _showImageSourceSelection,
child: Stack(
alignment: Alignment.bottomRight,
children: [
CircleAvatar(
radius: 60,
backgroundColor: Theme.of(context).colorScheme.secondary,
backgroundImage: _profileImageFile != null ? FileImage(_profileImageFile!) : null,
child: _profileImageFile == null ? Icon(Icons.person, size: 80, color: Theme.of(context).colorScheme.onSecondary) : null,
),
if (!_isLoading)
Positioned(
right: 0,
bottom: 0,
child: CircleAvatar(
radius: 20,
backgroundColor: Theme.of(context).primaryColor,
child: Icon(Icons.edit, color: Theme.of(context).colorScheme.onPrimary, size: 20),
),
),
],
),
),
),
const SizedBox(height: 32),
_buildProfileSection(context, "Personal Information", [
_buildProfileDetail(context, "Username:", profileData['username']),
_buildProfileDetail(context, "Email:", profileData['email']),
_buildProfileDetail(context, "First Name:", profileData['first_name']),
_buildProfileDetail(context, "Last Name:", profileData['last_name']),
_buildProfileDetail(context, "Phone Number:", profileData['phone_number']),
]),
const SizedBox(height: 24),
_buildProfileSection(context, "Organizational Details", [
_buildProfileDetail(context, "Role:", profileData['role_name']),
_buildProfileDetail(context, "Department:", profileData['department_name']),
_buildProfileDetail(context, "Company:", profileData['company_name']),
_buildProfileDetail(context, "Position:", profileData['position_name']),
]),
const SizedBox(height: 24),
_buildProfileSection(context, "Account Status", [
_buildProfileDetail(context, "Account Status:", profileData['account_status']),
_buildProfileDetail(context, "Registered On:", profileData['date_registered']),
]),
],
),
),
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Center(
child: ElevatedButton.icon(
icon: const Icon(Icons.logout),
label: const Text("Logout"),
onPressed: () {
showDialog(
context: context,
barrierDismissible: false,
builder: (dialogContext) => AlertDialog(
title: const Text("Confirm Logout"),
content: const Text("Are you sure you want to log out?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text("Cancel"),
),
ElevatedButton(
onPressed: () {
Navigator.pop(dialogContext);
auth.logout();
Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[700],
foregroundColor: Colors.white,
),
child: const Text("Logout"),
),
],
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[700],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
),
),
),
),
],
),
);
}
Widget _buildProfileSection(BuildContext context, String title, List<Widget> details) {
return Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor),
),
const Divider(height: 20, thickness: 1.5),
...details,
],
),
),
);
}
Widget _buildProfileDetail(BuildContext context, String label, dynamic value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 2,
child: Text(
label,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
const SizedBox(width: 16),
Expanded(
flex: 3,
child: Text(
value?.toString() ?? 'N/A',
style: Theme.of(context).textTheme.bodyLarge,
),
),
],
),
);
}
}