import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:real_estate_mobile/features/agents/data/agents_repository.dart'; import 'package:real_estate_mobile/features/agents/data/models/agent_profile.dart'; import 'package:real_estate_mobile/features/agents/presentation/providers/agents_provider.dart'; class AgentDetailState { final AgentProfile? agent; final List fieldValues; final List> testimonials; final Map? connectionStatus; final bool isLoading; final String? error; const AgentDetailState({ this.agent, this.fieldValues = const [], this.testimonials = const [], this.connectionStatus, this.isLoading = true, this.error, }); AgentDetailState copyWith({ AgentProfile? agent, List? fieldValues, List>? testimonials, Map? connectionStatus, bool? isLoading, String? error, bool clearError = false, bool clearConnection = false, }) { return AgentDetailState( agent: agent ?? this.agent, fieldValues: fieldValues ?? this.fieldValues, testimonials: testimonials ?? this.testimonials, connectionStatus: clearConnection ? null : (connectionStatus ?? this.connectionStatus), isLoading: isLoading ?? this.isLoading, error: clearError ? null : (error ?? this.error), ); } // ── Mapped data helpers (matching web profileDataMapper.ts) ── /// Get field value by slug dynamic _getFieldValue(String slug) { for (final fv in fieldValues) { if (fv.fieldSlug == slug) { if (fv.jsonValue != null) return fv.jsonValue; if (fv.textValue != null) return fv.textValue; if (fv.numberValue != null) return fv.numberValue; return null; } } return null; } // ── Experience data ── String get yearsInExperience { final val = _getFieldValue('years_in_business'); if (val == null) return '-'; const mapping = { '<2': 'Less than 2 years', '2+': '2+ years', '5+': '5+ years', '10+': '10+ years', }; return mapping[val.toString()] ?? val.toString(); } String get contractsClosed { final val = _getFieldValue('contracts_completed'); if (val == null) return '-'; final str = val.toString(); // Parse leading integer from value (handles "10-20", "50+", "<3", etc.) final match = RegExp(r'\d+').firstMatch(str); if (match == null) return '-'; final num = int.tryParse(match.group(0)!) ?? -1; if (num < 0) return '-'; if (num >= 100) return '100+ Contracts'; if (num >= 50) return '50+ Contracts'; if (num >= 20) return '20+ Contracts'; if (num >= 10) return '10+ Contracts'; if (num >= 5) return '5+ Contracts'; return '$num Contract${num != 1 ? 's' : ''}'; } List get licensingAreas { final val = _getFieldValue('licensed_areas'); if (val is List) return val.map((e) => titleCase(e.toString())).toList(); return []; } List> get expertiseYears { final val = _getFieldValue('expertise_areas'); if (val is! List) return []; return val.map((item) { final str = item.toString(); final match = RegExp(r'^(.+?)\s*[-–]\s*(\d+)\s*(yrs?|years?)$', caseSensitive: false) .firstMatch(str); if (match != null) { return {'area': match.group(1)!.trim(), 'years': '${match.group(2)} yrs'}; } return {'area': str.trim(), 'years': ''}; }).toList(); } List> get certifications { final val = _getFieldValue('certification_entries'); if (val is! List) return []; return val .where((e) => e is Map && e['certification_name'] != null) .map((e) => { 'name': (e['certification_name'] ?? '').toString(), 'org': (e['issuing_organization'] ?? '').toString(), }) .toList(); } // ── Expertise tags (matching web mapFieldValuesToProfileCard) ── List get expertiseTags { const slugs = [ 'about_me_expertise', 'expertise_areas', 'specializations', 'areas_of_expertise', 'expertise', 'specialization', ]; for (final slug in slugs) { final val = _getFieldValue(slug); if (val is List && val.isNotEmpty) { return val.map((v) => titleCase(v.toString())).toList(); } } // Fallback to agent model specializations if (agent != null) { final tags = agent!.specializations; if (tags.isNotEmpty) return tags; } return []; } // ── Description / Bio (from fieldValues first, then agent model) ── String get descriptionText { // Check detail fieldValues for 'description' slug for (final fv in fieldValues) { if (fv.fieldSlug == 'description' && fv.textValue != null && fv.textValue!.isNotEmpty) { return fv.textValue!; } } // Fallback to agent model bio return agent?.bio ?? ''; } // ── Location (from fieldValues, matching web profileDataMapper) ── /// All location parts as individual items (for "+N more" display). /// Sources: detail fieldValues city/state arrays → agent model → serviceAreas List get locationParts { final parts = []; // 1. Extract from detail fieldValues (full arrays, not just first item) List? fvCities; List? fvStates; for (final fv in fieldValues) { final slug = fv.fieldSlug.toLowerCase(); if (slug == 'city' || slug == 'city_name') { fvCities = _extractAllValuesAsList(fv); } else if (slug == 'state' || slug == 'state_name') { fvStates = _extractAllValuesAsList(fv); } } if (fvCities != null) parts.addAll(fvCities); if (fvStates != null) { for (final s in fvStates) { if (!parts.any((p) => p.toLowerCase() == s.toLowerCase())) { parts.add(s); } } } // 2. Fallback: agent model direct city/state if (parts.isEmpty && agent != null) { if (agent!.city != null && agent!.city!.isNotEmpty) { parts.add(agent!.city!); } if (agent!.state != null && agent!.state!.isNotEmpty) { parts.add(agent!.state!); } } // 3. Fallback: serviceAreas if (parts.isEmpty && agent != null) { for (final area in agent!.serviceAreas) { final trimmed = area.trim(); if (trimmed.isNotEmpty) parts.add(trimmed); } } return parts; } /// Combined location text for single-line display. String get locationText { final parts = locationParts; if (parts.isEmpty) return ''; return parts.join(', '); } /// Extract all values from a field as a list of title-cased strings. static List? _extractAllValuesAsList(AgentFieldValue fv) { if (fv.jsonValue is List && (fv.jsonValue as List).isNotEmpty) { return (fv.jsonValue as List) .map((v) => titleCase(v.toString())) .toList(); } if (fv.textValue != null && fv.textValue!.isNotEmpty) { return [titleCase(fv.textValue!)]; } return null; } /// Extract all values from a field (array or string), title-cased. static String? _extractAllValues(AgentFieldValue fv) { if (fv.jsonValue is List && (fv.jsonValue as List).isNotEmpty) { return (fv.jsonValue as List) .map((v) => titleCase(v.toString())) .join(', '); } if (fv.textValue != null && fv.textValue!.isNotEmpty) { return titleCase(fv.textValue!); } return null; } // ── Contact info (masked) ── String? get contactEmail { final val = _getFieldValue('email') ?? _getFieldValue('email_address') ?? agent?.email ?? agent?.user?.email; return val?.toString(); } String? get contactPhone { final val = _getFieldValue('phone_number') ?? _getFieldValue('phone') ?? _getFieldValue('cell_number') ?? _getFieldValue('office_number') ?? agent?.phone; return val?.toString(); } String maskEmail(String email) { final parts = email.split('@'); if (parts.length != 2) return email; final prefix = parts[0].length > 2 ? parts[0].substring(0, 2) : parts[0]; return '$prefix********@${parts[1]}'; } String maskPhone(String phone) { final digits = phone.replaceAll(RegExp(r'[^\d]'), ''); if (digits.length <= 4) return '****$digits'; return '${'*' * (digits.length - 4)}${digits.substring(digits.length - 4)}'; } // ── Availability ── String get availabilityType { final val = _getFieldValue('availability'); if (val is! List || val.isEmpty) return ''; final values = val.map((e) => e.toString()).toList(); if (values.contains('full_time') || values.contains('mf_9_5') || values.contains('mf_8_6')) return 'Full-time'; if (values.contains('part_time')) return 'Part-time'; if (values.contains('flexible')) return 'Flexible'; return 'Available'; } List get availabilitySchedule { final val = _getFieldValue('availability'); if (val is! List) return []; const labels = { 'mf_9_5': 'Monday - Friday, 9 AM - 5 PM', 'mf_8_6': 'Monday - Friday, 8 AM - 6 PM', 'weekends': 'Weekends', 'evenings': 'Evenings', 'flexible': 'Flexible Schedule', 'full_time': 'Full-time', 'part_time': 'Part-time', '24_7': '24/7 Available', 'by_appointment': 'By Appointment Only', 'monday': 'Monday', 'tuesday': 'Tuesday', 'wednesday': 'Wednesday', 'thursday': 'Thursday', 'friday': 'Friday', 'saturday': 'Saturday', 'sunday': 'Sunday', }; return val.map((v) => labels[v.toString()] ?? titleCase(v.toString())).toList(); } // ── Specialization fields ── List get specializationCards { final cards = []; for (final fv in fieldValues) { final section = (fv.sectionSlug ?? '').toLowerCase(); if (!section.contains('specialization')) continue; if (fv.fieldSlug.isEmpty || fv.fieldName.isEmpty) continue; final values = []; if (fv.jsonValue is List) { for (final v in (fv.jsonValue as List)) { final s = v.toString().trim(); if (s.isNotEmpty) values.add(s); } } else if (fv.textValue != null && fv.textValue!.trim().isNotEmpty) { values.add(fv.textValue!); } if (values.isEmpty) continue; cards.add(SpecializationCard( slug: fv.fieldSlug, name: fv.fieldName, values: values, )); } return cards; } // ── Connection status helpers ── String get connectionState { if (connectionStatus == null) return 'none'; final status = connectionStatus!['status']?.toString().toUpperCase(); if (status == 'PENDING') return 'pending'; if (status == 'ACCEPTED') return 'accepted'; return 'none'; } String? get connectionRequestId => connectionStatus?['id']?.toString(); static String titleCase(String s) { return s .split('_') .map((w) => w.isNotEmpty ? '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}' : '') .join(' '); } } class SpecializationCard { final String slug; final String name; final List values; const SpecializationCard({ required this.slug, required this.name, required this.values, }); } class AgentDetailNotifier extends StateNotifier { final AgentsRepository _repo; final String agentId; AgentDetailNotifier(this._repo, this.agentId) : super(const AgentDetailState()) { _load(); } Future _load() async { try { final results = await Future.wait([ _repo.getAgentById(agentId), _repo.getFieldValues(agentId), _repo.getTestimonials(agentId), ]); state = state.copyWith( agent: results[0] as AgentProfile, fieldValues: results[1] as List, testimonials: results[2] as List>, isLoading: false, ); } catch (e) { state = state.copyWith(isLoading: false, error: e.toString()); } } Future loadConnectionStatus() async { final status = await _repo.getConnectionStatus(agentId); state = state.copyWith(connectionStatus: status); } Future connect({String? message}) async { final success = await _repo.createConnectionRequest(agentId, message: message); if (success) { state = state.copyWith( connectionStatus: {'status': 'PENDING'}, ); } return success; } Future cancelConnection() async { final requestId = state.connectionRequestId; if (requestId == null) return false; final success = await _repo.cancelConnectionRequest(requestId); if (success) { state = state.copyWith(clearConnection: true); } return success; } } final agentDetailProvider = StateNotifierProvider.autoDispose .family((ref, agentId) { final repo = ref.read(agentsRepositoryProvider); return AgentDetailNotifier(repo, agentId); });