environment_monitoring_app/lib/screens/profile.dart

577 lines
21 KiB
Dart

import 'dart:io';
import 'dart:typed_data'; // <-- ADDED IMPORT
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:signature/signature.dart'; // <-- ADDED IMPORT
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> {
// FIX: Removed direct instantiation of ApiService
bool _isLoading = false;
String _errorMessage = '';
File? _profileImageFile;
File? _signatureImageFile; // <-- ADDED STATE VARIABLE
// FIX: Use late initialization to retrieve the service instance.
late ApiService _apiService;
// --- ADDED SIGNATURE CONTROLLER ---
final SignatureController _signatureController = SignatureController(
penStrokeWidth: 3,
penColor: Colors.black,
exportBackgroundColor: Colors.transparent, // For transparent PNG
);
// --- END OF CONTROLLER ---
@override
void initState() {
super.initState();
// FIX: Retrieve the ApiService instance after the context is fully available.
WidgetsBinding.instance.addPostFrameCallback((_) {
_apiService = Provider.of<ApiService>(context, listen: false);
_loadLocalProfileImage().then((_) {
// --- MODIFIED: Chain signature loading ---
_loadLocalSignatureImage().then((_) {
// If no profile data is available at all, trigger a refresh
if (Provider.of<AuthProvider>(context, listen: false).profileData == null) {
_refreshProfile();
}
});
// --- END MODIFIED ---
});
});
}
/// 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();
await _loadLocalSignatureImage(); // <-- ADDED THIS LINE
} 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;
// FIX: Use the injected _apiService instance
final downloadedFile = await _apiService.downloadProfilePicture(fullImageUrl, localFilePath);
if (downloadedFile != null && mounted) {
setState(() => _profileImageFile = downloadedFile);
}
}
} else {
if (mounted) setState(() => _profileImageFile = null);
}
}
// --- ADDED NEW FUNCTION ---
/// Loads the signature image from the local cache or downloads it if not present.
Future<void> _loadLocalSignatureImage() async {
final auth = Provider.of<AuthProvider>(context, listen: false);
// Assumes the field name in your DB/profile JSON is 'signature_path'
final String? serverSignaturePath = auth.profileData?['signature_path'];
if (serverSignaturePath != null && serverSignaturePath.isNotEmpty) {
final String localFileName = p.basename(serverSignaturePath);
final Directory appDocDir = await getApplicationDocumentsDirectory();
// Store signatures in their own sub-directory
final String localFilePath = p.join(appDocDir.path, 'signatures', localFileName);
final File localFile = File(localFilePath);
if (await localFile.exists()) {
if (mounted) setState(() => _signatureImageFile = localFile);
} else {
final String fullImageUrl = ApiService.imageBaseUrl + serverSignaturePath;
// We can re-use the downloadProfilePicture method, just provide a different local path
final downloadedFile = await _apiService.downloadProfilePicture(fullImageUrl, localFilePath);
if (downloadedFile != null && mounted) {
setState(() => _signatureImageFile = downloadedFile);
}
}
} else {
if (mounted) setState(() => _signatureImageFile = null);
}
}
// --- END OF NEW FUNCTION ---
/// 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);
// FIX: Use the injected _apiService instance
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);
}
}
}
// --- ADDED THESE 2 NEW FUNCTIONS ---
/// Shows the signature pad dialog.
Future<void> _showSignaturePad() async {
_signatureController.clear(); // Clear any previous drawings
final File? signatureFile = await showDialog<File?>(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: const Text("Provide Your Signature"),
content: Container(
width: double.maxFinite,
height: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
color: Colors.grey.shade200, // Background for the pad
),
child: Signature(
controller: _signatureController,
backgroundColor: Colors.white,
),
),
actions: <Widget>[
TextButton(
child: const Text("Clear"),
onPressed: () {
_signatureController.clear();
},
),
TextButton(
child: const Text("Cancel"),
onPressed: () {
Navigator.pop(dialogContext, null);
},
),
ElevatedButton(
child: const Text("Save"),
onPressed: () async {
if (_signatureController.isNotEmpty) {
final Uint8List? data = await _signatureController.toPngBytes();
if (data != null) {
final tempDir = await getTemporaryDirectory();
final file = File(p.join(tempDir.path, "signature_${DateTime.now().millisecondsSinceEpoch}.png"));
await file.writeAsBytes(data);
Navigator.pop(dialogContext, file);
}
} else {
// Show a snackbar if the signature is empty
ScaffoldMessenger.of(dialogContext).showSnackBar(
const SnackBar(
content: Text("Please provide a signature first."),
duration: Duration(seconds: 2),
),
);
}
},
),
],
);
},
);
// After dialog is closed, check if we got a file
if (signatureFile != null) {
await _uploadSignature(signatureFile);
}
}
/// Uploads the signature file and refreshes the profile.
Future<void> _uploadSignature(File signatureFile) async {
setState(() => _isLoading = true);
final uploadResult = await _apiService.uploadSignature(signatureFile);
if (mounted) {
if (uploadResult['success']) {
// Refresh profile to get new signature_path and display it
await _refreshProfile();
_showSnackBar("Signature updated successfully.", isError: false);
} else {
setState(() {
_errorMessage = uploadResult['message'] ?? 'Failed to upload signature.';
});
_showSnackBar(_errorMessage, isError: true);
}
setState(() => _isLoading = false);
}
// Clean up temp file
try {
if (await signatureFile.exists()) {
await signatureFile.delete();
}
} catch (e) {
debugPrint("Error deleting temp signature file: $e");
}
}
// --- END OF NEW FUNCTIONS ---
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),
// --- ADDED THIS SECTION ---
_buildSignatureSection(context),
const SizedBox(height: 24),
// --- END OF ADDED SECTION ---
_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,
],
),
),
);
}
// --- ADDED NEW WIDGET METHOD ---
Widget _buildSignatureSection(BuildContext context) {
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(
"Signature",
style: Theme.of(context).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor),
),
const Divider(height: 20, thickness: 1.5),
Center(
child: Container(
width: double.infinity,
height: 150,
decoration: BoxDecoration(
color: Colors.grey.shade200,
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(4.0),
child: _signatureImageFile != null
? Image.file(
_signatureImageFile!,
fit: BoxFit.contain,
)
: Center(
child: Text(
"No signature set.",
style: TextStyle(color: Colors.grey.shade700),
),
),
),
),
),
const SizedBox(height: 16),
Center(
child: ElevatedButton.icon(
onPressed: _isLoading ? null : _showSignaturePad,
icon: const Icon(Icons.edit_note),
label: const Text("Update Signature"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
)
],
),
),
);
}
// --- END OF NEW WIDGET METHOD ---
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,
),
),
],
),
);
}
}