/opt/canhelp/apps/mobile/lib/screens/specialists
NameSizeModeActions
specialists_screen.dart596870644editdlrm
specialist_profile_screen.dart836970644editdlrm
Edit: /opt/canhelp/apps/mobile/lib/screens/specialists/specialists_screen.dart (59687B)
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../../models/category.dart'; import '../../models/location.dart'; import '../../models/user.dart'; import '../../providers/favorites_provider.dart'; import '../../providers/locale_provider.dart'; import '../../providers/specialists_provider.dart'; import '../../services/api_client.dart'; import '../../theme/app_theme.dart'; import '../../widgets/plan_badge.dart'; class SpecialistsScreen extends StatefulWidget { final String? initialCategory; const SpecialistsScreen({super.key, this.initialCategory}); @override State createState() => _SpecialistsScreenState(); } class _SpecialistsScreenState extends State { final _searchCtrl = TextEditingController(); String? _selectedCategory; String? _selectedLocation; List _selectedLanguages = []; final _scrollCtrl = ScrollController(); List _categories = []; List _locationTree = []; String? _lastLocale; static const _fallbackSlugs = [ 'cleaning', 'plumbing', 'electrical', 'moving', 'tutoring', 'beauty', 'it_support', 'repairs', 'other', ]; @override void initState() { super.initState(); _selectedCategory = widget.initialCategory; WidgetsBinding.instance.addPostFrameCallback((_) { _reload(); _loadFilterData(); }); _scrollCtrl.addListener(_onScroll); } Future _loadFilterData() async { final api = ApiClient(); await Future.wait([ api .getCategories() .then((cats) { if (mounted && cats.isNotEmpty) setState(() => _categories = cats); }) .catchError((_) {}), api .getLocations() .then((locs) { if (mounted && locs.isNotEmpty) { setState(() => _locationTree = locs); } }) .catchError((_) {}), ]); } void _onScroll() { if (_scrollCtrl.position.pixels >= _scrollCtrl.position.maxScrollExtent - 200) { context.read().loadMore(); } } void _reload() { final locale = context.read().locale; context.read().search( query: _searchCtrl.text.trim().isEmpty ? null : _searchCtrl.text.trim(), category: _selectedCategory, location: _selectedLocation, languages: _selectedLanguages.isNotEmpty ? _selectedLanguages : null, locale: locale, reset: true, ); } @override void didChangeDependencies() { super.didChangeDependencies(); final locale = context.watch().locale; if (_lastLocale != locale) { _lastLocale = locale; WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _reload(); }); } } @override void dispose() { _searchCtrl.dispose(); _scrollCtrl.dispose(); super.dispose(); } int get _activeFilterCount => (_selectedCategory != null ? 1 : 0) + (_selectedLocation != null ? 1 : 0) + (_selectedLanguages.isNotEmpty ? 1 : 0); void _showFilterSheet() { final locale = context.read(); String? tempCategory = _selectedCategory; String? tempLocation = _selectedLocation; List tempLanguages = List.from(_selectedLanguages); final categoryItems = _categories.isNotEmpty ? _categories : _fallbackSlugs .map( (slug) => Category( id: slug, slug: slug, icon: '', names: {'en': locale.t('category.$slug')}, ), ) .toList(); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => StatefulBuilder( builder: (ctx, setSheetState) => DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.4, maxChildSize: 0.95, expand: false, builder: (ctx, scrollController) => Container( decoration: const BoxDecoration( color: AppTheme.surfaceVariant, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( children: [ // ── Header ── Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical( top: Radius.circular(20), ), boxShadow: [ BoxShadow( color: AppTheme.shadowColorLight, blurRadius: 4, offset: Offset(0, 2), ), ], ), padding: const EdgeInsets.fromLTRB(20, 12, 20, 16), child: Column( children: [ Center( child: Container( width: 36, height: 4, decoration: BoxDecoration( color: AppTheme.border, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 14), Row( children: [ Text( locale.t('tasks.filters'), style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), const Spacer(), GestureDetector( onTap: () { setState(() { _selectedCategory = null; _selectedLocation = null; _selectedLanguages = []; }); Navigator.of(ctx).pop(); _reload(); }, child: Text( locale.t('tasks.resetFilters'), style: const TextStyle( fontSize: 14, color: AppTheme.primary, fontWeight: FontWeight.w500, ), ), ), ], ), ], ), ), // ── Scrollable content ── Expanded( child: ListView( controller: scrollController, padding: EdgeInsets.only( left: 20, right: 20, top: 20, bottom: MediaQuery.of(ctx).viewInsets.bottom + 100, ), children: [ // Category _SectionLabel(locale.t('tasks.category')), const SizedBox(height: 8), _DropdownPicker( value: tempCategory, placeholder: locale.t('tasks.allCategories2'), items: [ _PickerItem( label: locale.t('tasks.allCategories2'), value: null, isParent: false, ), ...categoryItems.map( (cat) => _PickerItem( label: cat.nameFor(locale.locale), value: cat.slug, icon: cat.uiIcon.isNotEmpty ? cat.uiIcon : null, isParent: true, children: cat.children .map( (sub) => _PickerItem( label: sub.nameFor(locale.locale), value: sub.slug, isParent: false, ), ) .toList(), ), ), ], onChanged: (val) => setSheetState(() => tempCategory = val), ), if (_locationTree.isNotEmpty) ...[ const SizedBox(height: 20), // Location _SectionLabel(locale.t('tasks.location')), const SizedBox(height: 8), _DropdownPicker( value: tempLocation, placeholder: locale.t('tasks.anyLocation'), items: [ _PickerItem( label: locale.t('tasks.anyLocation'), value: null, isParent: false, ), ..._locationTree.map( (city) => _PickerItem( label: city.nameFor(locale.locale), value: city.slug, isParent: true, children: [ _PickerItem( label: '${locale.t('tasks.entireCity')} — ${city.nameFor(locale.locale)}', value: city.slug, isParent: false, ), ...city.children.map( (d) => _PickerItem( label: d.nameFor(locale.locale), value: d.slug, isParent: false, ), ), ], ), ), ], onChanged: (val) => setSheetState(() => tempLocation = val), ), ], const SizedBox(height: 20), // Language _SectionLabel(locale.t('profile.languages')), const SizedBox(height: 8), ...[ ('el', 'Ελληνικά'), ('en', 'English'), ('ru', 'Русский'), ('uk', 'Українська'), ('de', 'Deutsch'), ('fr', 'Français'), ('it', 'Italiano'), ('es', 'Español'), ('ar', 'العربية'), ('zh', '中文'), ].map((lang) { final isSelected = tempLanguages.contains(lang.$1); return InkWell( onTap: () => setSheetState(() { if (isSelected) { tempLanguages = tempLanguages .where((l) => l != lang.$1) .toList(); } else { tempLanguages = [...tempLanguages, lang.$1]; } }), borderRadius: BorderRadius.circular(8), child: Padding( padding: const EdgeInsets.symmetric( vertical: 6, horizontal: 4, ), child: Row( children: [ AnimatedContainer( duration: const Duration(milliseconds: 150), width: 20, height: 20, decoration: BoxDecoration( color: isSelected ? AppTheme.primary : Colors.transparent, border: Border.all( color: isSelected ? AppTheme.primary : AppTheme.border, width: 1.5, ), borderRadius: BorderRadius.circular(5), ), child: isSelected ? const Icon( Icons.check, size: 13, color: Colors.white, ) : null, ), const SizedBox(width: 10), Text( lang.$2, style: TextStyle( fontSize: 14, color: isSelected ? AppTheme.primary : AppTheme.textPrimary, fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400, ), ), ], ), ), ); }), const SizedBox(height: 32), ], ), ), // ── Apply button ── Container( color: Colors.white, padding: EdgeInsets.fromLTRB( 20, 12, 20, MediaQuery.of(ctx).padding.bottom + 12, ), child: SizedBox( height: 50, child: ElevatedButton( onPressed: () { setState(() { _selectedCategory = tempCategory; _selectedLocation = tempLocation; _selectedLanguages = tempLanguages; }); Navigator.of(ctx).pop(); _reload(); }, style: ElevatedButton.styleFrom( backgroundColor: AppTheme.primary, foregroundColor: Colors.white, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( AppTheme.cardRadius, ), ), ), child: Text( locale.t('tasks.applyFilters'), style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), ), ), ), ], ), ), ), ), ); } String _categoryLabel(String? slug, String locale) { if (slug == null) return ''; for (final cat in _categories) { if (cat.slug == slug) return cat.nameFor(locale); for (final sub in cat.children) { if (sub.slug == slug) return sub.nameFor(locale); } } return slug; } String _locationLabel(String slug, String locale) { for (final city in _locationTree) { if (city.slug == slug) return city.nameFor(locale); for (final d in city.children) { if (d.slug == slug) return d.nameFor(locale); } } return slug; } @override Widget build(BuildContext context) { final locale = context.watch(); final specialists = context.watch(); return Scaffold( backgroundColor: AppTheme.background, appBar: AppBar( backgroundColor: AppTheme.background, elevation: 0, scrolledUnderElevation: 0, foregroundColor: AppTheme.textPrimary, leading: IconButton( icon: const Icon( Icons.arrow_back_ios_new_rounded, size: 20, color: AppTheme.textPrimary, ), onPressed: () => context.canPop() ? context.pop() : context.go('/'), ), title: Text( locale.t('nav.specialists'), style: const TextStyle( fontWeight: FontWeight.w700, fontSize: 18, color: AppTheme.textPrimary, ), ), actions: [ Padding( padding: const EdgeInsets.only(right: 12), child: _FilterPill( activeCount: _activeFilterCount, onTap: _showFilterSheet, ), ), ], bottom: PreferredSize( preferredSize: const Size.fromHeight(58), child: Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 10), child: TextField( controller: _searchCtrl, onChanged: (_) => setState(() {}), onSubmitted: (_) => _reload(), style: const TextStyle(fontSize: 14, color: AppTheme.textPrimary), decoration: InputDecoration( hintText: locale.t('search.placeholder'), hintStyle: const TextStyle( color: AppTheme.textSecondary, fontSize: 14, ), prefixIcon: const Icon( Icons.search, size: 20, color: AppTheme.textSecondary, ), suffixIcon: _searchCtrl.text.isNotEmpty ? IconButton( icon: const Icon( Icons.clear, size: 18, color: AppTheme.textSecondary, ), onPressed: () { _searchCtrl.clear(); _reload(); setState(() {}); }, ) : null, filled: true, fillColor: Colors.white, contentPadding: const EdgeInsets.symmetric(vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.inputRadius), borderSide: const BorderSide(color: AppTheme.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.inputRadius), borderSide: const BorderSide(color: AppTheme.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.inputRadius), borderSide: const BorderSide( color: AppTheme.primary, width: 2, ), ), ), ), ), ), ), body: RefreshIndicator( color: AppTheme.primary, onRefresh: () async => _reload(), child: CustomScrollView( controller: _scrollCtrl, slivers: [ // Active filter chips if (_activeFilterCount > 0) SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 0), child: Wrap( spacing: 6, runSpacing: 6, children: [ if (_selectedCategory != null) _ActiveFilterChip( label: _categoryLabel( _selectedCategory, locale.locale, ), onRemove: () { setState(() => _selectedCategory = null); _reload(); }, ), if (_selectedLocation != null) _ActiveFilterChip( label: _locationLabel( _selectedLocation!, locale.locale, ), icon: Icons.location_on_outlined, onRemove: () { setState(() => _selectedLocation = null); _reload(); }, ), ], ), ), ), const SliverToBoxAdapter(child: SizedBox(height: 8)), // Main content if (specialists.isLoading && specialists.specialists.isEmpty) const SliverFillRemaining( child: Center( child: CircularProgressIndicator(color: AppTheme.primary), ), ) else if (specialists.error != null && specialists.specialists.isEmpty) SliverFillRemaining( child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon( Icons.wifi_off_rounded, size: 48, color: AppTheme.textSecondary, ), const SizedBox(height: 16), Text( specialists.error!, textAlign: TextAlign.center, style: const TextStyle( fontSize: 14, color: AppTheme.textSecondary, ), ), const SizedBox(height: 20), TextButton( onPressed: _reload, child: Text( context.read().t('common.retry'), ), ), ], ), ), ), ) else if (specialists.specialists.isEmpty) SliverFillRemaining( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 80, height: 80, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: AppTheme.shadowColor, blurRadius: 16, offset: Offset(0, 4), ), ], ), child: const Icon( Icons.person_search_outlined, size: 36, color: AppTheme.primary, ), ), const SizedBox(height: 16), Text( locale.t('specialists.empty'), style: const TextStyle( fontSize: 15, color: AppTheme.textSecondary, ), textAlign: TextAlign.center, ), ], ), ), ) else SliverPadding( padding: const EdgeInsets.fromLTRB(12, 2, 12, 40), sliver: SliverList( delegate: SliverChildBuilderDelegate( (_, i) { if (i == specialists.specialists.length) { return const Padding( padding: EdgeInsets.all(20), child: Center( child: CircularProgressIndicator( color: AppTheme.primary, ), ), ); } return _SpecialistCard(user: specialists.specialists[i]); }, childCount: specialists.specialists.length + (specialists.hasMore ? 1 : 0), ), ), ), ], ), ), ); } } // ── Filter pill ──────────────────────────────────────────────────────────────── class _FilterPill extends StatelessWidget { final int activeCount; final VoidCallback onTap; const _FilterPill({required this.activeCount, required this.onTap}); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: activeCount > 0 ? AppTheme.primary : Colors.white, borderRadius: BorderRadius.circular(AppTheme.chipRadius), border: Border.all( color: activeCount > 0 ? AppTheme.primary : AppTheme.border, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.tune_rounded, size: 16, color: activeCount > 0 ? Colors.white : const Color(0xFF475569), ), if (activeCount > 0) ...[ const SizedBox(width: 4), Container( width: 16, height: 16, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), child: Center( child: Text( '$activeCount', style: const TextStyle( fontSize: 10, color: AppTheme.primary, fontWeight: FontWeight.w700, ), ), ), ), ], ], ), ), ); } } // ── Active filter chip ───────────────────────────────────────────────────────── class _ActiveFilterChip extends StatelessWidget { final String label; final IconData? icon; final VoidCallback onRemove; const _ActiveFilterChip({ required this.label, this.icon, required this.onRemove, }); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), decoration: BoxDecoration( color: AppTheme.primaryContainer, borderRadius: BorderRadius.circular(AppTheme.chipRadius), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 13, color: AppTheme.primary), const SizedBox(width: 3), ], Text( label, style: const TextStyle( fontSize: 12, color: AppTheme.primary, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 4), GestureDetector( onTap: onRemove, child: const Icon(Icons.close, size: 13, color: AppTheme.primary), ), ], ), ); } } // ── Section label ────────────────────────────────────────────────────────────── class _SectionLabel extends StatelessWidget { final String text; const _SectionLabel(this.text); @override Widget build(BuildContext context) { return Text( text, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppTheme.textSecondary, letterSpacing: 0.4, ), ); } } // ── Dropdown picker ──────────────────────────────────────────────────────────── class _PickerItem { final String label; final String? value; final bool isParent; final String? icon; final List<_PickerItem> children; const _PickerItem({ required this.label, required this.value, required this.isParent, this.icon, this.children = const [], }); } class _DropdownPicker extends StatefulWidget { final String? value; final String placeholder; final List<_PickerItem> items; final ValueChanged onChanged; const _DropdownPicker({ required this.value, required this.placeholder, required this.items, required this.onChanged, }); @override State<_DropdownPicker> createState() => _DropdownPickerState(); } class _DropdownPickerState extends State<_DropdownPicker> { bool _open = false; String? _expandedParent; String _labelFor(String? val) { if (val == null) return widget.placeholder; for (final item in widget.items) { if (item.value == val) return item.label; for (final child in item.children) { if (child.value == val) return child.label; } } return widget.placeholder; } @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ GestureDetector( onTap: () => setState(() { _open = !_open; if (!_open) _expandedParent = null; }), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: _open ? const BorderRadius.vertical(top: Radius.circular(12)) : BorderRadius.circular(AppTheme.inputRadius), border: Border.all( color: _open ? AppTheme.primary : AppTheme.border, ), ), child: Row( children: [ Expanded( child: Text( _labelFor(widget.value), style: TextStyle( fontSize: 14, color: widget.value == null ? AppTheme.textHint : AppTheme.textPrimary, fontWeight: widget.value == null ? FontWeight.w400 : FontWeight.w500, ), ), ), Icon( _open ? Icons.keyboard_arrow_up_rounded : Icons.keyboard_arrow_down_rounded, size: 20, color: AppTheme.textHint, ), ], ), ), ), if (_open) Container( constraints: const BoxConstraints(maxHeight: 240), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)), border: Border( left: BorderSide(color: AppTheme.primary), right: BorderSide(color: AppTheme.primary), bottom: BorderSide(color: AppTheme.primary), ), ), child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Divider(height: 1, color: AppTheme.border), ...widget.items.map((item) { if (!item.isParent || item.children.isEmpty) { return _PickerRow( label: item.label, icon: item.icon, selected: widget.value == item.value, indent: false, onTap: () { widget.onChanged(item.value); setState(() { _open = false; _expandedParent = null; }); }, ); } final expanded = _expandedParent == item.value; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _PickerRow( label: item.label, icon: item.icon, selected: false, indent: false, trailing: Icon( expanded ? Icons.expand_less_rounded : Icons.chevron_right_rounded, size: 18, color: AppTheme.textHint, ), onTap: () => setState(() { _expandedParent = expanded ? null : item.value; }), ), if (expanded) ...item.children.map( (child) => _PickerRow( label: child.label, selected: widget.value == child.value, indent: true, onTap: () { widget.onChanged(child.value); setState(() { _open = false; _expandedParent = null; }); }, ), ), ], ); }), ], ), ), ), ], ); } } class _PickerRow extends StatelessWidget { final String label; final String? icon; final bool selected; final bool indent; final Widget? trailing; final VoidCallback onTap; const _PickerRow({ required this.label, this.icon, required this.selected, required this.indent, this.trailing, required this.onTap, }); @override Widget build(BuildContext context) { return Material( color: Colors.white, child: InkWell( onTap: onTap, child: Padding( padding: EdgeInsets.only( left: indent ? 28 : 14, right: 14, top: 10, bottom: 10, ), child: Row( children: [ if (icon != null && icon!.isNotEmpty) ...[ Text(icon!, style: const TextStyle(fontSize: 16)), const SizedBox(width: 8), ], Expanded( child: Text( label, style: TextStyle( fontSize: 13, color: selected ? AppTheme.primary : const Color(0xFF1E293B), fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), ), ), if (selected) const Icon( Icons.check_rounded, size: 16, color: AppTheme.primary, ) else if (trailing != null) trailing!, ], ), ), ), ); } } // ── Specialist card ──────────────────────────────────────────────────────────── class _SpecialistCard extends StatelessWidget { final User user; const _SpecialistCard({required this.user}); @override Widget build(BuildContext context) { final favorites = context.watch(); final isFav = favorites.isFavorite(user.id); final initials = '${user.firstName.isNotEmpty ? user.firstName[0] : ''}${user.lastName.isNotEmpty ? user.lastName[0] : ''}' .toUpperCase(); return GestureDetector( onTap: () => context.push('/specialists/${user.id}'), child: Container( margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(AppTheme.cardRadius), boxShadow: const [ BoxShadow( color: AppTheme.shadowColor, blurRadius: 8, offset: Offset(0, 2), ), ], ), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ── Header row: avatar + name + rating + fav ── Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Avatar Stack( children: [ CircleAvatar( radius: 26, backgroundImage: user.avatar != null ? NetworkImage(user.avatar!) : null, backgroundColor: AppTheme.primaryContainer, child: user.avatar == null ? Text( initials.isNotEmpty ? initials : '?', style: const TextStyle( color: AppTheme.primary, fontWeight: FontWeight.bold, fontSize: 15, ), ) : null, ), if (user.isOnline) Positioned( bottom: 0, right: 0, child: Container( width: 12, height: 12, decoration: BoxDecoration( color: AppTheme.emerald, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 2), ), ), ), ], ), const SizedBox(width: 12), // Name + rating Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( user.fullName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 6), PlanBadge(tier: user.planTier, small: true), ], ), const SizedBox(height: 4), Row( children: [ if (user.rating > 0) ...[ const Icon( Icons.star_rounded, size: 14, color: AppTheme.amber, ), const SizedBox(width: 3), Text( user.rating.toStringAsFixed(1), style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), if (user.reviewCount > 0) ...[ const SizedBox(width: 3), Text( '(${user.reviewCount})', style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], const SizedBox(width: 8), ], if (user.activeStatuses.contains('can_help_now')) Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFFF0FDF4), border: Border.all( color: const Color(0xFFBBF7D0), ), borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: AppTheme.success, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( context.watch().t( 'status.canHelpNow', ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF15803D), ), ), ], ), ) else if (user.todayStatus == 'available') Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2, ), decoration: BoxDecoration( color: AppTheme.successLight, borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: AppTheme.success, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( context.watch().t( 'schedule.available', ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF15803D), ), ), ], ), ) else if (user.todayStatus == 'busy') Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2, ), decoration: BoxDecoration( color: const Color(0xFFFEE2E2), borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 6, height: 6, decoration: const BoxDecoration( color: AppTheme.danger, shape: BoxShape.circle, ), ), const SizedBox(width: 4), Text( context.watch().t( 'schedule.busy', ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFFB91C1C), ), ), ], ), ) else if (user.isOnline) Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2, ), decoration: BoxDecoration( color: AppTheme.successLight, borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), ), child: Text( context.watch().t( 'specialists.online', ), style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w500, color: Color(0xFF15803D), ), ), ), ], ), ], ), ), // Favorite button GestureDetector( onTap: () => favorites.toggle(user.id, user), child: Padding( padding: const EdgeInsets.only(left: 8), child: Icon( isFav ? Icons.favorite_rounded : Icons.favorite_border_rounded, size: 22, color: isFav ? AppTheme.danger : const Color(0xFFCBD5E1), ), ), ), ], ), // ── Card titles (service cards) ── if (user.cards.isNotEmpty) ...[ const SizedBox(height: 10), Wrap( spacing: 6, runSpacing: 4, children: [ ...user.cards .take(3) .map( (card) => Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: AppTheme.background, borderRadius: BorderRadius.circular( AppTheme.chipRadius, ), ), child: Text( card['title'] as String? ?? '', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary, ), ), ), ), if (user.cards.length > 3) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), child: Text( '+${user.cards.length - 3}', style: const TextStyle( fontSize: 12, color: AppTheme.textHint, ), ), ), ], ), ] else if (user.skills != null && user.skills!.isNotEmpty) ...[ const SizedBox(height: 10), Wrap( spacing: 6, runSpacing: 4, children: user.skills! .take(3) .map( (s) => Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: AppTheme.background, borderRadius: BorderRadius.circular( AppTheme.smallRadius, ), ), child: Text( s, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ), ) .toList(), ), ], // ── Bio ── if (user.bio != null && user.bio!.isNotEmpty) ...[ const SizedBox(height: 8), Text( user.bio!, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary, height: 1.4, ), ), ], // ── Categories + location + languages ── if ((user.specialistCategories != null && user.specialistCategories!.isNotEmpty) || (user.specialistLocations != null && user.specialistLocations!.isNotEmpty) || user.languages.isNotEmpty || user.hasVerifiedBadge) ...[ const SizedBox(height: 10), const Divider(height: 1, color: AppTheme.border), const SizedBox(height: 10), Wrap( spacing: 12, runSpacing: 6, children: [ if (user.hasVerifiedBadge) Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.verified_rounded, size: 14, color: AppTheme.primary, ), const SizedBox(width: 4), Text( context.watch().t( 'profile.verifiedBadge', 'Verified', ), style: const TextStyle( fontSize: 12, color: AppTheme.primary, fontWeight: FontWeight.w500, ), ), ], ), if (user.specialistCategories != null && user.specialistCategories!.isNotEmpty) Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.category_outlined, size: 14, color: AppTheme.textSecondary, ), const SizedBox(width: 4), Text( user.specialistCategories!.take(2).join(', '), style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ), if (user.specialistLocations != null && user.specialistLocations!.isNotEmpty) Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.location_on_outlined, size: 14, color: AppTheme.textSecondary, ), const SizedBox(width: 4), Text( user.specialistLocations!.take(2).join(', '), style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ), if (user.languages.isNotEmpty) Row( mainAxisSize: MainAxisSize.min, children: [ const Icon( Icons.language_outlined, size: 14, color: AppTheme.textSecondary, ), const SizedBox(width: 4), Text( user.languages.take(3).join(', ').toUpperCase(), style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), ], ), ], ), ], ], ), ), ), ); } }