From fac413583516425d31f7a11213134747a7242baa Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 27 Jun 2026 09:16:31 -0500 Subject: [PATCH] fix: navbar StatefulWidget + dashboard colores dinamicos - Navbar: convertir a StatefulWidget, un solo nivel de build, usa context.watch en lugar de Consumer anidados - Iconos sol/luna y campana siempre visibles con color explicito - Dashboard: barra busqueda, sugerencias y tarjeta inferior ahora respetan isDark (cardBg, textColor, etc.) Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/shared/navbar.dart | 287 ++++++++++++++++--------------- lib/ui/views/dashboard_view.dart | 60 ++++--- 2 files changed, 183 insertions(+), 164 deletions(-) diff --git a/lib/ui/shared/navbar.dart b/lib/ui/shared/navbar.dart index c054c4a..233ebb1 100644 --- a/lib/ui/shared/navbar.dart +++ b/lib/ui/shared/navbar.dart @@ -8,119 +8,98 @@ import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/ui/shared/widgets/navbar_avatar.dart'; import 'package:provider/provider.dart'; -class Navbar extends StatelessWidget { +class Navbar extends StatefulWidget { const Navbar({super.key}); + @override + State createState() => _NavbarState(); +} + +class _NavbarState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final userId = context.read().user?.id; + if (userId != null) { + context.read().loadReputation(userId); + } + }); + } + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; + final themeProvider = context.watch(); + final isDark = themeProvider.isDark; + final scoreProvider = context.watch(); + final professionalProvider = context.watch(); + final servicesProvider = context.watch(); - return Consumer(builder: (context, themeProvider, _) { - final isDark = themeProvider.isDark; - final iconColor = isDark ? Colors.white70 : const Color(0xFF475569); - final bgColor = isDark ? const Color(0xFF1E293B) : Colors.white; - final shadowColor = isDark ? Colors.black54 : Colors.black12; + final iconColor = isDark ? Colors.white70 : const Color(0xFF475569); + final bgColor = isDark ? const Color(0xFF1E293B) : Colors.white; - return Consumer(builder: (context, scoreProvider, _) { - final professionalProvider = - Provider.of(context, listen: false); + final reputation = scoreProvider.reputation; + final pendingCount = + servicesProvider.services.where((s) => s.service.status.index == 0).length; - WidgetsBinding.instance.addPostFrameCallback((_) { - scoreProvider.loadReputation( - context.read().user!.id); - }); - - final reputation = scoreProvider.reputation; - - return Container( - width: double.infinity, - height: 56, - decoration: BoxDecoration( - color: bgColor, - boxShadow: [BoxShadow(color: shadowColor, blurRadius: 6)], + return Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + color: bgColor, + boxShadow: [ + BoxShadow( + color: isDark ? Colors.black54 : Colors.black12, + blurRadius: 6, ), - child: Row( + ], + ), + child: Row( + children: [ + if (size.width <= 700) + IconButton( + icon: Icon(Icons.menu, color: iconColor, size: 22), + onPressed: SideMenuProvider.openMenu, + ), + + const Spacer(), + + // Puntuación + Icon(Icons.star_rounded, color: Colors.amber, size: 16), + const SizedBox(width: 3), + Text( + (professionalProvider.isProModeActive + ? reputation.averagePro + : reputation.average) + .toStringAsFixed(1), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : const Color(0xFF1E293B), + ), + ), + + const SizedBox(width: 6), + + // Toggle día/noche + IconButton( + tooltip: isDark ? 'Modo día' : 'Modo noche', + icon: Icon( + isDark ? Icons.light_mode : Icons.dark_mode, + color: isDark ? Colors.amber : iconColor, + size: 22, + ), + onPressed: themeProvider.toggle, + ), + + // Campana + Stack( + clipBehavior: Clip.none, children: [ - // Menú hamburguesa (móvil) - if (size.width <= 700) - IconButton( - icon: Icon(Icons.menu, color: iconColor), - onPressed: () => SideMenuProvider.openMenu(), - ), - - const Spacer(), - - // Puntuación - Icon(Icons.star_rounded, color: Colors.amber, size: 17), - const SizedBox(width: 3), - Text( - professionalProvider.isProModeActive - ? reputation.averagePro.toStringAsFixed(1) - : reputation.average.toStringAsFixed(1), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: isDark ? Colors.white70 : const Color(0xFF1E293B), - ), - ), - - const SizedBox(width: 8), - - // Toggle día / noche - Tooltip( - message: isDark ? 'Cambiar a modo día' : 'Cambiar a modo noche', - child: InkWell( - borderRadius: BorderRadius.circular(20), - onTap: themeProvider.toggle, - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - isDark ? Icons.light_mode : Icons.dark_mode, - color: isDark ? Colors.amber : iconColor, - size: 22, - ), - ), - ), - ), - - // Campana de notificaciones - _NotificationBell(isDark: isDark, iconColor: iconColor), - - const SizedBox(width: 4), - - // Avatar - const NavbarAvatar(), - const SizedBox(width: 14), - ], - ), - ); - }); - }); - } -} - -class _NotificationBell extends StatelessWidget { - final bool isDark; - final Color iconColor; - const _NotificationBell({required this.isDark, required this.iconColor}); - - @override - Widget build(BuildContext context) { - return Consumer(builder: (context, sp, _) { - final pendingCount = - sp.services.where((s) => s.service.status.index == 0).length; - - return Tooltip( - message: 'Notificaciones', - child: Stack( - clipBehavior: Clip.none, - children: [ - InkWell( - borderRadius: BorderRadius.circular(20), - onTap: () => _showSheet(context, sp), - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( + IconButton( + tooltip: 'Notificaciones', + icon: Icon( pendingCount > 0 ? Icons.notifications_active : Icons.notifications_outlined, @@ -129,43 +108,55 @@ class _NotificationBell extends StatelessWidget { : iconColor, size: 22, ), + onPressed: () => _showNotifications( + context, + servicesProvider, + professionalProvider.isProModeActive, + isDark, + ), ), - ), - if (pendingCount > 0) - Positioned( - top: 4, - right: 4, - child: Container( - width: 15, - height: 15, - decoration: const BoxDecoration( - color: Color(0xFFEF4444), - shape: BoxShape.circle, - ), - child: Center( - child: Text( - pendingCount > 9 ? '9+' : '$pendingCount', - style: const TextStyle( - color: Colors.white, - fontSize: 8, - fontWeight: FontWeight.bold, + if (pendingCount > 0) + Positioned( + top: 6, + right: 6, + child: Container( + width: 15, + height: 15, + decoration: const BoxDecoration( + color: Color(0xFFEF4444), + shape: BoxShape.circle, + ), + child: Center( + child: Text( + pendingCount > 9 ? '9+' : '$pendingCount', + style: const TextStyle( + color: Colors.white, + fontSize: 8, + fontWeight: FontWeight.bold, + ), ), ), ), ), - ), - ], - ), - ); - }); + ], + ), + + const SizedBox(width: 4), + const NavbarAvatar(), + const SizedBox(width: 12), + ], + ), + ); } - void _showSheet(BuildContext context, ServicesProvider sp) { - final isDark = Theme.of(context).brightness == Brightness.dark; + void _showNotifications( + BuildContext context, + ServicesProvider sp, + bool isPro, + bool isDark, + ) { final pending = sp.services.where((s) => s.service.status.index == 0).toList(); - final isPro = Provider.of(context, listen: false) - .isProModeActive; showModalBottomSheet( context: context, @@ -192,11 +183,18 @@ class _NotificationBell extends StatelessWidget { const SizedBox(height: 16), Row( children: [ - const Icon(Icons.notifications_outlined, size: 20), + Icon(Icons.notifications_outlined, + size: 20, + color: isDark ? Colors.white70 : Colors.black87), const SizedBox(width: 8), - const Text('Notificaciones', - style: TextStyle( - fontSize: 16, fontWeight: FontWeight.bold)), + Text( + 'Notificaciones', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + ), const Spacer(), if (pending.isNotEmpty) Container( @@ -237,23 +235,28 @@ class _NotificationBell extends StatelessWidget { const Color(0xFF42A4EF).withOpacity(0.12), shape: BoxShape.circle, ), - child: const Icon( - Icons.room_service_outlined, - color: Color(0xFF42A4EF), - size: 20), + child: const Icon(Icons.room_service_outlined, + color: Color(0xFF42A4EF), size: 20), ), title: Text( isPro ? 'Nueva solicitud de servicio' : 'Servicio pendiente', - style: const TextStyle( - fontSize: 13, fontWeight: FontWeight.w600), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), ), subtitle: Text( s.service.address.isNotEmpty ? s.service.address : 'Sin dirección', - style: const TextStyle(fontSize: 12), + style: TextStyle( + fontSize: 12, + color: isDark + ? Colors.white54 + : Colors.black54), maxLines: 1, overflow: TextOverflow.ellipsis, ), diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart index a1dc2bb..7a54f25 100644 --- a/lib/ui/views/dashboard_view.dart +++ b/lib/ui/views/dashboard_view.dart @@ -13,6 +13,7 @@ import 'package:prosapp_web_app/models/usuario.dart'; import 'package:prosapp_web_app/models/usuario_profesional.dart'; import 'package:prosapp_web_app/providers/auth_provider.dart'; import 'package:prosapp_web_app/providers/cities_provider.dart'; +import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/router/router.dart'; import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/maps_service.dart'; @@ -292,6 +293,14 @@ class _DashboardViewState extends State { return const Center(child: CircularProgressIndicator()); } + final isDark = context.watch().isDark; + final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; + final cardBorder = isDark ? const Color(0xFF334155) : const Color(0xFFE0E0E0); + final textColor = isDark ? Colors.white : Colors.black87; + final subtextColor = isDark ? Colors.white54 : Colors.black54; + final hintColor = isDark ? Colors.white38 : Colors.grey; + final inputFill = isDark ? const Color(0xFF0F172A) : Colors.white; + return GestureDetector( onTap: () { _searchFocus.unfocus(); @@ -355,11 +364,11 @@ class _DashboardViewState extends State { children: [ Container( decoration: BoxDecoration( - color: Colors.white, + color: cardBg, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.15), + color: Colors.black.withOpacity(isDark ? 0.4 : 0.15), blurRadius: 14, offset: const Offset(0, 4), ), @@ -369,10 +378,10 @@ class _DashboardViewState extends State { controller: _searchController, focusNode: _searchFocus, onChanged: _onSearchChanged, - style: const TextStyle(fontSize: 14), + style: TextStyle(fontSize: 14, color: textColor), decoration: InputDecoration( hintText: 'Busca o mueve el mapa para fijar tu dirección', - hintStyle: const TextStyle(color: Colors.grey, fontSize: 13), + hintStyle: TextStyle(color: hintColor, fontSize: 13), prefixIcon: _geocoding ? const Padding( padding: EdgeInsets.all(13), @@ -406,10 +415,10 @@ class _DashboardViewState extends State { Container( margin: const EdgeInsets.only(top: 4), decoration: BoxDecoration( - color: Colors.white, + color: cardBg, borderRadius: BorderRadius.circular(12), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.12), blurRadius: 10), + BoxShadow(color: Colors.black.withOpacity(isDark ? 0.4 : 0.12), blurRadius: 10), ], ), child: Column( @@ -431,7 +440,7 @@ class _DashboardViewState extends State { Expanded( child: Text( addr, - style: const TextStyle(fontSize: 13), + style: TextStyle(fontSize: 13, color: textColor), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -440,7 +449,8 @@ class _DashboardViewState extends State { ), ), ), - if (!isLast) Divider(height: 1, indent: 16, color: Colors.grey[100]), + if (!isLast) Divider(height: 1, indent: 16, + color: isDark ? Colors.white12 : Colors.grey[100]), ], ); }).toList(), @@ -471,11 +481,11 @@ class _DashboardViewState extends State { bottom: 0, child: Container( decoration: BoxDecoration( - color: Colors.white, + color: cardBg, borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.13), + color: Colors.black.withOpacity(isDark ? 0.4 : 0.13), blurRadius: 18, offset: const Offset(0, -4), ), @@ -504,7 +514,9 @@ class _DashboardViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( - color: const Color(0xFFF0F8FF), + color: isDark + ? const Color(0xFF42A4EF).withOpacity(0.12) + : const Color(0xFFF0F8FF), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF42A4EF).withOpacity(0.35)), ), @@ -515,7 +527,7 @@ class _DashboardViewState extends State { Expanded( child: Text( _currentAddress, - style: const TextStyle(fontSize: 12, color: Colors.black87), + style: TextStyle(fontSize: 12, color: textColor), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -533,17 +545,21 @@ class _DashboardViewState extends State { border: Border.all( color: _professional != null ? const Color(0xFF42A4EF) - : const Color(0xFFE0E0E0), + : cardBorder, ), borderRadius: BorderRadius.circular(10), - color: _professional != null ? const Color(0xFFF0F8FF) : Colors.grey[50], + color: _professional != null + ? const Color(0xFF42A4EF).withOpacity(0.1) + : (isDark ? const Color(0xFF0F172A) : Colors.grey[50]), ), child: Row( children: [ Icon( Icons.person_outline, size: 20, - color: _professional != null ? const Color(0xFF42A4EF) : Colors.grey, + color: _professional != null + ? const Color(0xFF42A4EF) + : subtextColor, ), const SizedBox(width: 10), Expanded( @@ -551,11 +567,11 @@ class _DashboardViewState extends State { _professional?.user.name ?? 'Seleccionar profesional', style: TextStyle( fontSize: 13, - color: _professional != null ? Colors.black87 : Colors.grey, + color: _professional != null ? textColor : subtextColor, ), ), ), - const Icon(Icons.chevron_right, color: Colors.grey, size: 18), + Icon(Icons.chevron_right, color: subtextColor, size: 18), ], ), ), @@ -567,9 +583,9 @@ class _DashboardViewState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - border: Border.all(color: const Color(0xFFE0E0E0)), + border: Border.all(color: cardBorder), borderRadius: BorderRadius.circular(10), - color: Colors.grey[50], + color: isDark ? const Color(0xFF0F172A) : Colors.grey[50], ), child: Row( children: [ @@ -577,14 +593,14 @@ class _DashboardViewState extends State { const SizedBox(width: 8), Text( DateFormat('dd/MM/yyyy').format(_selectedDay!), - style: const TextStyle(fontSize: 13), + style: TextStyle(fontSize: 13, color: textColor), ), const SizedBox(width: 14), const Icon(Icons.access_time, size: 17, color: Color(0xFF42A4EF)), const SizedBox(width: 6), Text( ScheduleEntity.getFormatTime(_selectedHour!) ?? '', - style: const TextStyle(fontSize: 13), + style: TextStyle(fontSize: 13, color: textColor), ), const Spacer(), GestureDetector( @@ -593,7 +609,7 @@ class _DashboardViewState extends State { _selectedHour = null; _professional = null; }), - child: const Icon(Icons.close, size: 18, color: Colors.grey), + child: Icon(Icons.close, size: 18, color: subtextColor), ), ], ),