Files
mobile-app/lib/features/profile/presentation/screens/profile_settings_screen.dart

972 lines
30 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:real_estate_mobile/core/constants/app_colors.dart';
import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart';
class ProfileSettingsScreen extends ConsumerStatefulWidget {
const ProfileSettingsScreen({super.key});
@override
ConsumerState<ProfileSettingsScreen> createState() =>
_ProfileSettingsScreenState();
}
class _ProfileSettingsScreenState extends ConsumerState<ProfileSettingsScreen> {
int _selectedTab = 0;
// Profile form controllers
late TextEditingController _fullNameController;
late TextEditingController _titleController;
late TextEditingController _emailController;
late TextEditingController _phoneController;
late TextEditingController _locationController;
// Notification preferences
final Map<String, Map<String, bool>> _notificationPrefs = {
'newLeadAlerts': {'email': true, 'inApp': true},
'messageUpdates': {'email': true, 'inApp': true},
'urgentRequests': {'email': true, 'inApp': true},
};
String _digestFrequency = 'instant';
bool _controllersInitialized = false;
@override
void initState() {
super.initState();
_fullNameController = TextEditingController();
_titleController = TextEditingController();
_emailController = TextEditingController();
_phoneController = TextEditingController();
_locationController = TextEditingController();
}
@override
void dispose() {
_fullNameController.dispose();
_titleController.dispose();
_emailController.dispose();
_phoneController.dispose();
_locationController.dispose();
super.dispose();
}
void _initControllersFromProfile(ProfileState profileState) {
if (_controllersInitialized) return;
_controllersInitialized = true;
final profile = profileState.profile;
final isAgent = profileState.isAgent;
final firstName = profile['firstName'] as String? ?? '';
final lastName = profile['lastName'] as String? ?? '';
_fullNameController.text = '$firstName $lastName'.trim();
_emailController.text = profile['email'] as String? ??
(profile['user'] is Map
? (profile['user'] as Map)['email'] as String? ?? ''
: '');
_phoneController.text = profile['phone'] as String? ?? '';
if (isAgent) {
// Agent: headline from agentType.name or bio
final agentType = profile['agentType'] as Map<String, dynamic>?;
_titleController.text =
agentType?['name'] as String? ?? profile['bio'] as String? ?? '';
// Agent: serviceAreas for location
final serviceAreas = profile['serviceAreas'];
if (serviceAreas is List && serviceAreas.isNotEmpty) {
_locationController.text = serviceAreas.first.toString();
}
} else {
// User: headline
_titleController.text = profile['headline'] as String? ?? '';
// User: city/state/country
final city = profile['city'] as String? ?? '';
final stateName = profile['state'] as String? ?? '';
final country = profile['country'] as String? ?? '';
final parts = [city, stateName, country].where((s) => s.isNotEmpty);
_locationController.text = parts.join(', ');
}
}
@override
Widget build(BuildContext context) {
final profileState = ref.watch(profileProvider);
if (!profileState.isLoading && profileState.profile.isNotEmpty) {
_initControllersFromProfile(profileState);
}
// Show success/error snackbars
ref.listen<ProfileState>(profileProvider, (prev, next) {
if (next.successMessage != null && prev?.successMessage == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.successMessage!),
backgroundColor: const Color(0xFF638559),
),
);
}
if (next.errorMessage != null && prev?.errorMessage == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(next.errorMessage!),
backgroundColor: Colors.red,
),
);
}
});
if (profileState.isLoading) {
return const Center(
child: CircularProgressIndicator(
color: AppColors.accentOrange,
),
);
}
return Column(
children: [
const SizedBox(height: 16),
_buildTabBar(),
_buildProgressBar(),
Expanded(
child: _selectedTab == 0
? _buildProfileSettingsTab(profileState)
: _selectedTab == 1
? _buildNotificationsTab()
: _buildPrivacyTab(),
),
],
);
}
// ── Tab Bar ──
Widget _buildTabBar() {
return SizedBox(
height: 46,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 7),
children: [
_buildTab(0, 'Profile Settings', Icons.person),
const SizedBox(width: 6),
_buildTab(1, 'Notifications', Icons.notifications),
const SizedBox(width: 6),
_buildTab(2, 'Privacy', Icons.person),
],
),
);
}
Widget _buildTab(int index, String label, IconData icon) {
final isActive = _selectedTab == index;
return GestureDetector(
onTap: () => setState(() => _selectedTab = index),
child: Container(
width: 162,
height: 46,
decoration: BoxDecoration(
color: isActive ? AppColors.accentOrange : Colors.transparent,
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: isActive
? AppColors.accentOrange
: AppColors.primaryDark.withValues(alpha: 0.2),
width: 0.1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 24, color: AppColors.primaryDark),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
);
}
Widget _buildProgressBar() {
final progress = (_selectedTab + 1) / 3;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 16),
child: Container(
height: 5,
decoration: BoxDecoration(
color: const Color(0xFFD9D9D9),
borderRadius: BorderRadius.circular(15),
),
child: Align(
alignment: Alignment.centerLeft,
child: FractionallySizedBox(
widthFactor: progress,
child: Container(
decoration: BoxDecoration(
color: AppColors.primaryDark.withValues(alpha: 0.75),
borderRadius: BorderRadius.circular(15),
),
),
),
),
),
);
}
// ── Profile Settings Tab ──
Widget _buildProfileSettingsTab(ProfileState profileState) {
final profile = profileState.profile;
final isAgent = profileState.isAgent;
// Get avatar URL — agent avatar might be an S3 key or full URL
String? avatarUrl = profile['avatar'] as String?;
if (avatarUrl == null || avatarUrl.isEmpty) {
// Try nested user object (agent profile structure)
final user = profile['user'] as Map<String, dynamic>?;
avatarUrl = user?['avatar'] as String?;
}
final descriptionText = isAgent
? 'This information will be displayed on your public agent page.'
: 'This information will be displayed on your profile.';
return ListView(
padding: const EdgeInsets.fromLTRB(31, 10, 31, 30),
children: [
// Section title
const Text(
'Public Profile',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
Text(
descriptionText,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 39),
// Avatar
Center(
child: Column(
children: [
Container(
width: 71,
height: 69,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: AppColors.accentOrange,
width: 2,
),
),
child: ClipOval(
child: avatarUrl != null && avatarUrl.isNotEmpty
? Image.network(
avatarUrl,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(
Icons.person,
size: 36,
color: AppColors.primaryDark,
),
)
: const Icon(
Icons.person,
size: 36,
color: AppColors.primaryDark,
),
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: _pickAvatar,
child: Container(
width: 79,
height: 24,
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(7),
),
alignment: Alignment.center,
child: const Text(
'Upload Now',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 11,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
const SizedBox(width: 6),
GestureDetector(
onTap: () {
ref
.read(profileProvider.notifier)
.updateProfile({'avatar': ''});
},
child: Container(
width: 79,
height: 24,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark,
width: 0.1,
),
),
alignment: Alignment.center,
child: const Text(
'Delete',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 11,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
],
),
],
),
),
const SizedBox(height: 49),
// Form fields
_buildFormField('Full Name', _fullNameController),
const SizedBox(height: 24),
_buildFormField(
isAgent ? 'Career / Agent Type' : 'Professional Title',
_titleController,
enabled: !isAgent, // Agent type is read-only
),
const SizedBox(height: 24),
_buildFormField('Email Address', _emailController, enabled: false),
const SizedBox(height: 24),
_buildFormField('Phone Number', _phoneController,
keyboardType: TextInputType.phone),
const SizedBox(height: 24),
_buildFormField(
isAgent ? 'Service Areas' : 'Location',
_locationController,
prefixIcon: Icons.location_on,
),
const SizedBox(height: 30),
// Action buttons
_buildActionButtons(onSave: _saveProfileSettings),
],
);
}
Widget _buildFormField(
String label,
TextEditingController controller, {
bool enabled = true,
TextInputType? keyboardType,
IconData? prefixIcon,
}) {
final borderSide = BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
);
final border = OutlineInputBorder(
borderRadius: BorderRadius.circular(7),
borderSide: borderSide,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 14),
SizedBox(
height: 38,
child: TextField(
controller: controller,
enabled: enabled,
keyboardType: keyboardType,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
decoration: InputDecoration(
contentPadding: const EdgeInsets.symmetric(
horizontal: 26,
vertical: 8,
),
border: border,
enabledBorder: border,
focusedBorder: border,
disabledBorder: border,
prefixIcon: prefixIcon != null
? Icon(prefixIcon, size: 18, color: AppColors.accentOrange)
: null,
prefixIconConstraints: prefixIcon != null
? const BoxConstraints(minWidth: 40)
: null,
),
),
),
],
);
}
void _pickAvatar() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Avatar upload coming soon')),
);
}
void _saveProfileSettings() {
final profileState = ref.read(profileProvider);
final isAgent = profileState.isAgent;
final nameParts = _fullNameController.text.trim().split(RegExp(r'\s+'));
final firstName = nameParts.first;
final lastName =
nameParts.length > 1 ? nameParts.sublist(1).join(' ') : '';
final data = <String, dynamic>{
'firstName': firstName,
'lastName': lastName,
'phone': _phoneController.text.trim(),
};
if (isAgent) {
// Agent: serviceAreas as array
final location = _locationController.text.trim();
if (location.isNotEmpty) {
data['serviceAreas'] = [location];
}
} else {
// User: city/state/country + headline
data['headline'] = _titleController.text.trim();
final locationParts = _locationController.text
.split(',')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
if (locationParts.isNotEmpty) data['city'] = locationParts[0];
if (locationParts.length > 1) data['state'] = locationParts[1];
if (locationParts.length > 2) data['country'] = locationParts[2];
}
ref.read(profileProvider.notifier).updateProfile(data);
}
// ── Notifications Tab ──
Widget _buildNotificationsTab() {
return ListView(
padding: const EdgeInsets.fromLTRB(27, 10, 27, 30),
children: [
// Title — Figma: Fractul Bold 15px, y=263
const Text(
'Notification Preferences',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 15,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
// Description — Figma: SourceSerif4 Regular 14px, y=291
const Text(
'Choose what you want to be notified about and how.',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 20),
// Divider — Figma: y=322
_buildSectionDivider(),
const SizedBox(height: 19),
// New Lead Alerts — Figma: title y=341
_buildNotificationCategory(
title: 'New Lead Alerts',
description: 'Choose what you want to be notified about and how.',
prefKey: 'newLeadAlerts',
),
// Divider — Figma: y=416
_buildSectionDivider(),
const SizedBox(height: 13),
// Message Updates — Figma: title y=429
_buildNotificationCategory(
title: 'Message Updates',
description:
'Notices about new messages from clients or other agents.',
prefKey: 'messageUpdates',
),
// Divider — Figma: y=511
_buildSectionDivider(),
const SizedBox(height: 16),
// Urgent Requests & Tours — Figma: title y=527
_buildNotificationCategory(
title: 'Urgent Requests & Tours',
description:
'Immediate notifications for tour requests and time-sensitive items.',
prefKey: 'urgentRequests',
),
// Divider — Figma: y=621
_buildSectionDivider(),
const SizedBox(height: 20),
// Email Digest Frequency — Figma: Fractul Bold 14px, y=641
const Text(
'Email Digest Frequency',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 8),
// Description — Figma: y=667
const Text(
'Choose how often you receive non-urgent email summaries',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 16),
// Frequency selector — Figma: y=731
_buildFrequencySelector(),
const SizedBox(height: 40),
// Action buttons — Figma: y=879, rounded-[15px]
_buildNotificationActionButtons(),
],
);
}
Widget _buildSectionDivider() {
return Divider(
color: AppColors.primaryDark.withValues(alpha: 0.15),
height: 1,
thickness: 1,
);
}
Widget _buildNotificationCategory({
required String title,
required String description,
required String prefKey,
}) {
final prefs = _notificationPrefs[prefKey]!;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Left: title + description (max width ~220 from Figma)
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppColors.primaryDark,
),
),
const SizedBox(height: 4),
Text(
description,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
),
const SizedBox(width: 20),
// Right: checkbox labels — Figma: x=321, Email/In-App stacked ~26px apart
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCheckboxRow(
label: 'Email',
value: prefs['email']!,
onChanged: (val) {
setState(() {
_notificationPrefs[prefKey]!['email'] = val ?? false;
});
},
),
const SizedBox(height: 6),
_buildCheckboxRow(
label: 'In-App',
value: prefs['inApp']!,
onChanged: (val) {
setState(() {
_notificationPrefs[prefKey]!['inApp'] = val ?? false;
});
},
),
],
),
],
),
);
}
Widget _buildCheckboxRow({
required String label,
required bool value,
required ValueChanged<bool?> onChanged,
}) {
return GestureDetector(
onTap: () => onChanged(!value),
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 18,
height: 18,
child: Checkbox(
value: value,
onChanged: onChanged,
activeColor: AppColors.primaryDark,
checkColor: Colors.white,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(3),
),
side: BorderSide(
color: AppColors.primaryDark.withValues(alpha: 0.4),
width: 1,
),
),
),
const SizedBox(width: 4),
Text(
label,
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
],
),
);
}
/// Figma: h=32, w=278, rounded-[7px], with vertical dividers and inset orange pill
Widget _buildFrequencySelector() {
const options = ['instant', 'daily', 'weekly', 'off'];
const labels = ['Instant', 'Daily', 'Weekly', 'Off'];
return Container(
height: 32,
width: 278,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Row(
children: List.generate(options.length * 2 - 1, (index) {
// Odd indices = vertical dividers
if (index.isOdd) {
return Container(
width: 1,
height: 32,
color: AppColors.primaryDark.withValues(alpha: 0.15),
);
}
final i = index ~/ 2;
final isSelected = _digestFrequency == options[i];
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _digestFrequency = options[i]),
child: Center(
child: isSelected
? Container(
height: 22,
margin: const EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Text(
labels[i],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
)
: Text(
labels[i],
style: const TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
);
}),
),
),
);
}
/// Notification tab buttons — Figma: rounded-[15px] (different from profile tab)
Widget _buildNotificationActionButtons() {
final isSaving = ref.watch(profileProvider).isSaving;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => context.pop(),
child: Container(
height: 31,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.15),
width: 1,
),
),
alignment: Alignment.center,
child: const Text(
'Cancel',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: isSaving ? null : _saveNotificationSettings,
child: Container(
height: 31,
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryDark,
),
)
: const Text(
'Save Changes',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
),
],
),
);
}
void _saveNotificationSettings() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Notification settings saved'),
backgroundColor: Color(0xFF638559),
),
);
}
// ── Privacy Tab ──
Widget _buildPrivacyTab() {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Privacy settings coming soon',
style: TextStyle(
fontFamily: 'SourceSerif4',
fontSize: 16,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
);
}
// ── Shared Action Buttons ──
Widget _buildActionButtons({required VoidCallback onSave}) {
final isSaving = ref.watch(profileProvider).isSaving;
return Container(
height: 71,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: () => context.pop(),
child: Container(
width: 143,
height: 31,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: AppColors.primaryDark.withValues(alpha: 0.1),
width: 1,
),
),
alignment: Alignment.center,
child: const Text(
'Cancel',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
const SizedBox(width: 10),
GestureDetector(
onTap: isSaving ? null : onSave,
child: Container(
width: 143,
height: 31,
decoration: BoxDecoration(
color: AppColors.accentOrange,
borderRadius: BorderRadius.circular(7),
),
alignment: Alignment.center,
child: isSaving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryDark,
),
)
: const Text(
'Save Changes',
style: TextStyle(
fontFamily: 'Fractul',
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.primaryDark,
),
),
),
),
],
),
);
}
}