269 lines
8.6 KiB
Dart
269 lines
8.6 KiB
Dart
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../../../core/utils/media_url.dart';
|
|
import '../../../../core/utils/validators.dart';
|
|
import '../../../../shared/models/user_management_models.dart';
|
|
import '../../../../shared/providers/auth_provider.dart';
|
|
import '../../../../shared/widgets/app_button.dart';
|
|
import '../../../../shared/widgets/app_text_field.dart';
|
|
import '../../../../shared/widgets/app_toast.dart';
|
|
import '../../../../shared/widgets/user_avatar.dart';
|
|
import '../../../auth/data/repositories/auth_repository_impl.dart';
|
|
|
|
class UserProfileScreen extends ConsumerStatefulWidget {
|
|
const UserProfileScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<UserProfileScreen> createState() => _UserProfileScreenState();
|
|
}
|
|
|
|
class _UserProfileScreenState extends ConsumerState<UserProfileScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _fullNameController = TextEditingController();
|
|
final _nameController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _mobileController = TextEditingController();
|
|
bool _isUpdatingProfile = false;
|
|
bool _isUploadingAvatar = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUser();
|
|
}
|
|
|
|
void _loadUser() {
|
|
final user = ref.read(authStateProvider).user;
|
|
if (user == null) return;
|
|
_fullNameController.text = user.name;
|
|
_nameController.text = user.name;
|
|
_emailController.text = user.email;
|
|
_mobileController.text = user.mobile;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fullNameController.dispose();
|
|
_nameController.dispose();
|
|
_emailController.dispose();
|
|
_mobileController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _pickAndUploadAvatar() async {
|
|
final result = await FilePicker.pickFiles(
|
|
type: FileType.custom,
|
|
allowedExtensions: const ['jpg', 'jpeg', 'png', 'webp'],
|
|
withData: true,
|
|
);
|
|
if (result == null || result.files.isEmpty) return;
|
|
|
|
final file = result.files.first;
|
|
final bytes = file.bytes;
|
|
if (bytes == null || bytes.isEmpty) {
|
|
if (!mounted) return;
|
|
showAppToastFromSnackBar(
|
|
context,
|
|
const SnackBar(content: Text('Could not read the selected image.')),
|
|
);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isUploadingAvatar = true);
|
|
final repository = ref.read(authRepositoryProvider);
|
|
final uploadResult = await repository.uploadProfileAvatar(
|
|
bytes: bytes,
|
|
filename: file.name.isNotEmpty ? file.name : 'avatar.png',
|
|
);
|
|
|
|
if (!mounted) return;
|
|
|
|
if (uploadResult.failure != null) {
|
|
setState(() => _isUploadingAvatar = false);
|
|
showAppToastFromSnackBar(
|
|
context,
|
|
SnackBar(
|
|
content: Text(
|
|
uploadResult.failure?.message ?? 'Unable to upload profile picture.',
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final current = ref.read(authStateProvider).user;
|
|
if (current != null) {
|
|
ref.read(authStateProvider.notifier).setUser(
|
|
current.copyWith(avatarUrl: uploadResult.data),
|
|
);
|
|
}
|
|
// Sync role/department/avatar from /auth/me when available.
|
|
await ref.read(authStateProvider.notifier).refreshCurrentUser();
|
|
|
|
if (!mounted) return;
|
|
setState(() => _isUploadingAvatar = false);
|
|
showAppToastFromSnackBar(
|
|
context,
|
|
const SnackBar(content: Text('Profile picture updated')),
|
|
);
|
|
}
|
|
|
|
Future<void> _updateProfile() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
setState(() => _isUpdatingProfile = true);
|
|
|
|
final repository = ref.read(authRepositoryProvider);
|
|
final result = await repository.updateProfile(
|
|
UpdateProfileRequest(
|
|
fullName: _fullNameController.text.trim(),
|
|
name: _nameController.text.trim(),
|
|
email: _emailController.text.trim(),
|
|
mobile: _mobileController.text.trim(),
|
|
),
|
|
);
|
|
|
|
if (!mounted) return;
|
|
setState(() => _isUpdatingProfile = false);
|
|
|
|
if (result.failure != null) {
|
|
showAppToastFromSnackBar(
|
|
context,
|
|
SnackBar(
|
|
content: Text(
|
|
result.failure?.message ?? 'Unable to update profile.',
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (result.data != null) {
|
|
ref.read(authStateProvider.notifier).setUser(result.data!);
|
|
_fullNameController.text = result.data!.name;
|
|
_nameController.text = result.data!.name;
|
|
_emailController.text = result.data!.email;
|
|
_mobileController.text = result.data!.mobile;
|
|
}
|
|
|
|
showAppToastFromSnackBar(
|
|
context,
|
|
const SnackBar(content: Text('Profile updated')),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final user = ref.watch(authStateProvider).user;
|
|
final theme = Theme.of(context);
|
|
|
|
if (user == null) {
|
|
return const Center(child: Text('Not signed in'));
|
|
}
|
|
|
|
final avatarUrl = resolveMediaUrl(user.avatarUrl);
|
|
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 640),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
Text(
|
|
'User Profile',
|
|
style: theme.textTheme.headlineSmall,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Center(
|
|
child: Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
UserAvatar(
|
|
name: user.name,
|
|
avatarUrl: avatarUrl,
|
|
radius: 48,
|
|
),
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: IconButton.filled(
|
|
onPressed:
|
|
_isUploadingAvatar ? null : _pickAndUploadAvatar,
|
|
icon: _isUploadingAvatar
|
|
? const SizedBox(
|
|
width: 18,
|
|
height: 18,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: const Icon(Icons.camera_alt, size: 18),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
children: [
|
|
AppTextField(
|
|
controller: _fullNameController,
|
|
label: 'Full Name',
|
|
validator: (v) =>
|
|
Validators.required(v, fieldName: 'Full Name'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
AppTextField(
|
|
controller: _nameController,
|
|
label: 'Name',
|
|
validator: (v) =>
|
|
Validators.required(v, fieldName: 'Name'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
AppTextField(
|
|
controller: _emailController,
|
|
label: 'Email',
|
|
keyboardType: TextInputType.emailAddress,
|
|
validator: Validators.email,
|
|
),
|
|
const SizedBox(height: 16),
|
|
AppTextField(
|
|
controller: _mobileController,
|
|
label: 'Mobile',
|
|
keyboardType: TextInputType.phone,
|
|
validator: Validators.mobile,
|
|
inputFormatters: Validators.mobileInput,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text('Department: ${user.department ?? '—'}'),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text('Role: ${user.role}'),
|
|
),
|
|
const SizedBox(height: 24),
|
|
AppButton(
|
|
label: 'Update Profile',
|
|
isLoading: _isUpdatingProfile,
|
|
onPressed: _updateProfile,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|