import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:real_estate_mobile/core/constants/app_colors.dart'; import 'package:real_estate_mobile/core/widgets/s3_image.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agent_detail_provider.dart'; import 'package:real_estate_mobile/features/profile/data/profile_repository.dart'; import 'package:real_estate_mobile/features/home/presentation/screens/home_screen.dart'; import 'package:go_router/go_router.dart'; /// Provider that fetches the agent's own profile ID from /agents/profile/me final _agentMyProfileProvider = FutureProvider.autoDispose((ref) async { final repo = ProfileRepository(); try { final data = await repo.getMyProfile('AGENT'); return data['id'] as String?; } catch (_) { return null; } }); class AgentHomeScreen extends ConsumerWidget { const AgentHomeScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final myProfileAsync = ref.watch(_agentMyProfileProvider); return myProfileAsync.when( loading: () => const Center( child: CircularProgressIndicator( color: AppColors.accentOrange, strokeWidth: 2, ), ), error: (_, __) => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.accentOrange), const SizedBox(height: 16), const Text('Failed to load profile', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.primaryDark)), const SizedBox(height: 12), TextButton( onPressed: () => ref.invalidate(_agentMyProfileProvider), child: const Text('Retry', style: TextStyle(color: AppColors.accentOrange)), ), ], ), ), data: (agentProfileId) { if (agentProfileId == null) { return const Center( child: Text('No agent profile found', style: TextStyle( fontFamily: 'Fractul', fontSize: 16, color: AppColors.primaryDark)), ); } return _AgentHomeContent(agentProfileId: agentProfileId); }, ); } } class _AgentHomeContent extends ConsumerStatefulWidget { final String agentProfileId; const _AgentHomeContent({required this.agentProfileId}); @override ConsumerState<_AgentHomeContent> createState() => _AgentHomeContentState(); } class _AgentHomeContentState extends ConsumerState<_AgentHomeContent> { bool _isUploadingAvatar = false; bool _isTogglingAvailability = false; bool _showEmail = false; bool _showPhone = false; final Set _expandedTagSections = {}; File? _localAvatarFile; Future _pickAndUploadAvatar() async { final picker = ImagePicker(); final picked = await picker.pickImage( source: ImageSource.gallery, maxWidth: 1200, maxHeight: 1200, imageQuality: 85, ); if (picked == null) return; final file = File(picked.path); final fileName = picked.name; final ext = fileName.split('.').last.toLowerCase(); // Validate file type const allowedTypes = ['jpg', 'jpeg', 'png', 'gif']; if (!allowedTypes.contains(ext)) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Only JPG, PNG, and GIF images are allowed')), ); } return; } // Validate file size (2MB max) final fileSize = await file.length(); if (fileSize > 2 * 1024 * 1024) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Image must be smaller than 2MB')), ); } return; } final contentType = ext == 'png' ? 'image/png' : ext == 'gif' ? 'image/gif' : 'image/jpeg'; setState(() { _localAvatarFile = file; _isUploadingAvatar = true; }); try { final repo = ProfileRepository(); // 1. Get presigned upload URL final uploadData = await repo.getAvatarUploadUrl('AGENT', fileName, contentType); final uploadUrl = uploadData['uploadUrl']!; final key = uploadData['key']!; // 2. Upload to S3 final fileBytes = await file.readAsBytes(); await repo.uploadToS3(uploadUrl, fileBytes, contentType); // 3. Update profile with new avatar key await repo.updateProfile('AGENT', {'avatar': key}); // 4. Refresh the agent detail provider ref.invalidate(agentDetailProvider(widget.agentProfileId)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Photo updated successfully')), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to upload photo: ${e.toString()}')), ); } } finally { if (mounted) { setState(() { _isUploadingAvatar = false; _localAvatarFile = null; }); } } } Future _toggleAvailability(bool newValue) async { if (_isTogglingAvailability) return; setState(() => _isTogglingAvailability = true); // Optimistically update the UI immediately ref .read(agentDetailProvider(widget.agentProfileId).notifier) .updateAvailability(newValue); try { final repo = ProfileRepository(); await repo.updateProfile('AGENT', {'isAvailable': newValue}); } catch (e) { // Revert on failure if (mounted) { ref .read(agentDetailProvider(widget.agentProfileId).notifier) .updateAvailability(!newValue); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to update availability: $e')), ); } } finally { if (mounted) { setState(() => _isTogglingAvailability = false); } } } @override Widget build(BuildContext context) { final state = ref.watch(agentDetailProvider(widget.agentProfileId)); if (state.isLoading) { return const Center( child: CircularProgressIndicator(color: AppColors.accentOrange)); } if (state.error != null) { return _buildError(state.error!); } return _buildContent(state, ref); } Widget _buildError(String error) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 48, color: AppColors.accentOrange), const SizedBox(height: 16), const Text('Unable to Load Profile', style: TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 12), TextButton( onPressed: () => ref.invalidate(agentDetailProvider(widget.agentProfileId)), child: const Text('Retry', style: TextStyle(color: AppColors.accentOrange)), ), ], ), ); } Widget _buildContent(AgentDetailState state, WidgetRef ref) { final agent = state.agent!; final scrollController = ref.watch(homeScrollControllerProvider); return SingleChildScrollView( controller: scrollController, child: Column( children: [ const SizedBox(height: 12), _buildVerificationBanner(agent), _buildCoverAndAvatar(agent), const SizedBox(height: 16), _buildStatusSection(agent), const SizedBox(height: 12), _buildContactInfo(state), const SizedBox(height: 16), _buildProfileInfo(agent), const SizedBox(height: 24), _buildExperienceSection(state), const SizedBox(height: 24), _buildInfoCards(state), const SizedBox(height: 24), _buildSpecializationSection(state), const SizedBox(height: 24), _buildTestimonialsSection(state), const SizedBox(height: 24), ], ), ); } // ── Verification status banner ── Widget _buildVerificationBanner(AgentProfile agent) { final status = agent.verificationStatus; if (status == null || status == 'APPROVED') return const SizedBox.shrink(); if (status == 'REJECTED') { return Padding( padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 0), child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFFEF2F2), border: Border.all(color: const Color(0xFFFECACA)), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.warning_amber_rounded, size: 18, color: Colors.red[700]), const SizedBox(width: 8), Text('Profile Verification Rejected', style: TextStyle(fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: Colors.red[800])), ], ), if (agent.verificationNote != null && agent.verificationNote!.isNotEmpty) ...[ const SizedBox(height: 6), Text('Reason: ${agent.verificationNote}', style: TextStyle(fontFamily: 'SourceSerif4', fontSize: 13, color: Colors.red[700])), ], const SizedBox(height: 8), Text('Please update your profile and documents, then resubmit.', style: TextStyle(fontFamily: 'SourceSerif4', fontSize: 13, color: Colors.red[600])), const SizedBox(height: 10), GestureDetector( onTap: () => context.push('/agent/edit-profile'), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(8), ), child: const Text('Update & Resubmit', style: TextStyle(fontFamily: 'SourceSerif4', fontSize: 13, color: Colors.white, fontWeight: FontWeight.w600)), ), ), ], ), ), ); } if (status == 'PENDING_REVIEW') { return Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFFEFCE8), border: Border.all(color: const Color(0xFFFDE68A)), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Container(width: 10, height: 10, decoration: const BoxDecoration(color: Color(0xFFFBBF24), shape: BoxShape.circle)), const SizedBox(width: 10), const Expanded( child: Text('Profile verification is under review. You will be notified once approved.', style: TextStyle(fontFamily: 'Fractul', fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF854D0E))), ), ], ), ), ); } if (status == 'NONE') { return Padding( padding: const EdgeInsets.symmetric(horizontal: 22), child: Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), border: Border.all(color: const Color(0xFFBFDBFE)), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.info_outline, size: 18, color: Colors.blue[700]), const SizedBox(width: 8), const Expanded( child: Text('Complete your profile and upload verification documents to get verified.', style: TextStyle(fontFamily: 'Fractul', fontSize: 13, fontWeight: FontWeight.w700, color: Color(0xFF1E40AF))), ), ], ), const SizedBox(height: 10), GestureDetector( onTap: () => context.push('/agent/edit-profile'), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( color: AppColors.accentOrange, borderRadius: BorderRadius.circular(8), ), child: const Text('Complete Profile', style: TextStyle(fontFamily: 'SourceSerif4', fontSize: 13, color: Colors.white, fontWeight: FontWeight.w600)), ), ), ], ), ), ); } return const SizedBox.shrink(); } // ── Agent profile photo as cover image ── Widget _buildCoverAndAvatar(AgentProfile agent) { final screenWidth = MediaQuery.of(context).size.width; final coverWidth = screenWidth * 0.877; // 377/430 from Figma return Center( child: Stack( children: [ // Agent photo as the cover ClipRRect( borderRadius: BorderRadius.circular(20), child: SizedBox( width: coverWidth, height: 346, child: _localAvatarFile != null ? Image.file( _localAvatarFile!, width: coverWidth, height: 346, fit: BoxFit.cover, ) : agent.avatarUrl != null && agent.avatarUrl!.isNotEmpty ? S3Image( imageUrl: agent.avatarUrl!, width: coverWidth, height: 346, fit: BoxFit.cover, ) : Container( color: const Color(0xFFC4D9D4), alignment: Alignment.center, child: Icon(Icons.person, size: 80, color: AppColors.primaryDark), ), ), ), // Loading overlay during upload if (_isUploadingAvatar) Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(20), child: Container( color: Colors.black.withValues(alpha: 0.4), child: const Center( child: CircularProgressIndicator( color: Colors.white, strokeWidth: 3, ), ), ), ), ), // Orange edit icon top-right (tabler:edit style) Positioned( top: 8, right: 6, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _isUploadingAvatar ? null : _pickAndUploadAvatar, child: Padding( padding: const EdgeInsets.all(8), child: Container( width: 28, height: 26, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(7), ), alignment: Alignment.center, child: SvgPicture.asset( 'assets/icons/edit_icon.svg', width: 20, height: 20, ), ), ), ), ), ], ), ); } // ── Available / Unavailable status toggle (matches web design) ── Widget _buildStatusSection(AgentProfile agent) { final isAvailable = agent.isAvailable; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ _buildStatusOption( label: 'Available.', isActive: isAvailable, dotColor: const Color(0xFF4CAF50), activeColor: const Color(0xFFE8F5E9), activeBorderColor: const Color(0xFF4CAF50), onTap: isAvailable ? null : () => _toggleAvailability(true), ), const SizedBox(height: 12), _buildStatusOption( label: 'Unavailable.', isActive: !isAvailable, dotColor: const Color(0xFF00293D), activeColor: const Color(0xFFE8EDF2), activeBorderColor: const Color(0xFF00293D), onTap: !isAvailable ? null : () => _toggleAvailability(false), ), ], ), ); } Widget _buildStatusOption({ required String label, required bool isActive, required Color dotColor, required Color activeColor, required Color activeBorderColor, VoidCallback? onTap, }) { return GestureDetector( onTap: _isTogglingAvailability ? null : onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), height: 50, padding: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: isActive ? activeColor : Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: isActive ? activeBorderColor : const Color(0xFFE0E0E0), width: isActive ? 1.5 : 1, ), ), child: Row( children: [ Container( width: 12, height: 12, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? dotColor : Colors.white, border: Border.all( color: isActive ? dotColor : const Color(0xFFD0D0D0), width: 1.5, ), ), ), const SizedBox(width: 10), Text( label, style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: isActive ? FontWeight.w700 : FontWeight.w500, color: AppColors.primaryDark, ), ), const Spacer(), if (isActive) Text( 'Active', style: TextStyle( fontFamily: 'Fractul', fontSize: 13, fontWeight: FontWeight.w500, color: activeBorderColor, ), ), ], ), ), ); } // ── Contact Info ── Widget _buildContactInfo(AgentDetailState state) { final email = state.contactEmail ?? ''; final phone = state.contactPhone ?? ''; return Container( margin: const EdgeInsets.symmetric(horizontal: 24), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.1), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( children: [ const Text('Email:', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(width: 16), Expanded( child: Text( email.isNotEmpty ? (_showEmail ? email : state.maskEmail(email)) : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (email.isNotEmpty) GestureDetector( onTap: () => setState(() => _showEmail = !_showEmail), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.all(4), child: Icon( _showEmail ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: AppColors.primaryDark, ), ), ), ], ), const SizedBox(height: 14), Row( children: [ const Text('Ph.No:', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(width: 16), Expanded( child: Text( phone.isNotEmpty ? (_showPhone ? phone : state.maskPhone(phone)) : '-', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (phone.isNotEmpty) GestureDetector( onTap: () => setState(() => _showPhone = !_showPhone), behavior: HitTestBehavior.opaque, child: Padding( padding: const EdgeInsets.all(4), child: Icon( _showPhone ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: AppColors.primaryDark, ), ), ), ], ), ], ), ); } // ── Name, verified badge, title, location, member since, bio ── Widget _buildProfileInfo(AgentProfile agent) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Name + Verified badge + (Verified Expert) + edit icon Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Flexible( child: Text.rich( TextSpan( children: [ TextSpan( text: '${agent.firstName} ${agent.lastName}', style: const TextStyle( fontFamily: 'Fractul', fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), ], ), overflow: TextOverflow.ellipsis, ), ), if (agent.isVerified) ...[ const Padding( padding: EdgeInsets.only(left: 6, right: 4), child: Icon(Icons.verified, size: 16, color: Color(0xFF2196F3)), ), const Text( '(Verified Expert)', style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: Color(0xFF638559), ), ), ], const SizedBox(width: 8), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => context.push('/agent/edit-profile'), child: Padding( padding: const EdgeInsets.all(4), child: SvgPicture.asset( 'assets/icons/edit_icon.svg', width: 20, height: 20, ), ), ), ], ), // Title if (agent.agentType != null) ...[ const SizedBox(height: 6), Text( agent.headline ?? agent.agentType!.name, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), ], const SizedBox(height: 8), // Location + Member Since Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (agent.location.isNotEmpty) ...[ SvgPicture.asset( 'assets/icons/location_filled_icon.svg', width: 16, height: 16, ), const SizedBox(width: 4), Text( agent.location, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(width: 20), ], SvgPicture.asset( 'assets/icons/calendar_icon.svg', width: 16, height: 16, colorFilter: const ColorFilter.mode( AppColors.primaryDark, BlendMode.srcIn, ), ), const SizedBox(width: 4), Text( agent.memberSinceText, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ], ), // Bio if (agent.description.isNotEmpty) ...[ const SizedBox(height: 16), SizedBox( width: 338, child: Text( agent.description, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.43, ), textAlign: TextAlign.center, ), ), ], ], ), ); } // ── Experience Section ── Widget _buildExperienceSection(AgentDetailState state) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text('Experience', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 16), _buildExperienceRow( state.yearsInExperienceLabel, state.yearsInExperience), const Divider(height: 24, thickness: 0.2), _buildExperienceRow( state.contractsClosedLabel, state.contractsClosed), const Divider(height: 24, thickness: 0.2), if (state.licensingAreas.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, child: Text(state.licensingAreasLabel, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), _buildTagsWrap(state.licensingAreas, sectionKey: 'licensing'), const SizedBox(height: 16), ], if (state.expertiseYears.isNotEmpty) ...[ Align( alignment: Alignment.centerLeft, child: Text(state.expertiseYearsLabel, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), _buildTagsWrap(state.expertiseYears .map((e) => e['years']!.isNotEmpty ? '${e['area']} – ${e['years']}' : e['area']!) .toList(), sectionKey: 'expertise'), ], if (state.certifications.isNotEmpty) ...[ const SizedBox(height: 16), Align( alignment: Alignment.centerLeft, child: Text(state.certificationsLabel, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), ), const SizedBox(height: 8), ...state.certifications.map((cert) => Padding( padding: const EdgeInsets.only(bottom: 12, left: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('\u2022 ', style: TextStyle( fontSize: 14, color: AppColors.primaryDark)), Expanded( child: Text(cert['name']!, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark)), ), ], ), if (cert['org']!.isNotEmpty) Padding( padding: const EdgeInsets.only(left: 16), child: Opacity( opacity: 0.5, child: Text(cert['org']!, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark)), ), ), ], ), )), ], ], ), ); } Widget _buildExperienceRow(String label, String value) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 6), Padding( padding: const EdgeInsets.only(left: 16), child: Row( children: [ const Text('\u2022 ', style: TextStyle( fontSize: 15, color: AppColors.primaryDark)), Text(value, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.primaryDark)), ], ), ), ], ); } Widget _buildTagsWrap(List tags, {required String sectionKey}) { final isExpanded = _expandedTagSections.contains(sectionKey); final displayTags = isExpanded ? tags : tags.take(6).toList(); final remaining = tags.length - 6; return Wrap( spacing: 8, runSpacing: 8, children: [ ...displayTags.map((tag) => Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.7), ), child: Text(tag, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark)), )), if (remaining > 0) GestureDetector( onTap: () { setState(() { if (isExpanded) { _expandedTagSections.remove(sectionKey); } else { _expandedTagSections.add(sectionKey); } }); }, behavior: HitTestBehavior.opaque, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.1), ), child: Text( isExpanded ? 'Show Less' : '+$remaining More', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 15, fontWeight: FontWeight.w300, color: AppColors.primaryDark)), ), ), ], ); } // ── Info Cards (Availability, Work Env, Testimonial) — matches web ── Widget _buildInfoCards(AgentDetailState state) { // Work environment — try fieldValues first, fallback to mock (same as web) String workEnv = ''; for (final fv in state.fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug.contains('work_environment') || slug.contains('preferred_environment')) { if (fv.textValue != null) workEnv = fv.textValue!; if (fv.jsonValue is List) { workEnv = (fv.jsonValue as List).map((e) => e.toString()).join(', '); } } } // Fallback mock data (same as web dashboard) if (workEnv.isEmpty) { workEnv = 'Preferred work environment includes on-site property visits, ' 'in-depth market research, client consultations, property tours, ' 'and active involvement in negotiations to ensure the best outcomes'; } // Extract first testimonial for preview String testimonialText = ''; if (state.testimonials.isNotEmpty) { final first = state.testimonials.first; testimonialText = (first['text'] ?? first['content'] ?? first['review'] ?? '') .toString(); if (testimonialText.length > 150) { testimonialText = '${testimonialText.substring(0, 150)}...'; } } return Padding( padding: const EdgeInsets.symmetric(horizontal: 36), child: Column( children: [ // Availability card (with title — matches web) _buildInfoCard( icon: Icons.access_time_filled, title: state.availabilityLabel, subtitle: state.availabilityType.isNotEmpty ? state.availabilityType : 'Not specified', items: state.availabilitySchedule.isNotEmpty ? state.availabilitySchedule : null, isEmpty: state.availabilityType.isEmpty, ), const SizedBox(height: 16), // Work environment _buildInfoCard( icon: Icons.landscape_outlined, title: state.workEnvironmentLabel, subtitle: '', description: workEnv, ), const SizedBox(height: 16), // Testimonial preview _buildInfoCard( icon: Icons.star_rounded, title: state.bestExperienceLabel, subtitle: '', description: testimonialText.isNotEmpty ? '\u201C$testimonialText\u201D' : 'No testimonials yet', isEmpty: testimonialText.isEmpty, ), ], ), ); } Widget _buildInfoCard({ required IconData icon, required String title, required String subtitle, List? items, String? description, bool isEmpty = false, }) { return Container( width: double.infinity, constraints: const BoxConstraints(minHeight: 217), padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: const Color(0xFFD9D9D9).withValues(alpha: 0.5), offset: const Offset(0, 10), blurRadius: 20, ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Icon(icon, size: 36, color: AppColors.accentOrange), if (title.isNotEmpty) ...[ const SizedBox(height: 8), Text( title, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.primaryDark, ), ), ], if (subtitle.isNotEmpty) ...[ const SizedBox(height: 2), Text( subtitle, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: isEmpty ? AppColors.primaryDark.withValues(alpha: 0.4) : AppColors.primaryDark, ), ), ], if (items != null && items.isNotEmpty) ...[ const SizedBox(height: 8), ...items.map( (item) => Padding( padding: const EdgeInsets.symmetric(vertical: 1), child: Text( item, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), ), ), ), ], if (description != null) ...[ const SizedBox(height: 8), Text( description, style: TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w600, color: isEmpty ? AppColors.primaryDark.withValues(alpha: 0.4) : AppColors.primaryDark, ), textAlign: TextAlign.center, ), ], ], ), ); } // ── Specialization Section (grey background) ── Widget _buildSpecializationSection(AgentDetailState state) { final cards = state.specializationCards; if (cards.isEmpty) return const SizedBox.shrink(); return Container( width: double.infinity, color: const Color(0xFFE6E6E6), padding: const EdgeInsets.symmetric(vertical: 24), child: Column( children: [ const Text('Specialization', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 4), const Text('Area Of Expertise and Focus', style: TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark)), const SizedBox(height: 16), ...cards.map((card) => _buildSpecCard(card)), ], ), ); } Widget _buildSpecCard(SpecializationCard card) { final displayName = AgentDetailState.titleCase(card.name); final sectionKey = 'spec_${card.slug}'; final isExpanded = _expandedTagSections.contains(sectionKey); final displayValues = isExpanded ? card.values : card.values.take(3).toList(); final hasMore = card.values.length > 3; return Container( margin: const EdgeInsets.symmetric(horizontal: 66, vertical: 8), width: 298, padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.primaryDark, width: 0.7), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.category_outlined, size: 28, color: AppColors.accentOrange), const SizedBox(height: 10), Text(displayName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w700, color: AppColors.primaryDark)), const SizedBox(height: 12), ...displayValues.map((val) => Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Text( AgentDetailState.titleCase(val), textAlign: TextAlign.center, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, height: 1.8), ), )), if (hasMore) ...[ const SizedBox(height: 8), GestureDetector( onTap: () { setState(() { if (isExpanded) { _expandedTagSections.remove(sectionKey); } else { _expandedTagSections.add(sectionKey); } }); }, behavior: HitTestBehavior.opaque, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), border: Border.all(color: AppColors.accentOrange, width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(isExpanded ? 'Show Less' : 'Show More', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 10, fontWeight: FontWeight.w700, color: AppColors.accentOrange)), const SizedBox(width: 4), Icon( isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, size: 14, color: AppColors.accentOrange), ], ), ), ), ], ], ), ); } // ── Testimonials ── Widget _buildTestimonialsSection(AgentDetailState state) { if (state.testimonials.isEmpty) return const SizedBox.shrink(); // Calculate average rating dynamically final totalRating = state.testimonials.fold( 0, (sum, t) => sum + ((t['rating'] as num?)?.toDouble() ?? 5.0), ); final avgRating = (totalRating / state.testimonials.length).toStringAsFixed(1); final count = state.testimonials.length; return Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( children: [ const Text( 'Testimonials', style: TextStyle( fontFamily: 'Fractul', fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.primaryDark, ), ), const SizedBox(height: 8), Divider( color: AppColors.primaryDark.withValues(alpha: 0.15), height: 1, ), const SizedBox(height: 8), Text( 'Clients rate our real estate services $avgRating out of 5 on average, based on $count recent client review${count != 1 ? 's' : ''}.', style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, color: AppColors.primaryDark, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), SizedBox( height: 380, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: state.testimonials.length, separatorBuilder: (_, __) => const SizedBox(width: 12), itemBuilder: (_, i) => _TestimonialCard(data: state.testimonials[i]), ), ), ], ), ); } } // ── Testimonial Card (matches Figma & web TestimonialCard) ── class _TestimonialCard extends StatelessWidget { final Map data; const _TestimonialCard({required this.data}); @override Widget build(BuildContext context) { final text = data['text'] as String? ?? ''; final authorName = data['authorName'] as String? ?? ''; final authorRole = data['authorRole'] as String? ?? ''; final rating = (data['rating'] as num?)?.toInt() ?? 5; return Container( width: 300, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: AppColors.primaryDark.withValues(alpha: 0.1), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Quote icon SvgPicture.asset( 'assets/icons/quote_icon.svg', width: 23, height: 13, ), const SizedBox(height: 12), // Title snippet (bold) Text( text.length > 35 ? '${text.substring(0, 35)} ..' : text, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 12), // Full text Expanded( child: Text( text, style: const TextStyle( fontFamily: 'SourceSerif4', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, height: 1.4, ), overflow: TextOverflow.fade, ), ), const SizedBox(height: 12), // Divider Divider( color: AppColors.primaryDark.withValues(alpha: 0.08), height: 1, ), const SizedBox(height: 12), // Star rating Row( mainAxisSize: MainAxisSize.min, children: List.generate( 5, (i) => Padding( padding: const EdgeInsets.only(right: 2), child: Icon( i < rating ? Icons.star : Icons.star_border, size: 18, color: i < rating ? const Color(0xFFFFDE21) : AppColors.primaryDark.withValues(alpha: 0.3), ), ), ), ), const SizedBox(height: 8), // Author name Text( authorName, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w400, color: AppColors.primaryDark, ), ), // Author role if (authorRole.isNotEmpty) ...[ const SizedBox(height: 2), Text( authorRole, style: const TextStyle( fontFamily: 'Fractul', fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.primaryDark, ), ), ], ], ), ); } }