feat: Enhance agent profile data model with new fields and improved data derivation, and update agent search screen UI and icon.

This commit is contained in:
pradeepkumar
2026-03-07 22:26:08 +05:30
parent ee08e86449
commit 402c1cf206
2 changed files with 86 additions and 25 deletions

View File

@@ -23,6 +23,9 @@ class AgentProfile {
final AgentUser? user; final AgentUser? user;
final List<AgentFieldValue> fieldValues; final List<AgentFieldValue> fieldValues;
final String? createdAt; final String? createdAt;
final List<String> legacySpecializations;
final List<String> languages;
final List<String> serviceAreas;
const AgentProfile({ const AgentProfile({
required this.id, required this.id,
@@ -47,6 +50,9 @@ class AgentProfile {
this.user, this.user,
this.fieldValues = const [], this.fieldValues = const [],
this.createdAt, this.createdAt,
this.legacySpecializations = const [],
this.languages = const [],
this.serviceAreas = const [],
}); });
String get fullName => '$firstName $lastName'.trim(); String get fullName => '$firstName $lastName'.trim();
@@ -69,6 +75,16 @@ class AgentProfile {
/// Get avatar URL — check agent avatar first, then user avatar /// Get avatar URL — check agent avatar first, then user avatar
String? get avatarUrl => avatar ?? user?.avatar; String? get avatarUrl => avatar ?? user?.avatar;
/// Get description — check fieldValues 'description' first, then bio (matches web)
String get description {
for (final fv in fieldValues) {
if (fv.fieldSlug == 'description' && fv.textValue != null && fv.textValue!.isNotEmpty) {
return fv.textValue!;
}
}
return bio ?? '';
}
String get location { String get location {
// Direct fields first // Direct fields first
if (city != null && state != null) return '$city, $state'; if (city != null && state != null) return '$city, $state';
@@ -89,6 +105,9 @@ class AgentProfile {
final parts = [fvCity, fvState].where((s) => s != null && s.isNotEmpty); final parts = [fvCity, fvState].where((s) => s != null && s.isNotEmpty);
if (parts.isNotEmpty) return parts.join(', '); if (parts.isNotEmpty) return parts.join(', ');
if (country != null && country!.isNotEmpty) return country!;
if (serviceAreas.isNotEmpty) return serviceAreas.first;
return ''; return '';
} }
@@ -159,6 +178,17 @@ class AgentProfile {
if (!tags.contains(display)) tags.add(display); if (!tags.contains(display)) tags.add(display);
} }
} }
// Add from legacy specializations array
for (final spec in legacySpecializations) {
if (spec.isNotEmpty && !tags.contains(spec)) tags.add(spec);
}
// Add from languages array
for (final lang in languages) {
if (lang.isNotEmpty && !tags.contains(lang)) tags.add(lang);
}
return tags; return tags;
} }
@@ -193,6 +223,18 @@ class AgentProfile {
.toList() ?? .toList() ??
const [], const [],
createdAt: json['createdAt'] as String?, createdAt: json['createdAt'] as String?,
legacySpecializations: (json['specializations'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
const [],
languages: (json['languages'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
const [],
serviceAreas: (json['serviceAreas'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
const [],
); );
} }
} }
@@ -241,11 +283,14 @@ class AgentFieldValue {
}); });
factory AgentFieldValue.fromJson(Map<String, dynamic> json) { factory AgentFieldValue.fromJson(Map<String, dynamic> json) {
// API returns field info nested: { field: { slug, name, fieldType } }
// Fallback to top-level fieldSlug/fieldName/fieldType for compatibility
final field = json['field'] as Map<String, dynamic>?;
return AgentFieldValue( return AgentFieldValue(
id: json['id'] as String? ?? '', id: json['id'] as String? ?? '',
fieldSlug: json['fieldSlug'] as String? ?? '', fieldSlug: field?['slug'] as String? ?? json['fieldSlug'] as String? ?? '',
fieldName: json['fieldName'] as String? ?? '', fieldName: field?['name'] as String? ?? json['fieldName'] as String? ?? '',
fieldType: json['fieldType'] as String? ?? '', fieldType: field?['fieldType'] as String? ?? json['fieldType'] as String? ?? '',
textValue: json['textValue'] as String?, textValue: json['textValue'] as String?,
numberValue: json['numberValue'] as num?, numberValue: json['numberValue'] as num?,
booleanValue: json['booleanValue'] as bool?, booleanValue: json['booleanValue'] as bool?,

View File

@@ -86,39 +86,49 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
Expanded( Expanded(
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white,
border: Border.all( border: Border.all(
color: AppColors.primaryDark, color: AppColors.primaryDark,
width: 0.1, width: 0.1,
), ),
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
), ),
child: TextField( child: Theme(
controller: _searchController, data: Theme.of(context).copyWith(
style: const TextStyle( inputDecorationTheme: const InputDecorationTheme(
fontFamily: 'Fractul', filled: false,
fontSize: 14, border: InputBorder.none,
fontWeight: FontWeight.w500, ),
color: AppColors.primaryDark,
), ),
decoration: InputDecoration( child: TextField(
hintText: 'Search Agents', controller: _searchController,
hintStyle: TextStyle( style: const TextStyle(
fontFamily: 'Fractul', fontFamily: 'Fractul',
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.black.withValues(alpha: 0.5), color: AppColors.primaryDark,
), ),
border: InputBorder.none, decoration: InputDecoration(
enabledBorder: InputBorder.none, hintText: 'Search Agents',
focusedBorder: InputBorder.none, hintStyle: TextStyle(
contentPadding: const EdgeInsets.symmetric( fontFamily: 'Fractul',
horizontal: 14, fontSize: 14,
vertical: 10, fontWeight: FontWeight.w500,
color: Colors.black.withValues(alpha: 0.5),
),
filled: false,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 10,
),
), ),
onSubmitted: (value) {
ref.read(searchAgentsProvider.notifier).search(value);
},
), ),
onSubmitted: (value) {
ref.read(searchAgentsProvider.notifier).search(value);
},
), ),
), ),
), ),
@@ -140,7 +150,13 @@ class _AgentSearchScreenState extends ConsumerState<AgentSearchScreen> {
), ),
borderRadius: BorderRadius.circular(7), borderRadius: BorderRadius.circular(7),
), ),
child: const Icon(Icons.search, color: Colors.white, size: 24), child: Center(
child: SvgPicture.string(
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" fill="white"/></svg>',
width: 24,
height: 24,
),
),
), ),
), ),
], ],
@@ -413,7 +429,7 @@ class _AgentCardState extends State<_AgentCard> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final agent = widget.agent; final agent = widget.agent;
final specializations = agent.specializations; final specializations = agent.specializations;
final bio = agent.bio ?? ''; final bio = agent.description;
final matchPercent = agent.averageRating != null final matchPercent = agent.averageRating != null
? '${(agent.averageRating! * 20).round()}%' ? '${(agent.averageRating! * 20).round()}%'
: null; : null;