696 lines
23 KiB
Dart
696 lines
23 KiB
Dart
/// Profile Edit Page
|
|
///
|
|
/// Allows users to edit their profile information.
|
|
/// Features:
|
|
/// - Avatar upload with image picker
|
|
/// - Form fields for personal information
|
|
/// - Form validation
|
|
/// - Save/cancel actions
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:worker/core/constants/ui_constants.dart';
|
|
import 'package:worker/core/theme/colors.dart';
|
|
|
|
/// Profile Edit Page
|
|
///
|
|
/// Page for editing user profile information with avatar upload.
|
|
class ProfileEditPage extends HookConsumerWidget {
|
|
const ProfileEditPage({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// Form key for validation
|
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
|
|
|
// Image picker
|
|
final selectedImage = useState<File?>(null);
|
|
|
|
// Form controllers
|
|
final nameController = useTextEditingController(text: 'Hoàng Minh Hiệp');
|
|
final phoneController = useTextEditingController(text: '0347302911');
|
|
final emailController = useTextEditingController(
|
|
text: 'hoanghiep@example.com',
|
|
);
|
|
final birthDateController = useTextEditingController(text: '15/03/1985');
|
|
final idNumberController = useTextEditingController(text: '123456789012');
|
|
final taxIdController = useTextEditingController(text: '0359837618');
|
|
final companyController = useTextEditingController(
|
|
text: 'Công ty TNHH Xây dựng ABC',
|
|
);
|
|
final addressController = useTextEditingController(
|
|
text: '123 Man Thiện, Thủ Đức, Hồ Chí Minh',
|
|
);
|
|
final experienceController = useTextEditingController(text: '10');
|
|
|
|
// Dropdown values
|
|
final selectedGender = useState<String>('male');
|
|
final selectedPosition = useState<String>('contractor');
|
|
|
|
// Has unsaved changes
|
|
final hasChanges = useState<bool>(false);
|
|
|
|
return PopScope(
|
|
canPop: !hasChanges.value,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
|
|
final shouldPop = await _showUnsavedChangesDialog(context);
|
|
if (shouldPop == true && context.mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xFFF4F6F8),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
|
onPressed: () async {
|
|
if (hasChanges.value) {
|
|
final shouldPop = await _showUnsavedChangesDialog(context);
|
|
if (shouldPop == true && context.mounted) {
|
|
context.pop();
|
|
}
|
|
} else {
|
|
context.pop();
|
|
}
|
|
},
|
|
),
|
|
title: const Text(
|
|
'Thông tin cá nhân',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
centerTitle: false,
|
|
actions: const [SizedBox(width: AppSpacing.sm)],
|
|
),
|
|
body: Form(
|
|
key: formKey,
|
|
onChanged: () {
|
|
hasChanges.value = true;
|
|
},
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Profile Avatar Section
|
|
_buildAvatarSection(context, selectedImage),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Form Card
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Full Name
|
|
_buildTextField(
|
|
label: 'Họ và tên',
|
|
controller: nameController,
|
|
required: true,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Vui lòng nhập họ và tên';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Phone
|
|
_buildTextField(
|
|
label: 'Số điện thoại',
|
|
controller: phoneController,
|
|
required: true,
|
|
keyboardType: TextInputType.phone,
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Vui lòng nhập số điện thoại';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Email
|
|
_buildTextField(
|
|
label: 'Email',
|
|
controller: emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Birth Date
|
|
_buildDateField(
|
|
context: context,
|
|
label: 'Ngày sinh',
|
|
controller: birthDateController,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Gender
|
|
_buildDropdownField(
|
|
label: 'Giới tính',
|
|
value: selectedGender.value,
|
|
items: const [
|
|
{'value': 'male', 'label': 'Nam'},
|
|
{'value': 'female', 'label': 'Nữ'},
|
|
{'value': 'other', 'label': 'Khác'},
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedGender.value = value;
|
|
hasChanges.value = true;
|
|
}
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// ID Number
|
|
_buildTextField(
|
|
label: 'Số CMND/CCCD',
|
|
controller: idNumberController,
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Tax ID
|
|
_buildTextField(
|
|
label: 'Mã số thuế',
|
|
controller: taxIdController,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Company
|
|
_buildTextField(
|
|
label: 'Công ty',
|
|
controller: companyController,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Address
|
|
_buildTextField(
|
|
label: 'Địa chỉ',
|
|
controller: addressController,
|
|
maxLines: 2,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Position
|
|
_buildDropdownField(
|
|
label: 'Chức vụ',
|
|
value: selectedPosition.value,
|
|
items: const [
|
|
{'value': 'contractor', 'label': 'Thầu thợ'},
|
|
{'value': 'architect', 'label': 'Kiến trúc sư'},
|
|
{'value': 'dealer', 'label': 'Đại lý phân phối'},
|
|
{'value': 'broker', 'label': 'Môi giới'},
|
|
{'value': 'other', 'label': 'Khác'},
|
|
],
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
selectedPosition.value = value;
|
|
hasChanges.value = true;
|
|
}
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Experience
|
|
_buildTextField(
|
|
label: 'Kinh nghiệm (năm)',
|
|
controller: experienceController,
|
|
keyboardType: TextInputType.number,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.lg),
|
|
|
|
// Action Buttons
|
|
_buildActionButtons(
|
|
context: context,
|
|
formKey: formKey,
|
|
hasChanges: hasChanges,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.lg),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Build avatar section with edit button
|
|
Widget _buildAvatarSection(
|
|
BuildContext context,
|
|
ValueNotifier<File?> selectedImage,
|
|
) {
|
|
return Center(
|
|
child: Stack(
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 100,
|
|
height: 100,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: AppColors.primaryBlue,
|
|
image: selectedImage.value != null
|
|
? DecorationImage(
|
|
image: FileImage(selectedImage.value!),
|
|
fit: BoxFit.cover,
|
|
)
|
|
: null,
|
|
),
|
|
child: selectedImage.value == null
|
|
? const Center(
|
|
child: Text(
|
|
'HMH',
|
|
style: TextStyle(
|
|
fontSize: 32,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
)
|
|
: null,
|
|
),
|
|
|
|
// Edit Button
|
|
Positioned(
|
|
bottom: 0,
|
|
right: 0,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
await _pickImage(context, selectedImage);
|
|
},
|
|
child: Container(
|
|
width: 32,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: AppColors.primaryBlue,
|
|
shape: BoxShape.circle,
|
|
border: Border.all(color: Colors.white, width: 2),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.1),
|
|
blurRadius: 4,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Icon(
|
|
Icons.camera_alt,
|
|
size: 16,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Build text field
|
|
Widget _buildTextField({
|
|
required String label,
|
|
required TextEditingController controller,
|
|
bool required = false,
|
|
TextInputType? keyboardType,
|
|
int maxLines = 1,
|
|
String? Function(String?)? validator,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
RichText(
|
|
text: TextSpan(
|
|
text: label,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
children: [
|
|
if (required)
|
|
const TextSpan(
|
|
text: ' *',
|
|
style: TextStyle(color: AppColors.danger),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextFormField(
|
|
controller: controller,
|
|
keyboardType: keyboardType,
|
|
maxLines: maxLines,
|
|
validator: validator,
|
|
decoration: InputDecoration(
|
|
hintText: 'Nhập $label',
|
|
hintStyle: TextStyle(
|
|
color: AppColors.grey500.withValues(alpha: 0.6),
|
|
fontSize: 14,
|
|
),
|
|
filled: true,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primaryBlue,
|
|
width: 2,
|
|
),
|
|
),
|
|
errorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: AppColors.danger),
|
|
),
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Build date field
|
|
Widget _buildDateField({
|
|
required BuildContext context,
|
|
required String label,
|
|
required TextEditingController controller,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextFormField(
|
|
controller: controller,
|
|
readOnly: true,
|
|
onTap: () async {
|
|
final date = await showDatePicker(
|
|
context: context,
|
|
initialDate: DateTime(1985, 3, 15),
|
|
firstDate: DateTime(1940),
|
|
lastDate: DateTime.now(),
|
|
);
|
|
if (date != null) {
|
|
controller.text =
|
|
'${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
|
|
}
|
|
},
|
|
decoration: InputDecoration(
|
|
hintText: 'Chọn ngày sinh',
|
|
hintStyle: TextStyle(
|
|
color: AppColors.grey500.withValues(alpha: 0.6),
|
|
fontSize: 14,
|
|
),
|
|
filled: true,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
suffixIcon: const Icon(Icons.calendar_today, size: 20),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primaryBlue,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Build dropdown field
|
|
Widget _buildDropdownField({
|
|
required String label,
|
|
required String value,
|
|
required List<Map<String, String>> items,
|
|
required ValueChanged<String?> onChanged,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1E293B),
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
DropdownButtonFormField<String>(
|
|
initialValue: value,
|
|
onChanged: onChanged,
|
|
decoration: InputDecoration(
|
|
filled: true,
|
|
fillColor: const Color(0xFFF8FAFC),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primaryBlue,
|
|
width: 2,
|
|
),
|
|
),
|
|
),
|
|
items: items.map((item) {
|
|
return DropdownMenuItem<String>(
|
|
value: item['value'],
|
|
child: Text(item['label']!, style: const TextStyle(fontSize: 14)),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
/// Build action buttons
|
|
Widget _buildActionButtons({
|
|
required BuildContext context,
|
|
required GlobalKey<FormState> formKey,
|
|
required ValueNotifier<bool> hasChanges,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
child: Row(
|
|
children: [
|
|
// Cancel Button
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: () {
|
|
context.pop();
|
|
},
|
|
style: OutlinedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
side: const BorderSide(color: AppColors.grey100),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'Hủy bỏ',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppColors.grey900,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: AppSpacing.sm),
|
|
|
|
// Save Button
|
|
Expanded(
|
|
flex: 2,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () {
|
|
if (formKey.currentState?.validate() ?? false) {
|
|
// TODO: Save profile data
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Thông tin đã được cập nhật thành công!'),
|
|
backgroundColor: AppColors.success,
|
|
duration: Duration(seconds: 2),
|
|
),
|
|
);
|
|
hasChanges.value = false;
|
|
context.pop();
|
|
}
|
|
},
|
|
icon: const Icon(Icons.save, size: 20),
|
|
label: const Text(
|
|
'Lưu thay đổi',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
elevation: 0,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Pick image from gallery or camera
|
|
Future<void> _pickImage(
|
|
BuildContext context,
|
|
ValueNotifier<File?> selectedImage,
|
|
) async {
|
|
final ImagePicker picker = ImagePicker();
|
|
|
|
// Show dialog to choose source
|
|
final source = await showDialog<ImageSource>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Chọn ảnh từ'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListTile(
|
|
leading: const Icon(Icons.camera_alt),
|
|
title: const Text('Máy ảnh'),
|
|
onTap: () => Navigator.pop(context, ImageSource.camera),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.photo_library),
|
|
title: const Text('Thư viện ảnh'),
|
|
onTap: () => Navigator.pop(context, ImageSource.gallery),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
if (source != null) {
|
|
final XFile? image = await picker.pickImage(
|
|
source: source,
|
|
maxWidth: 512,
|
|
maxHeight: 512,
|
|
imageQuality: 85,
|
|
);
|
|
|
|
if (image != null) {
|
|
selectedImage.value = File(image.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Show unsaved changes dialog
|
|
Future<bool?> _showUnsavedChangesDialog(BuildContext context) {
|
|
return showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Thay đổi chưa được lưu'),
|
|
content: const Text(
|
|
'Bạn có thay đổi chưa được lưu. Bạn có muốn thoát không?',
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, false),
|
|
child: const Text('Ở lại'),
|
|
),
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context, true),
|
|
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
|
|
child: const Text('Thoát'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|