bharat_erp/lib/modules/users/presentation/screens/user_profile_screen.dart
2026-07-24 14:01:50 +05:30

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,
),
],
),
),
],
),
),
),
);
}
}