update mobile design
This commit is contained in:
@@ -20,15 +20,19 @@ class SearchAgentsResponse {
|
||||
required this.totalPages,
|
||||
});
|
||||
|
||||
/// Parses: { data: [...], meta: { total, page, limit, totalPages } }
|
||||
factory SearchAgentsResponse.fromJson(Map<String, dynamic> json) {
|
||||
final meta = json['meta'] as Map<String, dynamic>? ?? {};
|
||||
return SearchAgentsResponse(
|
||||
data: (json['data'] as List<dynamic>)
|
||||
.map((e) => AgentProfile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
total: json['total'] as int? ?? 0,
|
||||
page: json['page'] as int? ?? 1,
|
||||
limit: json['limit'] as int? ?? 10,
|
||||
totalPages: json['totalPages'] as int? ?? 1,
|
||||
data: (json['data'] as List<dynamic>?)
|
||||
?.map(
|
||||
(e) => AgentProfile.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
total: meta['total'] as int? ?? 0,
|
||||
page: meta['page'] as int? ?? 1,
|
||||
limit: meta['limit'] as int? ?? 10,
|
||||
totalPages: meta['totalPages'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +40,8 @@ class SearchAgentsResponse {
|
||||
class AgentsRepository {
|
||||
final Dio _dio = ApiClient.instance.dio;
|
||||
|
||||
/// Search agents: GET /agents
|
||||
/// Response: { success, data: { data: [...], meta: {...} } }
|
||||
Future<SearchAgentsResponse> searchAgents({
|
||||
String? agentTypeId,
|
||||
int page = 1,
|
||||
@@ -58,8 +64,10 @@ class AgentsRepository {
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final data = response.data['data'] as Map<String, dynamic>;
|
||||
return SearchAgentsResponse.fromJson(data);
|
||||
// Backend wraps: { success: true, data: { data: [...], meta: {...} } }
|
||||
final responseData = response.data;
|
||||
final payload = responseData['data'] as Map<String, dynamic>;
|
||||
return SearchAgentsResponse.fromJson(payload);
|
||||
} on DioException catch (e) {
|
||||
if (e.error is ApiException) {
|
||||
throw e.error as ApiException;
|
||||
@@ -71,9 +79,13 @@ class AgentsRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get agent types: GET /agent-types
|
||||
/// Response: { success, data: [...] }
|
||||
Future<List<AgentType>> getAgentTypes() async {
|
||||
try {
|
||||
final response = await _dio.get(ApiConstants.agentTypes);
|
||||
|
||||
// Backend wraps: { success: true, data: [...] }
|
||||
final data = response.data['data'] as List<dynamic>;
|
||||
return data
|
||||
.map((e) => AgentType.fromJson(e as Map<String, dynamic>))
|
||||
|
||||
@@ -2,164 +2,183 @@ import 'package:real_estate_mobile/features/agents/data/models/agent_type.dart';
|
||||
|
||||
class AgentProfile {
|
||||
final String id;
|
||||
final String? userId;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String slug;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? headline;
|
||||
final String? bio;
|
||||
final String? avatar;
|
||||
final String? licenseNumber;
|
||||
final int? experience;
|
||||
final List<String> specializations;
|
||||
final List<String> languages;
|
||||
final List<String> serviceAreas;
|
||||
final String? companyName;
|
||||
final String? phone;
|
||||
final String? website;
|
||||
final String? city;
|
||||
final String? state;
|
||||
final String? country;
|
||||
final double? rating;
|
||||
final int reviewCount;
|
||||
final double? averageRating;
|
||||
final int totalReviews;
|
||||
final bool isVerified;
|
||||
final bool isFeatured;
|
||||
final bool isAvailable;
|
||||
final String? agentTypeId;
|
||||
final bool isActive;
|
||||
final AgentType? agentType;
|
||||
final AgentUser? user;
|
||||
final List<AgentFieldValue> fieldValues;
|
||||
|
||||
const AgentProfile({
|
||||
required this.id,
|
||||
this.userId,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
required this.slug,
|
||||
this.email,
|
||||
this.phone,
|
||||
this.headline,
|
||||
this.bio,
|
||||
this.avatar,
|
||||
this.licenseNumber,
|
||||
this.experience,
|
||||
this.specializations = const [],
|
||||
this.languages = const [],
|
||||
this.serviceAreas = const [],
|
||||
this.companyName,
|
||||
this.phone,
|
||||
this.website,
|
||||
this.city,
|
||||
this.state,
|
||||
this.country,
|
||||
this.rating,
|
||||
this.reviewCount = 0,
|
||||
this.averageRating,
|
||||
this.totalReviews = 0,
|
||||
this.isVerified = false,
|
||||
this.isFeatured = false,
|
||||
this.isAvailable = false,
|
||||
this.agentTypeId,
|
||||
this.isActive = true,
|
||||
this.agentType,
|
||||
this.user,
|
||||
this.fieldValues = const [],
|
||||
});
|
||||
|
||||
String get fullName => '$firstName $lastName'.trim();
|
||||
|
||||
/// Get avatar URL — check agent avatar first, then user avatar
|
||||
String? get avatarUrl => avatar ?? user?.avatar;
|
||||
|
||||
String get location {
|
||||
if (city != null && state != null) return '$city, $state';
|
||||
if (city != null) return city!;
|
||||
if (state != null) return state!;
|
||||
if (serviceAreas.isNotEmpty) return serviceAreas.first;
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Get experience from field values
|
||||
String get experienceText {
|
||||
if (experience == null) return '';
|
||||
return '$experience+ years in the real estate industry.';
|
||||
for (final fv in fieldValues) {
|
||||
if (fv.fieldSlug.contains('experience') ||
|
||||
fv.fieldName.toLowerCase().contains('experience')) {
|
||||
if (fv.textValue != null) return fv.textValue!;
|
||||
if (fv.numberValue != null) {
|
||||
return '${fv.numberValue!.toInt()}+ years in the real estate industry.';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Get specializations from field values
|
||||
List<String> get specializations {
|
||||
for (final fv in fieldValues) {
|
||||
final slug = fv.fieldSlug.toLowerCase();
|
||||
if (slug.contains('specialization') ||
|
||||
slug.contains('property_type') ||
|
||||
slug.contains('loan_type')) {
|
||||
if (fv.jsonValue is List) {
|
||||
return (fv.jsonValue as List).map((e) => e.toString()).toList();
|
||||
}
|
||||
if (fv.textValue != null) {
|
||||
return fv.textValue!.split(',').map((s) => s.trim()).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
factory AgentProfile.fromJson(Map<String, dynamic> json) {
|
||||
return AgentProfile(
|
||||
id: json['id'] as String,
|
||||
userId: json['userId'] as String?,
|
||||
firstName: json['firstName'] as String? ?? '',
|
||||
lastName: json['lastName'] as String? ?? '',
|
||||
slug: json['slug'] as String? ?? '',
|
||||
email: json['email'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
headline: json['headline'] as String?,
|
||||
bio: json['bio'] as String?,
|
||||
avatar: json['avatar'] as String?,
|
||||
licenseNumber: json['licenseNumber'] as String?,
|
||||
experience: json['experience'] as int?,
|
||||
specializations: _parseStringList(json['specializations']),
|
||||
languages: _parseStringList(json['languages']),
|
||||
serviceAreas: _parseStringList(json['serviceAreas']),
|
||||
companyName: json['companyName'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
website: json['website'] as String?,
|
||||
city: json['city'] as String?,
|
||||
state: json['state'] as String?,
|
||||
country: json['country'] as String?,
|
||||
rating: (json['rating'] as num?)?.toDouble(),
|
||||
reviewCount: json['reviewCount'] as int? ?? 0,
|
||||
averageRating: (json['averageRating'] as num?)?.toDouble(),
|
||||
totalReviews: json['totalReviews'] as int? ?? 0,
|
||||
isVerified: json['isVerified'] as bool? ?? false,
|
||||
isFeatured: json['isFeatured'] as bool? ?? false,
|
||||
isAvailable: json['isAvailable'] as bool? ?? false,
|
||||
agentTypeId: json['agentTypeId'] as String?,
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
agentType: json['agentType'] != null
|
||||
? AgentType.fromJson(json['agentType'] as Map<String, dynamic>)
|
||||
: null,
|
||||
user: json['user'] != null
|
||||
? AgentUser.fromJson(json['user'] as Map<String, dynamic>)
|
||||
: null,
|
||||
fieldValues: (json['fieldValues'] as List<dynamic>?)
|
||||
?.map((e) => AgentFieldValue.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> _parseStringList(dynamic value) {
|
||||
if (value == null) return const [];
|
||||
if (value is List) return value.map((e) => e.toString()).toList();
|
||||
return const [];
|
||||
class AgentUser {
|
||||
final String id;
|
||||
final String email;
|
||||
final String? avatar;
|
||||
|
||||
const AgentUser({
|
||||
required this.id,
|
||||
required this.email,
|
||||
this.avatar,
|
||||
});
|
||||
|
||||
factory AgentUser.fromJson(Map<String, dynamic> json) {
|
||||
return AgentUser(
|
||||
id: json['id'] as String,
|
||||
email: json['email'] as String? ?? '',
|
||||
avatar: json['avatar'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AgentFieldValue {
|
||||
final String id;
|
||||
final String fieldId;
|
||||
final String fieldSlug;
|
||||
final String fieldName;
|
||||
final String fieldType;
|
||||
final String? textValue;
|
||||
final num? numberValue;
|
||||
final bool? booleanValue;
|
||||
final dynamic jsonValue;
|
||||
final String? dateValue;
|
||||
final AgentField field;
|
||||
|
||||
const AgentFieldValue({
|
||||
required this.id,
|
||||
required this.fieldId,
|
||||
required this.fieldSlug,
|
||||
required this.fieldName,
|
||||
required this.fieldType,
|
||||
this.textValue,
|
||||
this.numberValue,
|
||||
this.booleanValue,
|
||||
this.jsonValue,
|
||||
this.dateValue,
|
||||
required this.field,
|
||||
});
|
||||
|
||||
factory AgentFieldValue.fromJson(Map<String, dynamic> json) {
|
||||
return AgentFieldValue(
|
||||
id: json['id'] as String,
|
||||
fieldId: json['fieldId'] as String,
|
||||
id: json['id'] as String? ?? '',
|
||||
fieldSlug: json['fieldSlug'] as String? ?? '',
|
||||
fieldName: json['fieldName'] as String? ?? '',
|
||||
fieldType: json['fieldType'] as String? ?? '',
|
||||
textValue: json['textValue'] as String?,
|
||||
numberValue: json['numberValue'] as num?,
|
||||
booleanValue: json['booleanValue'] as bool?,
|
||||
jsonValue: json['jsonValue'],
|
||||
dateValue: json['dateValue'] as String?,
|
||||
field: AgentField.fromJson(json['field'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AgentField {
|
||||
final String slug;
|
||||
final String name;
|
||||
final String fieldType;
|
||||
|
||||
const AgentField({
|
||||
required this.slug,
|
||||
required this.name,
|
||||
required this.fieldType,
|
||||
});
|
||||
|
||||
factory AgentField.fromJson(Map<String, dynamic> json) {
|
||||
return AgentField(
|
||||
slug: json['slug'] as String,
|
||||
name: json['name'] as String,
|
||||
fieldType: json['fieldType'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
class AgentType {
|
||||
final String id;
|
||||
final String name;
|
||||
final String slug;
|
||||
final String? description;
|
||||
final bool isActive;
|
||||
final int sortOrder;
|
||||
final int agentCount;
|
||||
|
||||
const AgentType({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.slug,
|
||||
this.description,
|
||||
this.isActive = true,
|
||||
this.sortOrder = 0,
|
||||
this.agentCount = 0,
|
||||
});
|
||||
|
||||
factory AgentType.fromJson(Map<String, dynamic> json) {
|
||||
final count = json['_count'] as Map<String, dynamic>?;
|
||||
return AgentType(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
slug: json['slug'] as String,
|
||||
description: json['description'] as String?,
|
||||
isActive: json['isActive'] as bool? ?? true,
|
||||
sortOrder: json['sortOrder'] as int? ?? 0,
|
||||
agentCount: count?['agents'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ final agentsRepositoryProvider = Provider<AgentsRepository>((ref) {
|
||||
return AgentsRepository();
|
||||
});
|
||||
|
||||
// Agent types provider
|
||||
final agentTypesProvider = FutureProvider<List<AgentType>>((ref) async {
|
||||
final repository = ref.watch(agentsRepositoryProvider);
|
||||
return repository.getAgentTypes();
|
||||
});
|
||||
|
||||
// Top professionals state
|
||||
class TopProfessionalsState {
|
||||
final List<AgentProfile> agents;
|
||||
@@ -70,16 +64,30 @@ class TopProfessionalsNotifier extends StateNotifier<TopProfessionalsState> {
|
||||
final agentTypes = await _repository.getAgentTypes();
|
||||
state = state.copyWith(agentTypes: agentTypes);
|
||||
|
||||
// Find agent and lender type IDs
|
||||
// Find agent and lender type IDs by name
|
||||
String? agentTypeId;
|
||||
String? lenderTypeId;
|
||||
|
||||
for (final type in agentTypes) {
|
||||
final slug = type.slug.toLowerCase();
|
||||
if (slug.contains('agent')) {
|
||||
agentTypeId = type.id;
|
||||
} else if (slug.contains('lender')) {
|
||||
final name = type.name.toLowerCase();
|
||||
if (name.contains('lender') || name.contains('lending')) {
|
||||
lenderTypeId = type.id;
|
||||
} else if (name.contains('agent') ||
|
||||
name.contains('realtor') ||
|
||||
name.contains('broker')) {
|
||||
agentTypeId = type.id;
|
||||
}
|
||||
}
|
||||
|
||||
// If no specific agent type found, use first type as agents
|
||||
if (agentTypeId == null && agentTypes.isNotEmpty) {
|
||||
agentTypeId = agentTypes.first.id;
|
||||
// Look for lender in remaining
|
||||
for (final type in agentTypes) {
|
||||
if (type.id != agentTypeId) {
|
||||
lenderTypeId = type.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,23 +99,24 @@ class TopProfessionalsNotifier extends StateNotifier<TopProfessionalsState> {
|
||||
sortBy: 'averageRating',
|
||||
sortOrder: 'desc',
|
||||
),
|
||||
_repository.searchAgents(
|
||||
agentTypeId: lenderTypeId,
|
||||
limit: 10,
|
||||
sortBy: 'averageRating',
|
||||
sortOrder: 'desc',
|
||||
),
|
||||
if (lenderTypeId != null)
|
||||
_repository.searchAgents(
|
||||
agentTypeId: lenderTypeId,
|
||||
limit: 10,
|
||||
sortBy: 'averageRating',
|
||||
sortOrder: 'desc',
|
||||
),
|
||||
]);
|
||||
|
||||
state = state.copyWith(
|
||||
agents: results[0].data,
|
||||
lenders: results[1].data,
|
||||
lenders: results.length > 1 ? results[1].data : [],
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load professionals',
|
||||
error: e.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user