diff --git a/lib/ui/views/professionals_view.dart b/lib/ui/views/professionals_view.dart index 6dbbe1a..2186416 100644 --- a/lib/ui/views/professionals_view.dart +++ b/lib/ui/views/professionals_view.dart @@ -1,172 +1,553 @@ import 'package:flutter/material.dart'; import 'package:prosapp_web_app/models/usuario_profesional.dart'; -import 'package:prosapp_web_app/services/navigation_service.dart'; -import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; -import 'package:provider/provider.dart'; import 'package:prosapp_web_app/providers/professionals_provider.dart'; -import 'package:prosapp_web_app/ui/cards/white_card.dart'; +import 'package:prosapp_web_app/providers/theme_provider.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; -class ProfessionalsView extends StatelessWidget { +class ProfessionalsView extends StatefulWidget { const ProfessionalsView({super.key}); + @override + State createState() => _ProfessionalsViewState(); +} + +class _ProfessionalsViewState extends State { + String _search = ''; + @override Widget build(BuildContext context) { - final professionalsProvider = Provider.of(context); + final provider = context.watch(); + final isDark = context.watch().isDark; - return LayoutBuilder( - builder: (context, constraints) { - int crossAxisCount; - double childAspectRatio; + final filtered = provider.professionals.where((p) { + if (_search.isEmpty) return true; + final q = _search.toLowerCase(); + return p.user.name.toLowerCase().contains(q) || + p.professionalInfo.profession.toLowerCase().contains(q) || + (p.user.city ?? '').toLowerCase().contains(q); + }).toList(); - if (constraints.maxWidth >= 1540) { - crossAxisCount = 5; - childAspectRatio = 0.75; - } else if (constraints.maxWidth >= 1000) { - crossAxisCount = 4; - childAspectRatio = 0.65; - } else if (constraints.maxWidth >= 650) { - crossAxisCount = 3; - childAspectRatio = 0.55; - } else if (constraints.maxWidth >= 400) { - crossAxisCount = 2; - childAspectRatio = 0.55; - } else { - crossAxisCount = 1; - childAspectRatio = 0.7; - } + final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; + final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); + final inputFill = isDark ? const Color(0xFF0F172A) : const Color(0xFFF1F5F9); + final hintColor = isDark ? const Color(0xFF64748B) : const Color(0xFF94A3B8); - if (professionalsProvider.isLoading) { - return const Center( - child: CircularProgressIndicator(), - ); - } - - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - childAspectRatio: childAspectRatio, + return Column( + children: [ + // ── Barra de búsqueda ── + Padding( + padding: const EdgeInsets.fromLTRB(0, 0, 0, 14), + child: TextField( + onChanged: (v) => setState(() => _search = v), + style: TextStyle( + color: isDark ? Colors.white : const Color(0xFF111827), + fontSize: 14, + ), + decoration: InputDecoration( + hintText: 'Buscar por nombre, profesión o ciudad...', + hintStyle: TextStyle(color: hintColor, fontSize: 13), + prefixIcon: + const Icon(Icons.search, color: Color(0xFF42A4EF), size: 20), + suffixIcon: _search.isNotEmpty + ? IconButton( + icon: Icon(Icons.close, size: 18, color: hintColor), + onPressed: () => setState(() => _search = ''), + ) + : null, + filled: true, + fillColor: inputFill, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: border), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: border), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: + const BorderSide(color: Color(0xFF42A4EF), width: 2), + ), + contentPadding: + const EdgeInsets.symmetric(vertical: 13, horizontal: 4), + ), ), - itemCount: professionalsProvider.professionals.length, - itemBuilder: (context, index) { - UsuarioProfesional data = professionalsProvider.professionals[index]; + ), - final image = (data.user.picture == '' || data.user.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: data.user.picture!, - ); + // ── Lista / grid / estados ── + Expanded( + child: provider.isLoading + ? const Center(child: CircularProgressIndicator()) + : filtered.isEmpty + ? _EmptyState(isDark: isDark, search: _search) + : LayoutBuilder( + builder: (context, constraints) { + final w = constraints.maxWidth; + final cols = w >= 1200 + ? 4 + : w >= 800 + ? 3 + : w >= 500 + ? 2 + : 1; - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () async { - final List result = await NavigationService.navigateToFuture('/dashboard/calendar/${data.user.id}'); + return GridView.builder( + padding: EdgeInsets.zero, + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: cols, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: cols == 1 ? 2.4 : 0.78, + ), + itemCount: filtered.length, + itemBuilder: (context, i) => _ProfCard( + data: filtered[i], + isDark: isDark, + cardBg: cardBg, + border: border, + compact: cols == 1, + onTap: () async { + final result = await NavigationService + .navigateToFuture( + '/dashboard/calendar/${filtered[i].user.id}', + ); + if (context.mounted) { + Navigator.pop(context, [ + filtered[i], + result[0] as DateTime, + result[1] as TimeOfDay, + ]); + } + }, + ), + ); + }, + ), + ), + ], + ); + } +} - DateTime day = result[0]; - TimeOfDay time = result[1]; +// ── Card ───────────────────────────────────────────────────────────────────── - Navigator.pop(context, [data, day, time]); - }, - child: WhiteCard( - title: data.user.name.toUpperCase(), +class _ProfCard extends StatelessWidget { + final UsuarioProfesional data; + final bool isDark; + final Color cardBg; + final Color border; + final bool compact; + final VoidCallback onTap; + + const _ProfCard({ + required this.data, + required this.isDark, + required this.cardBg, + required this.border, + required this.compact, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final textPrimary = isDark ? Colors.white : const Color(0xFF111827); + final textSecondary = + isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + final hasPic = + data.user.picture != null && data.user.picture!.isNotEmpty; + final score = data.averageScore; + final pm = data.professionalInfo.paymentMethods; + + if (compact) { + // Fila horizontal en pantallas pequeñas + return Material( + color: cardBg, + borderRadius: BorderRadius.circular(14), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(14), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + border: Border.all(color: border), + ), + child: Row( + children: [ + _Avatar(hasPic: hasPic, picture: data.user.picture, name: data.user.name, size: 60), + const SizedBox(width: 14), + Expanded( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - children: [ - const Icon( - Icons.star, - size: 15, - color: Colors.yellow, - ), - const SizedBox(width: 3), - Text( - data.averageScore.toString(), - style: CustomLabels.h4, - ), - ], - ), - Center( - child: SizedBox( - width: 100, - height: 100, - child: ClipOval( - child: image, - ), - ), - ), - const SizedBox(height: 10), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 5), - Row( - children: [ - const Icon(Icons.location_on_outlined, - size: 15), - const SizedBox(width: 2), - Text( - data.user.city ?? '', - textAlign: TextAlign.center, - ), - ], - ), - Row( - children: [ - const Icon(Icons.work_outline, size: 15), - const SizedBox(width: 5), - Text(data.professionalInfo.profession), - ], - ), - const Divider(), - if (data.professionalInfo.paymentMethods.datafono || - data.professionalInfo.paymentMethods.nequi || - data.professionalInfo.paymentMethods - .transferencia) ...[ - const Row( - children: [ - Icon(Icons.payment_outlined, size: 15), - SizedBox(width: 5), - Text('Metodos de pago'), - ], - ), - Visibility( - visible: data - .professionalInfo.paymentMethods.datafono, - child: const Text('- Datafono'), - ), - Visibility( - visible: - data.professionalInfo.paymentMethods.nequi, - child: const Text('- Nequi'), - ), - Visibility( - visible: data.professionalInfo.paymentMethods - .transferencia, - child: const Text('- Transferencia'), - ), - ] else - const Text( - 'No hay metodos de pago registrados', + Text(data.user.name, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: textPrimary)), + const SizedBox(height: 3), + if (data.professionalInfo.profession.isNotEmpty) + Text(data.professionalInfo.profession, + style: TextStyle( + fontSize: 12, + color: const Color(0xFF42A4EF), + fontWeight: FontWeight.w500)), + const SizedBox(height: 4), + Row(children: [ + _StarRow(score: score), + const SizedBox(width: 10), + if (data.user.city != null && data.user.city!.isNotEmpty) + Row(children: [ + Icon(Icons.location_on_outlined, + size: 11, color: textSecondary), + const SizedBox(width: 2), + Text(data.user.city!, style: TextStyle( - fontSize: 14, - color: Colors.black45, - ), - ), + fontSize: 11, color: textSecondary)), + ]), + ]), + ], + ), + ), + Icon(Icons.chevron_right, color: textSecondary, size: 20), + ], + ), + ), + ), + ); + } + + // Tarjeta vertical (grid) + return Material( + color: cardBg, + borderRadius: BorderRadius.circular(16), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // ── Header con avatar ── + Container( + height: 90, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: isDark + ? [const Color(0xFF1E3A5F), const Color(0xFF0D1B3E)] + : [const Color(0xFFDBEAFE), const Color(0xFFBFDBFE)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16)), + ), + child: Stack( + clipBehavior: Clip.none, + children: [ + // Score badge + Positioned( + top: 10, + right: 10, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.25), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_rounded, + color: Colors.amber, size: 13), + const SizedBox(width: 3), + Text( + score.toStringAsFixed(1), + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w700), + ), ], ), ), + ), + // Avatar centrado en la parte inferior del header + Positioned( + bottom: -28, + left: 0, + right: 0, + child: Center( + child: _Avatar( + hasPic: hasPic, + picture: data.user.picture, + name: data.user.name, + size: 56, + border: true, + isDark: isDark, + ), + ), + ), + ], + ), + ), + + // ── Contenido ── + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 36, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Nombre + Text( + data.user.name, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: textPrimary, + ), + ), + + // Profesión + if (data.professionalInfo.profession.isNotEmpty) ...[ + const SizedBox(height: 3), + Text( + data.professionalInfo.profession, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 11, + color: Color(0xFF42A4EF), + fontWeight: FontWeight.w600, + ), + ), + ], + + // Ciudad + if (data.user.city != null && + data.user.city!.isNotEmpty) ...[ + const SizedBox(height: 4), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.location_on_outlined, + size: 11, color: textSecondary), + const SizedBox(width: 2), + Flexible( + child: Text( + data.user.city!, + style: TextStyle( + fontSize: 11, color: textSecondary), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + + const Spacer(), + + // Métodos de pago + if (pm.datafono || pm.nequi || pm.transferencia) ...[ + const Divider(height: 16), + Wrap( + alignment: WrapAlignment.center, + spacing: 4, + runSpacing: 4, + children: [ + if (pm.datafono) + _PayChip( + label: 'Datáfono', + icon: Icons.credit_card_outlined, + isDark: isDark), + if (pm.nequi) + _PayChip( + label: 'Nequi', + icon: Icons.phone_android_outlined, + isDark: isDark), + if (pm.transferencia) + _PayChip( + label: 'Transferencia', + icon: Icons.account_balance_outlined, + isDark: isDark), + ], + ), + ] else + Text('Sin métodos de pago', + style: TextStyle( + fontSize: 10, color: textSecondary)), ], ), ), ), - ); - }, - ); - }, + ], + ), + ), + ), + ); + } +} + +// ── Widgets auxiliares ──────────────────────────────────────────────────────── + +class _Avatar extends StatelessWidget { + final bool hasPic; + final String? picture; + final String name; + final double size; + final bool border; + final bool isDark; + + const _Avatar({ + required this.hasPic, + required this.picture, + required this.name, + required this.size, + this.border = false, + this.isDark = false, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: const Color(0xFF42A4EF).withOpacity(0.15), + border: border + ? Border.all( + color: isDark ? const Color(0xFF1E293B) : Colors.white, + width: 3) + : null, + ), + child: ClipOval( + child: hasPic + ? FadeInImage.assetNetwork( + placeholder: 'loader.gif', + image: picture!, + fit: BoxFit.cover, + ) + : Center( + child: Text( + name.isNotEmpty ? name[0].toUpperCase() : '?', + style: TextStyle( + color: const Color(0xFF42A4EF), + fontSize: size * 0.38, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ); + } +} + +class _StarRow extends StatelessWidget { + final double score; + const _StarRow({required this.score}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.star_rounded, color: Colors.amber, size: 14), + const SizedBox(width: 3), + Text(score.toStringAsFixed(1), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.amber)), + ], + ); + } +} + +class _PayChip extends StatelessWidget { + final String label; + final IconData icon; + final bool isDark; + + const _PayChip( + {required this.label, required this.icon, required this.isDark}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: const Color(0xFF42A4EF).withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + border: Border.all( + color: const Color(0xFF42A4EF).withOpacity(0.25)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 10, color: const Color(0xFF42A4EF)), + const SizedBox(width: 3), + Text(label, + style: const TextStyle( + fontSize: 9, + color: Color(0xFF42A4EF), + fontWeight: FontWeight.w600)), + ], + ), + ); + } +} + +class _EmptyState extends StatelessWidget { + final bool isDark; + final String search; + const _EmptyState({required this.isDark, required this.search}); + + @override + Widget build(BuildContext context) { + final textPrimary = isDark ? Colors.white : const Color(0xFF111827); + final textSecondary = + isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.person_search_outlined, + size: 64, color: textSecondary.withOpacity(0.4)), + const SizedBox(height: 16), + Text( + search.isNotEmpty + ? 'Sin resultados para "$search"' + : 'No hay profesionales disponibles', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textPrimary), + ), + const SizedBox(height: 6), + Text( + search.isNotEmpty + ? 'Intenta con otro nombre o profesión' + : 'Aún no hay profesionales activos en tu ciudad', + style: TextStyle(fontSize: 13, color: textSecondary), + ), + ], + ), ); } }