563 lines
22 KiB
Dart
563 lines
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter/services.dart';
|
|
import '../../core/services/api_service.dart';
|
|
import '../../core/models/group_member.dart';
|
|
import '../../core/utils/snackbar_util.dart';
|
|
|
|
class EditMemberDialog extends StatefulWidget {
|
|
final GroupMember member;
|
|
|
|
const EditMemberDialog({
|
|
super.key,
|
|
required this.member,
|
|
});
|
|
|
|
@override
|
|
State<EditMemberDialog> createState() => _EditMemberDialogState();
|
|
}
|
|
|
|
class _EditMemberDialogState extends State<EditMemberDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _nameController = TextEditingController();
|
|
final _phoneController = TextEditingController();
|
|
final _emailController = TextEditingController();
|
|
final _addressController = TextEditingController();
|
|
final _emergencyContactController = TextEditingController();
|
|
final _apiService = ApiService();
|
|
|
|
bool _isLoading = false;
|
|
bool _showMemberId = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_nameController.text = widget.member.user?.fullName ?? '';
|
|
_phoneController.text = widget.member.user?.mobileNumber ?? '';
|
|
_emailController.text = widget.member.user?.email ?? '';
|
|
_addressController.text = widget.member.user?.address ?? '';
|
|
_emergencyContactController.text = widget.member.user?.emergencyContact ?? '';
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nameController.dispose();
|
|
_phoneController.dispose();
|
|
_emailController.dispose();
|
|
_addressController.dispose();
|
|
_emergencyContactController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _handleSubmit() async {
|
|
if (_formKey.currentState!.validate()) {
|
|
setState(() => _isLoading = true);
|
|
|
|
try {
|
|
final updates = <String, dynamic>{};
|
|
|
|
// Only include changed fields
|
|
if (_nameController.text != (widget.member.user?.fullName ?? '')) {
|
|
updates['full_name'] = _nameController.text;
|
|
}
|
|
|
|
if (_phoneController.text != (widget.member.user?.mobileNumber ?? '')) {
|
|
updates['mobile_number'] = _phoneController.text;
|
|
}
|
|
|
|
if (_emailController.text != (widget.member.user?.email ?? '')) {
|
|
updates['email'] = _emailController.text.isEmpty ? null : _emailController.text;
|
|
}
|
|
|
|
if (_addressController.text != (widget.member.user?.address ?? '')) {
|
|
updates['address'] = _addressController.text.isEmpty ? null : _addressController.text;
|
|
}
|
|
|
|
if (_emergencyContactController.text != (widget.member.user?.emergencyContact ?? '')) {
|
|
updates['emergency_contact'] = _emergencyContactController.text.isEmpty
|
|
? null
|
|
: _emergencyContactController.text;
|
|
}
|
|
|
|
if (updates.isEmpty) {
|
|
SnackbarUtil.showWarning('No changes made');
|
|
Get.back();
|
|
return;
|
|
}
|
|
|
|
final response = await _apiService.updateMemberDetails(
|
|
widget.member.userId,
|
|
updates,
|
|
);
|
|
|
|
if (response['success']) {
|
|
SnackbarUtil.showSuccess(
|
|
'Member details updated successfully',
|
|
title: 'Success',
|
|
);
|
|
Get.back(result: true);
|
|
} else {
|
|
SnackbarUtil.showError(
|
|
response['message'] ?? 'Failed to update member',
|
|
title: 'Error',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
SnackbarUtil.showError('Error: ${e.toString()}');
|
|
} finally {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _copyMemberId() {
|
|
Clipboard.setData(ClipboardData(text: widget.member.userId));
|
|
SnackbarUtil.showSuccess(
|
|
'Member ID copied to clipboard',
|
|
duration: const Duration(seconds: 2),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Dialog(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.r)),
|
|
child: Container(
|
|
width: 500.w,
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(context).size.height * 0.85,
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Fixed Header
|
|
Container(
|
|
padding: EdgeInsets.all(24.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(20.r),
|
|
topRight: Radius.circular(20.r),
|
|
),
|
|
border: Border(
|
|
bottom: BorderSide(color: Colors.grey.shade200),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Member Number Badge
|
|
Container(
|
|
width: 40.w,
|
|
height: 40.w,
|
|
decoration: BoxDecoration(
|
|
color: Colors.purple.shade600,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'#${widget.member.memberNumber}',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16.sp,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 12.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Edit Member #${widget.member.memberNumber}',
|
|
style: TextStyle(
|
|
fontSize: 20.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
Text(
|
|
widget.member.user?.fullName ?? 'Unknown',
|
|
style: TextStyle(
|
|
fontSize: 13.sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Get.back(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Scrollable Content
|
|
Flexible(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.all(24.w),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Member Number & ID Section
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.purple.shade50,
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
border: Border.all(color: Colors.purple.shade200),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.badge,
|
|
color: Colors.purple.shade700, size: 22.w),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'Member Number (Readable)',
|
|
style: TextStyle(
|
|
fontSize: 15.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.purple.shade900,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 12.h),
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(8.r),
|
|
border: Border.all(color: Colors.purple.shade300, width: 2),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'Member #${widget.member.memberNumber}',
|
|
style: TextStyle(
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.purple.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
'Use this number to easily reference this member',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.purple.shade700,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
|
|
// Unique ID Section (Technical)
|
|
Container(
|
|
padding: EdgeInsets.all(16.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade50,
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.fingerprint,
|
|
color: Colors.grey.shade700, size: 20.w),
|
|
SizedBox(width: 8.w),
|
|
Text(
|
|
'Unique ID (Technical)',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey.shade700,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: Icon(_showMemberId ? Icons.visibility_off : Icons.visibility,
|
|
size: 18.w),
|
|
onPressed: () {
|
|
setState(() => _showMemberId = !_showMemberId);
|
|
},
|
|
tooltip: _showMemberId ? 'Hide ID' : 'Show ID',
|
|
),
|
|
],
|
|
),
|
|
if (_showMemberId) ...[
|
|
SizedBox(height: 8.h),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
widget.member.userId,
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
fontFamily: 'monospace',
|
|
color: Colors.grey.shade800,
|
|
),
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.copy, size: 18),
|
|
onPressed: _copyMemberId,
|
|
tooltip: 'Copy UUID',
|
|
),
|
|
],
|
|
),
|
|
],
|
|
SizedBox(height: 4.h),
|
|
Text(
|
|
'UUID for database operations',
|
|
style: TextStyle(
|
|
fontSize: 11.sp,
|
|
color: Colors.grey.shade600,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 24.h),
|
|
|
|
// Full Name
|
|
Text(
|
|
'Full Name *',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
TextFormField(
|
|
controller: _nameController,
|
|
decoration: InputDecoration(
|
|
hintText: 'Full name',
|
|
prefixIcon: const Icon(Icons.person),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
validator: (value) {
|
|
if (value?.isEmpty ?? true) return 'Name is required';
|
|
if (value!.length < 2) return 'Name too short';
|
|
return null;
|
|
},
|
|
),
|
|
SizedBox(height: 20.h),
|
|
|
|
// Mobile Number
|
|
Text(
|
|
'Mobile Number *',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
TextFormField(
|
|
controller: _phoneController,
|
|
keyboardType: TextInputType.phone,
|
|
maxLength: 10,
|
|
decoration: InputDecoration(
|
|
hintText: '10-digit mobile number',
|
|
prefixIcon: const Icon(Icons.phone),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
counterText: '',
|
|
),
|
|
validator: (value) {
|
|
if (value?.isEmpty ?? true) return 'Mobile number is required';
|
|
if (!RegExp(r'^[0-9]{10}$').hasMatch(value!)) {
|
|
return 'Must be exactly 10 digits';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
SizedBox(height: 20.h),
|
|
|
|
// Email (Optional)
|
|
Text(
|
|
'Email (Optional)',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
TextFormField(
|
|
controller: _emailController,
|
|
keyboardType: TextInputType.emailAddress,
|
|
decoration: InputDecoration(
|
|
hintText: 'email@example.com',
|
|
prefixIcon: const Icon(Icons.email),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
|
return 'Invalid email format';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
SizedBox(height: 20.h),
|
|
|
|
// Address (Optional)
|
|
Text(
|
|
'Address (Optional)',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
TextFormField(
|
|
controller: _addressController,
|
|
maxLines: 3,
|
|
decoration: InputDecoration(
|
|
hintText: 'Full address',
|
|
prefixIcon: const Icon(Icons.location_on),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
|
|
// Emergency Contact (Optional)
|
|
Text(
|
|
'Emergency Contact (Optional)',
|
|
style: TextStyle(
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
TextFormField(
|
|
controller: _emergencyContactController,
|
|
keyboardType: TextInputType.phone,
|
|
maxLength: 10,
|
|
decoration: InputDecoration(
|
|
hintText: '10-digit emergency contact',
|
|
prefixIcon: const Icon(Icons.contacts),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
counterText: '',
|
|
),
|
|
validator: (value) {
|
|
if (value != null && value.isNotEmpty) {
|
|
if (!RegExp(r'^[0-9]{10}$').hasMatch(value)) {
|
|
return 'Must be exactly 10 digits';
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Fixed Footer with Actions
|
|
Container(
|
|
padding: EdgeInsets.all(24.w),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.only(
|
|
bottomLeft: Radius.circular(20.r),
|
|
bottomRight: Radius.circular(20.r),
|
|
),
|
|
border: Border(
|
|
top: BorderSide(color: Colors.grey.shade200),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: OutlinedButton(
|
|
onPressed: _isLoading ? null : () => Get.back(),
|
|
style: OutlinedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(vertical: 14.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
),
|
|
child: Text(
|
|
'Cancel',
|
|
style: TextStyle(fontSize: 16.sp),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 16.w),
|
|
Expanded(
|
|
child: ElevatedButton(
|
|
onPressed: _isLoading ? null : _handleSubmit,
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(vertical: 14.h),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12.r),
|
|
),
|
|
elevation: 2,
|
|
),
|
|
child: _isLoading
|
|
? SizedBox(
|
|
height: 20.h,
|
|
width: 20.w,
|
|
child: const CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
)
|
|
: Text(
|
|
'Update Member',
|
|
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w600),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|