import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; import 'package:intl/intl.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/features/auth/presentation/providers/auth_provider.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; import 'package:real_estate_mobile/features/profile/presentation/providers/profile_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class ProfileSettingsScreen extends ConsumerStatefulWidget { const ProfileSettingsScreen({super.key}); @override ConsumerState createState() => _ProfileSettingsScreenState(); } class _ProfileSettingsScreenState extends ConsumerState { int _selectedTab = 0; // Profile form controllers late TextEditingController _fullNameController; late TextEditingController _titleController; late TextEditingController _emailController; late TextEditingController _phoneController; late TextEditingController _locationController; // Password controllers final _currentPasswordController = TextEditingController(); final _newPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); bool _showCurrentPassword = false; bool _showNewPassword = false; bool _showConfirmPassword = false; bool _isChangingPassword = false; // Notification preferences final Map> _notificationPrefs = { 'newLeadAlerts': {'email': true, 'inApp': true}, 'messageUpdates': {'email': false, 'inApp': true}, 'urgentRequests': {'email': true, 'inApp': true}, }; String _digestFrequency = 'instant'; bool _notificationsLoaded = false; bool _isLoadingNotifications = false; bool _isSavingNotifications = false; // Privacy preferences String _profileVisibility = 'public'; String _contactInfo = 'connections'; String _activityStatus = 'public'; bool _shareAnalytics = false; bool _personalizedAds = false; bool _thirdPartySharing = false; bool _isLoadingPrivacy = false; bool _isSavingPrivacy = false; bool _privacyLoaded = false; // Testimonials (agent only) List> _testimonials = []; bool _isLoadingTestimonials = false; String _testimonialSort = 'latest'; String? _testimonialLink; bool _isGeneratingLink = false; bool _linkCopied = false; // Billing (agent only) Map? _subscription; bool _isLoadingSubscription = false; bool _controllersInitialized = false; bool _isUploadingAvatar = false; File? _localAvatarFile; @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(); _currentPasswordController.dispose(); _newPasswordController.dispose(); _confirmPasswordController.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) { final agentType = profile['agentType'] as Map?; _titleController.text = agentType?['name'] as String? ?? profile['bio'] as String? ?? ''; final serviceAreas = profile['serviceAreas']; if (serviceAreas is List && serviceAreas.isNotEmpty) { _locationController.text = serviceAreas.first.toString(); } } else { _titleController.text = profile['headline'] as String? ?? ''; 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(', '); } } List<_TabItem> _getTabs(bool isAgent) { final tabs = <_TabItem>[ _TabItem(label: 'Profile Settings', icon: Icons.person_outline), _TabItem(label: 'Change Password', icon: Icons.lock_outline), _TabItem(label: 'Notifications', icon: Icons.notifications_none), _TabItem(label: 'Privacy', icon: Icons.shield_outlined), ]; if (isAgent) { tabs.add( _TabItem(label: 'Billings & Payments', icon: Icons.payment_outlined)); tabs.add( _TabItem(label: 'Add Testimonials', icon: Icons.rate_review_outlined)); } return tabs; } @override Widget build(BuildContext context) { final profileState = ref.watch(profileProvider); if (!profileState.isLoading && profileState.profile.isNotEmpty) { _initControllersFromProfile(profileState); } ref.listen(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), ); } final tabs = _getTabs(profileState.isAgent); final totalTabs = tabs.length; return Column( children: [ const SizedBox(height: 16), _buildTabBar(tabs), _buildProgressBar(totalTabs), Expanded(child: _buildSelectedTab(profileState, tabs)), ], ); } Widget _buildSelectedTab(ProfileState profileState, List<_TabItem> tabs) { final label = tabs[_selectedTab].label; switch (label) { case 'Profile Settings': return _buildProfileSettingsTab(profileState); case 'Change Password': return _buildChangePasswordTab(); case 'Notifications': return _buildNotificationsTab(); case 'Privacy': return _buildPrivacyTab(); case 'Billings & Payments': return _buildBillingTab(); case 'Add Testimonials': return _buildTestimonialsTab(); default: return const SizedBox.shrink(); } } // ── Tab Bar ── Widget _buildTabBar(List<_TabItem> tabs) { return SizedBox( height: 46, child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 7), itemCount: tabs.length, separatorBuilder: (_, _) => const SizedBox(width: 6), itemBuilder: (_, i) => _buildTab(i, tabs[i], tabs), ), ); } Widget _buildTab(int index, _TabItem tab, List<_TabItem> tabs) { final isActive = _selectedTab == index; return GestureDetector( onTap: () { setState(() => _selectedTab = index); if (tab.label == 'Privacy') _loadPrivacyPreferences(); if (tab.label == 'Notifications') _loadNotificationPreferences(); if (tab.label == 'Add Testimonials') _loadTestimonials(); if (tab.label == 'Billings & Payments') _loadSubscription(); }, 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(tab.icon, size: 20, color: isActive ? Colors.white : AppColors.primaryDark), const SizedBox(width: 8), Flexible( child: Text( tab.label, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w400, color: isActive ? Colors.white : AppColors.primaryDark, ), overflow: TextOverflow.ellipsis, ), ), ], ), ), ); } Widget _buildProgressBar(int totalTabs) { final progress = (_selectedTab + 1) / totalTabs; 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), ), ), ), ), ), ); } // ══════════════════════════════════════════════════════════════════ // ── TAB 1: Profile Settings ── // ══════════════════════════════════════════════════════════════════ Widget _buildProfileSettingsTab(ProfileState profileState) { final profile = profileState.profile; final isAgent = profileState.isAgent; String? avatarUrl = profile['avatar'] as String?; if (avatarUrl == null || avatarUrl.isEmpty) { final user = profile['user'] as Map?; 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: [ 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: _isUploadingAvatar ? const Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.accentOrange, ), ), ) : _localAvatarFile != null ? Image.file(_localAvatarFile!, width: 71, height: 69, fit: BoxFit.cover) : avatarUrl != null && avatarUrl.isNotEmpty ? S3Image( imageUrl: avatarUrl, width: 71, height: 69, fit: BoxFit.cover, ) : const Icon(Icons.person, size: 36, color: AppColors.primaryDark), ), ), const SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( onTap: _isUploadingAvatar ? null : _pickAvatar, child: Container( width: 95, height: 24, decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(7), ), alignment: Alignment.center, child: _isUploadingAvatar ? const SizedBox( width: 14, height: 14, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primaryDark, ), ) : const Text('Upload Now', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 11, fontWeight: FontWeight.w400, color: AppColors.primaryDark, )), ), ), const SizedBox(width: 6), GestureDetector( onTap: _isUploadingAvatar ? null : _deleteAvatar, 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), _buildFormField('Full Name', _fullNameController), const SizedBox(height: 24), _buildFormField( isAgent ? 'Career / Agent Type' : 'Professional Title', _titleController, enabled: !isAgent, ), 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), _buildActionButtons(onSave: _saveProfileSettings), ], ); } // ══════════════════════════════════════════════════════════════════ // ── TAB 2: Change Password ── // ══════════════════════════════════════════════════════════════════ Widget _buildChangePasswordTab() { return ListView( padding: const EdgeInsets.fromLTRB(27, 10, 27, 30), children: [ const Text( 'Password & Security', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), const Text( 'Update your password to keep your account secure.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), const SizedBox(height: 30), _buildPasswordField( label: 'Current Password', controller: _currentPasswordController, obscure: !_showCurrentPassword, onToggle: () => setState(() => _showCurrentPassword = !_showCurrentPassword), ), const SizedBox(height: 24), _buildPasswordField( label: 'New Password', controller: _newPasswordController, obscure: !_showNewPassword, onToggle: () => setState(() => _showNewPassword = !_showNewPassword), ), const SizedBox(height: 4), Text( 'Min 8 characters, 1 uppercase, 1 lowercase, 1 number', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 11, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), const SizedBox(height: 24), _buildPasswordField( label: 'Confirm New Password', controller: _confirmPasswordController, obscure: !_showConfirmPassword, onToggle: () => setState(() => _showConfirmPassword = !_showConfirmPassword), ), const SizedBox(height: 40), _buildActionButtons( onSave: _changePassword, isSavingOverride: _isChangingPassword, saveLabel: 'Update Password', ), ], ); } Widget _buildPasswordField({ required String label, required TextEditingController controller, required bool obscure, required VoidCallback onToggle, }) { 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, obscureText: obscure, 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, suffixIcon: GestureDetector( onTap: onToggle, child: Icon( obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), suffixIconConstraints: const BoxConstraints(minWidth: 40), ), ), ), ], ); } Future _changePassword() async { final current = _currentPasswordController.text.trim(); final newPw = _newPasswordController.text.trim(); final confirm = _confirmPasswordController.text.trim(); if (current.isEmpty || newPw.isEmpty || confirm.isEmpty) { _showSnackBar('Please fill in all fields', isError: true); return; } final passwordRegex = RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$'); if (!passwordRegex.hasMatch(newPw)) { _showSnackBar( 'Password must be at least 8 characters with uppercase, lowercase, and number', isError: true); return; } if (newPw != confirm) { _showSnackBar('Passwords do not match', isError: true); return; } setState(() => _isChangingPassword = true); try { final repo = ref.read(profileRepositoryProvider); await repo.changePassword(current, newPw); _currentPasswordController.clear(); _newPasswordController.clear(); _confirmPasswordController.clear(); _showSnackBar('Password changed successfully'); } catch (e) { _showSnackBar('$e', isError: true); } finally { if (mounted) setState(() => _isChangingPassword = false); } } // ══════════════════════════════════════════════════════════════════ // ── TAB 3: Notifications ── // ══════════════════════════════════════════════════════════════════ Future _loadNotificationPreferences() async { if (_notificationsLoaded) return; setState(() => _isLoadingNotifications = true); try { final role = ref.read(profileProvider).role; final repo = ref.read(profileRepositoryProvider); final data = await repo.getNotificationPreferences(role); final notifications = data['notifications'] as Map? ?? {}; setState(() { for (final key in ['new_lead_alerts', 'message_updates', 'urgent_requests_tours']) { final mapped = key == 'new_lead_alerts' ? 'newLeadAlerts' : key == 'message_updates' ? 'messageUpdates' : 'urgentRequests'; final vals = notifications[key] as Map?; if (vals != null) { _notificationPrefs[mapped] = { 'email': vals['email'] as bool? ?? false, 'inApp': vals['inApp'] as bool? ?? true, }; } } _digestFrequency = data['digestFrequency'] as String? ?? 'instant'; _isLoadingNotifications = false; _notificationsLoaded = true; }); } catch (_) { setState(() { _isLoadingNotifications = false; _notificationsLoaded = true; }); } } Widget _buildNotificationsTab() { if (_isLoadingNotifications) { return const Center( child: CircularProgressIndicator(color: AppColors.accentOrange)); } return ListView( padding: const EdgeInsets.fromLTRB(27, 10, 27, 30), children: [ const Text( 'Notification Preferences', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), 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), _buildSectionDivider(), const SizedBox(height: 19), _buildNotificationCategory( title: 'New Lead Alerts', description: 'Notifications about new leads and inquiries.', prefKey: 'newLeadAlerts', ), _buildSectionDivider(), const SizedBox(height: 13), _buildNotificationCategory( title: 'Message Updates', description: 'Notices about new messages from clients or other agents.', prefKey: 'messageUpdates', ), _buildSectionDivider(), const SizedBox(height: 16), _buildNotificationCategory( title: 'Urgent Requests & Tours', description: 'Immediate notifications for tour requests and time-sensitive items.', prefKey: 'urgentRequests', ), _buildSectionDivider(), const SizedBox(height: 20), const Text( 'Email Digest Frequency', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), 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), _buildFrequencySelector(), const SizedBox(height: 40), _buildActionButtons( onSave: _saveNotificationSettings, isSavingOverride: _isSavingNotifications, ), ], ); } Future _saveNotificationSettings() async { setState(() => _isSavingNotifications = true); try { final role = ref.read(profileProvider).role; final repo = ref.read(profileRepositoryProvider); await repo.updateNotificationPreferences(role, { 'notifications': { 'new_lead_alerts': _notificationPrefs['newLeadAlerts'], 'message_updates': _notificationPrefs['messageUpdates'], 'urgent_requests_tours': _notificationPrefs['urgentRequests'], }, 'digestFrequency': _digestFrequency, }); _showSnackBar('Notification settings saved'); } catch (e) { _showSnackBar('Failed to save: $e', isError: true); } finally { if (mounted) setState(() => _isSavingNotifications = false); } } // ══════════════════════════════════════════════════════════════════ // ── TAB 4: Privacy ── // ══════════════════════════════════════════════════════════════════ Future _loadPrivacyPreferences() async { if (_privacyLoaded) return; setState(() => _isLoadingPrivacy = true); try { final role = ref.read(profileProvider).role; final repo = ref.read(profileRepositoryProvider); final data = await repo.getPrivacyPreferences(role); final privacy = data['privacySettings'] as Map? ?? {}; final dataSettings = data['dataSettings'] as Map? ?? {}; setState(() { _profileVisibility = privacy['profile_visibility'] as String? ?? 'public'; _contactInfo = privacy['contact_info'] as String? ?? 'connections'; _activityStatus = privacy['activity_status'] as String? ?? 'public'; _shareAnalytics = dataSettings['shareAnalytics'] as bool? ?? false; _personalizedAds = dataSettings['personalizedAds'] as bool? ?? false; _thirdPartySharing = dataSettings['thirdPartySharing'] as bool? ?? false; _isLoadingPrivacy = false; _privacyLoaded = true; }); } catch (_) { setState(() => _isLoadingPrivacy = false); _privacyLoaded = true; } } Future _savePrivacyPreferences() async { setState(() => _isSavingPrivacy = true); try { final role = ref.read(profileProvider).role; final repo = ref.read(profileRepositoryProvider); await repo.updatePrivacyPreferences(role, { 'privacySettings': { 'profile_visibility': _profileVisibility, 'contact_info': _contactInfo, 'activity_status': _activityStatus, }, 'dataSettings': { 'shareAnalytics': _shareAnalytics, 'personalizedAds': _personalizedAds, 'thirdPartySharing': _thirdPartySharing, }, }); _showSnackBar('Privacy settings saved'); } catch (e) { _showSnackBar('Failed to save: $e', isError: true); } finally { if (mounted) setState(() => _isSavingPrivacy = false); } } Future _deleteAccount() async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete Account', style: TextStyle( fontFamily: 'Fractul', fontWeight: FontWeight.w700, color: AppColors.primaryDark)), content: const Text( 'Are you sure you want to delete your account? This action cannot be undone and all your data will be permanently removed.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Delete')), ], ), ); if (confirmed != true || !mounted) return; final doubleConfirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Final Confirmation', style: TextStyle( fontFamily: 'Fractul', fontWeight: FontWeight.w700, color: Colors.red)), content: const Text( 'This will permanently delete your account, profile, and all associated data. Are you absolutely sure?', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.red), child: const Text('Yes, Delete My Account'), ), ], ), ); if (doubleConfirmed != true || !mounted) return; try { final role = ref.read(profileProvider).role; final repo = ref.read(profileRepositoryProvider); await repo.deleteAccount(role); if (mounted) ref.read(authProvider.notifier).logout(); } catch (e) { if (mounted) _showSnackBar('Failed to delete account: $e', isError: true); } } Widget _buildPrivacyTab() { if (_isLoadingPrivacy) { return const Center( child: CircularProgressIndicator(color: AppColors.accentOrange)); } return SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildSectionCard( title: 'Privacy Settings', subtitle: 'Control your privacy and visibility preferences', child: Column( children: [ _buildDropdownRow( label: 'Profile Visibility', description: 'Control who can see your profile', value: _profileVisibility, options: const { 'public': 'Public', 'connections': 'Connections Only', 'private': 'Private', }, onChanged: (v) => setState(() => _profileVisibility = v ?? _profileVisibility), ), const Divider(height: 24, color: Color(0xFFE8E8E8)), _buildDropdownRow( label: 'Contact Information', description: 'Control who can see your contact details', value: _contactInfo, options: const { 'public': 'Public', 'connections': 'Connections Only', 'private': 'Hidden', }, onChanged: (v) => setState(() => _contactInfo = v ?? _contactInfo), ), const Divider(height: 24, color: Color(0xFFE8E8E8)), _buildDropdownRow( label: 'Activity Status', description: 'Show when you are active on the platform', value: _activityStatus, options: const { 'public': 'Everyone', 'connections': 'Connections Only', 'private': 'No One', }, onChanged: (v) => setState(() => _activityStatus = v ?? _activityStatus), ), ], ), ), const SizedBox(height: 20), _buildSectionCard( title: 'Data & Personalization', subtitle: 'Manage how your data is used', child: Column( children: [ _buildToggleRow( label: 'Share Analytics', description: 'Help improve our service by sharing usage data', value: _shareAnalytics, onChanged: (v) => setState(() => _shareAnalytics = v), ), const Divider(height: 24, color: Color(0xFFE8E8E8)), _buildToggleRow( label: 'Personalized Ads', description: 'See ads tailored to your interests', value: _personalizedAds, onChanged: (v) => setState(() => _personalizedAds = v), ), const Divider(height: 24, color: Color(0xFFE8E8E8)), _buildToggleRow( label: 'Third-Party Data Sharing', description: 'Allow sharing data with third-party partners', value: _thirdPartySharing, onChanged: (v) => setState(() => _thirdPartySharing = v), ), ], ), ), const SizedBox(height: 20), _buildActionButtons( onSave: _savePrivacyPreferences, isSavingOverride: _isSavingPrivacy), const SizedBox(height: 24), _buildSectionCard( title: 'Danger Zone', subtitle: 'Irreversible and destructive actions', borderColor: Colors.red.withValues(alpha: 0.3), titleColor: Colors.red, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Once you delete your account, there is no going back. All your data will be permanently removed.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.hintText), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: OutlinedButton( onPressed: _deleteAccount, style: OutlinedButton.styleFrom( foregroundColor: Colors.red, side: const BorderSide(color: Colors.red, width: 1.5), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), child: const Text('Delete Account', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700)), ), ), ], ), ), const SizedBox(height: 32), ], ), ); } // ══════════════════════════════════════════════════════════════════ // ── TAB 5: Billings & Payments (Agent only) ── // ══════════════════════════════════════════════════════════════════ Future _loadSubscription() async { if (_isLoadingSubscription) return; setState(() => _isLoadingSubscription = true); try { final repo = ref.read(profileRepositoryProvider); final sub = await repo.getSubscription(); setState(() { _subscription = sub; _isLoadingSubscription = false; }); } catch (_) { setState(() => _isLoadingSubscription = false); } } Widget _buildBillingTab() { if (_isLoadingSubscription) { return const Center( child: CircularProgressIndicator(color: AppColors.accentOrange)); } final hasActive = _subscription != null && _subscription!['status'] == 'ACTIVE'; final plan = _subscription?['plan'] as Map?; return ListView( padding: const EdgeInsets.fromLTRB(24, 10, 24, 30), children: [ const Text( 'Billings & Payments', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), const Text( 'Manage your subscription and billing details.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), const SizedBox(height: 24), if (hasActive) ...[ // Active subscription card _buildSectionCard( title: 'Current Subscription', subtitle: 'Your active plan details', child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.check_circle, color: Color(0xFF4CAF50), size: 20), const SizedBox(width: 8), Text( plan?['name'] as String? ?? 'Premium', style: const TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF4CAF50).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(10), ), child: const Text('Active', style: TextStyle( fontFamily: 'Fractul', fontSize: 12, fontWeight: FontWeight.w600, color: Color(0xFF4CAF50))), ), ], ), if (plan?['amount'] != null) ...[ const SizedBox(height: 12), Text( '\$${plan!['amount']}/${plan['interval'] ?? 'year'}', style: const TextStyle( fontFamily: 'Fractul', fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ], if (_subscription?['currentPeriodEnd'] != null) ...[ const SizedBox(height: 8), Text( 'Renews on ${_formatDate(_subscription!['currentPeriodEnd'] as String)}', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), ], const SizedBox(height: 20), // Plan features ..._buildPlanFeatures(), const SizedBox(height: 20), // Manage Subscription SizedBox( width: double.infinity, child: OutlinedButton( onPressed: _openBillingPortal, style: OutlinedButton.styleFrom( foregroundColor: AppColors.primaryDark, side: BorderSide( color: AppColors.primaryDark.withValues(alpha: 0.2)), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), child: const Text('Manage Subscription', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600)), ), ), const SizedBox(height: 10), // Cancel SizedBox( width: double.infinity, child: TextButton( onPressed: _cancelSubscription, style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Cancel Subscription', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500)), ), ), ], ), ), ] else ...[ // No subscription — upgrade card _buildSectionCard( title: 'Get Premium Access', subtitle: 'Unlock all features with our premium plan', child: Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.accentOrange.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.accentOrange.withValues(alpha: 0.3)), ), child: Column( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4), decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(10), ), child: const Text('Best Value', style: TextStyle( fontFamily: 'Fractul', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white)), ), const SizedBox(height: 12), const Text( '\$499/Year', style: TextStyle( fontFamily: 'Fractul', fontSize: 28, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 16), ..._buildPlanFeatures(), ], ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _startCheckout, style: ElevatedButton.styleFrom( backgroundColor: AppColors.accentOrange, foregroundColor: AppColors.primaryDark, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), child: const Text('Get Premium', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700)), ), ), ], ), ), ], const SizedBox(height: 24), // Need help section Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1)), ), child: Row( children: [ Icon(Icons.help_outline, color: AppColors.primaryDark.withValues(alpha: 0.6)), const SizedBox(width: 12), const Expanded( child: Text( 'Need help with billing? Contact support.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark, ), ), ), ], ), ), ], ); } List _buildPlanFeatures() { const features = [ 'Agent Co-Marketing', 'Priority 24/7 Support', 'Whitelabel Client Portals', 'Advanced CRM Tools & Analytics', ]; return features .map((f) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ const Icon(Icons.check_circle_outline, size: 18, color: AppColors.accentOrange), const SizedBox(width: 10), Text(f, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark)), ], ), )) .toList(); } Future _openBillingPortal() async { try { final repo = ref.read(profileRepositoryProvider); final url = await repo.createPortalSession(); if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } } catch (e) { if (mounted) _showSnackBar('Failed to open billing portal: $e', isError: true); } } Future _startCheckout() async { try { final repo = ref.read(profileRepositoryProvider); final plans = await repo.getStripePlans(); if (plans.isEmpty) { _showSnackBar('No plans available', isError: true); return; } final planId = plans.first['id'] as String; final url = await repo.createCheckoutSession(planId); if (await canLaunchUrl(Uri.parse(url))) { await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); } } catch (e) { if (mounted) _showSnackBar('Failed to start checkout: $e', isError: true); } } Future _cancelSubscription() async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Cancel Subscription', style: TextStyle( fontFamily: 'Fractul', fontWeight: FontWeight.w700, color: AppColors.primaryDark)), content: const Text( 'Are you sure you want to cancel your subscription? You will lose access to premium features at the end of the billing period.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Keep Subscription')), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Yes, Cancel'), ), ], ), ); if (confirmed != true || !mounted) return; try { final repo = ref.read(profileRepositoryProvider); await repo.cancelSubscription(); _showSnackBar('Subscription cancelled'); _loadSubscription(); } catch (e) { if (mounted) _showSnackBar('Failed to cancel: $e', isError: true); } } // ══════════════════════════════════════════════════════════════════ // ── TAB 6: Testimonials (Agent only) ── // ══════════════════════════════════════════════════════════════════ Future _loadTestimonials() async { if (_isLoadingTestimonials) return; setState(() => _isLoadingTestimonials = true); try { final repo = ref.read(profileRepositoryProvider); final data = await repo.getMyTestimonials(sort: _testimonialSort); setState(() { _testimonials = data; _isLoadingTestimonials = false; }); } catch (_) { setState(() => _isLoadingTestimonials = false); } } Future _generateTestimonialLink() async { setState(() => _isGeneratingLink = true); try { final repo = ref.read(profileRepositoryProvider); final token = await repo.generateTestimonialLink(); setState(() { _testimonialLink = 'https://re-quest.co/testimonials/$token'; _isGeneratingLink = false; }); } catch (e) { setState(() => _isGeneratingLink = false); if (mounted) _showSnackBar('Failed to generate link: $e', isError: true); } } Widget _buildTestimonialsTab() { return ListView( padding: const EdgeInsets.fromLTRB(24, 10, 24, 30), children: [ const Text( 'Add Testimonials', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), const Text( 'Generate a link for your clients to submit testimonials. Share it via email or messaging.', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), const SizedBox(height: 24), // Generate link section _buildSectionCard( title: 'Testimonial Collection Link', subtitle: 'Share this link with your clients', child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_testimonialLink != null) ...[ Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppColors.primaryDark.withValues(alpha: 0.05), borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1)), ), child: Text( _testimonialLink!, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark, ), ), ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: () { Clipboard.setData( ClipboardData(text: _testimonialLink!)); setState(() => _linkCopied = true); Future.delayed(const Duration(seconds: 2), () { if (mounted) setState(() => _linkCopied = false); }); }, icon: Icon( _linkCopied ? Icons.check : Icons.copy, size: 16), label: Text(_linkCopied ? 'Copied!' : 'Copy Link'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.accentOrange, side: const BorderSide(color: AppColors.accentOrange), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), ), ), ] else ...[ SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _isGeneratingLink ? null : _generateTestimonialLink, style: ElevatedButton.styleFrom( backgroundColor: AppColors.accentOrange, foregroundColor: AppColors.primaryDark, padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), child: _isGeneratingLink ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primaryDark)) : const Text('Generate Link', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700)), ), ), ], ], ), ), const SizedBox(height: 24), // Testimonials list header Row( children: [ const Text( 'Received Testimonials', style: TextStyle( fontFamily: 'Fractul', fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const Spacer(), // Sort toggle GestureDetector( onTap: () { setState(() { _testimonialSort = _testimonialSort == 'latest' ? 'oldest' : 'latest'; }); _loadTestimonials(); }, child: Row( children: [ Icon(Icons.sort, size: 16, color: AppColors.primaryDark.withValues(alpha: 0.6)), const SizedBox(width: 4), Text( _testimonialSort == 'latest' ? 'Latest' : 'Oldest', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), ], ), ), ], ), const SizedBox(height: 12), if (_isLoadingTestimonials) const Center( child: Padding( padding: EdgeInsets.all(40), child: CircularProgressIndicator(color: AppColors.accentOrange), )) else if (_testimonials.isEmpty) Container( padding: const EdgeInsets.symmetric(vertical: 40), child: Center( child: Column( children: [ Icon(Icons.rate_review_outlined, size: 48, color: AppColors.primaryDark.withValues(alpha: 0.3)), const SizedBox(height: 12), const Text( 'No testimonials yet', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), const SizedBox(height: 6), Text( 'Share your testimonial link with clients to start collecting reviews.', textAlign: TextAlign.center, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), ], ), ), ) else ..._testimonials .map((t) => _buildTestimonialCard(t)), ], ); } Widget _buildTestimonialCard(Map testimonial) { final rating = (testimonial['rating'] as num?)?.toInt() ?? 5; final text = testimonial['text'] as String? ?? ''; final authorName = testimonial['authorName'] as String? ?? ''; final authorRole = testimonial['authorRole'] as String? ?? ''; final createdAt = testimonial['createdAt'] as String?; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1)), boxShadow: [ BoxShadow( color: const Color(0xFFD9D9D9).withValues(alpha: 0.3), offset: const Offset(0, 4), blurRadius: 10, ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stars + date Row( children: [ ...List.generate( 5, (i) => Icon( i < rating ? Icons.star : Icons.star_border, size: 16, color: i < rating ? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.3), ), ), const Spacer(), if (createdAt != null) Text( _formatRelativeTime(createdAt), style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 12, color: AppColors.primaryDark.withValues(alpha: 0.5), ), ), ], ), const SizedBox(height: 10), // Text Text( text, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.5, ), ), const SizedBox(height: 12), // Author Text( authorName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), if (authorRole.isNotEmpty) Text( authorRole, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.primaryDark.withValues(alpha: 0.6), ), ), ], ), ); } // ══════════════════════════════════════════════════════════════════ // ── Shared Widgets ── // ══════════════════════════════════════════════════════════════════ 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, ), ), ), ], ); } 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: [ 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), 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 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, )), ], ), ); } 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) { 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, )), ), ), ); }), ), ), ); } Widget _buildSectionCard({ required String title, required String subtitle, required Widget child, Color borderColor = const Color(0xFFE8E8E8), Color titleColor = AppColors.primaryDark, }) { return Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all(color: borderColor, width: 1), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w700, color: titleColor, )), const SizedBox(height: 4), Text(subtitle, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, color: AppColors.hintText, )), const SizedBox(height: 16), child, ], ), ); } Widget _buildDropdownRow({ required String label, required String description, required String value, required Map options, required ValueChanged onChanged, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, )), const SizedBox(height: 2), Text(description, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 12, color: AppColors.hintText, )), const SizedBox(height: 8), Container( height: 42, padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.15)), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, icon: const Icon(Icons.keyboard_arrow_down, color: AppColors.primaryDark, size: 20), style: const TextStyle( fontFamily: 'Fractul', fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), items: options.entries .map((e) => DropdownMenuItem(value: e.key, child: Text(e.value))) .toList(), onChanged: onChanged, ), ), ), ], ); } Widget _buildToggleRow({ required String label, required String description, required bool value, required ValueChanged onChanged, }) { return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, )), const SizedBox(height: 2), Text(description, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 12, color: AppColors.hintText, )), ], ), ), const SizedBox(width: 12), GestureDetector( onTap: () => onChanged(!value), child: AnimatedContainer( duration: const Duration(milliseconds: 200), width: 44, height: 24, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: value ? AppColors.accentOrange : AppColors.primaryDark.withValues(alpha: 0.2), ), child: AnimatedAlign( duration: const Duration(milliseconds: 200), alignment: value ? Alignment.centerRight : Alignment.centerLeft, child: Container( width: 20, height: 20, margin: const EdgeInsets.symmetric(horizontal: 2), decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle), ), ), ), ), ], ); } Widget _buildActionButtons({ required VoidCallback onSave, bool? isSavingOverride, String saveLabel = 'Save Changes', }) { final isSaving = isSavingOverride ?? 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), ) : Text(saveLabel, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, )), ), ), ], ), ); } // ── Avatar methods ── Future _pickAvatar() async { final picker = ImagePicker(); final picked = await picker.pickImage( source: ImageSource.gallery, maxWidth: 800, maxHeight: 800, imageQuality: 85, ); if (picked == null) return; final ext = picked.path.split('.').last.toLowerCase(); if (!['jpg', 'jpeg', 'png', 'gif'].contains(ext)) { if (mounted) _showSnackBar('Please select a JPEG, PNG, or GIF image', isError: true); return; } final fileBytes = await picked.readAsBytes(); if (fileBytes.length > 2 * 1024 * 1024) { if (mounted) _showSnackBar('Image must be less than 2MB', isError: true); return; } setState(() { _localAvatarFile = File(picked.path); _isUploadingAvatar = true; }); try { final profileState = ref.read(profileProvider); final role = profileState.isAgent ? 'AGENT' : 'USER'; final contentType = 'image/$ext'; final fileName = picked.name; final repo = ProfileRepository(); final uploadData = await repo.getAvatarUploadUrl(role, fileName, contentType); final uploadUrl = uploadData['uploadUrl']!; final key = uploadData['key']!; await repo.uploadToS3(uploadUrl, fileBytes, contentType); await ref .read(profileProvider.notifier) .updateProfile({'avatar': key}); if (mounted) _showSnackBar('Avatar updated successfully'); } catch (e) { if (mounted) _showSnackBar('Failed to upload avatar: $e', isError: true); setState(() => _localAvatarFile = null); } finally { if (mounted) setState(() => _isUploadingAvatar = false); } } Future _deleteAvatar() async { final profileState = ref.read(profileProvider); final currentAvatar = profileState.profile['avatar'] as String?; if (currentAvatar != null && currentAvatar.isNotEmpty) { final repo = ProfileRepository(); await repo.deleteAvatar(currentAvatar); } await ref.read(profileProvider.notifier).updateProfile({'avatar': null}); setState(() => _localAvatarFile = null); if (mounted) _showSnackBar('Avatar deleted'); } 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 = { 'firstName': firstName, 'lastName': lastName, 'phone': _phoneController.text.trim(), }; if (isAgent) { final location = _locationController.text.trim(); if (location.isNotEmpty) data['serviceAreas'] = [location]; } else { 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); } // ── Helpers ── void _showSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Colors.red : const Color(0xFF638559), ), ); } String _formatDate(String dateStr) { try { final date = DateTime.parse(dateStr); return DateFormat('MMM d, y').format(date); } catch (_) { return dateStr; } } String _formatRelativeTime(String dateStr) { try { final date = DateTime.parse(dateStr); final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays == 0) return 'Today'; if (diff.inDays == 1) return 'Yesterday'; if (diff.inDays < 30) return '${diff.inDays} days ago'; if (diff.inDays < 365) return '${diff.inDays ~/ 30} months ago'; return DateFormat('MMM d, y').format(date); } catch (_) { return ''; } } } class _TabItem { final String label; final IconData icon; const _TabItem({required this.label, required this.icon}); }