From 97d8535d5476fcbced5e8224a3690f20d19b060e Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Sat, 27 Jun 2026 19:32:11 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20navbar=20hover=20gris=20(overlayColor)?= =?UTF-8?q?=20+=20soporte=20UI/UX=20mejorado=20+=20pol=C3=ADticas=20en=20d?= =?UTF-8?q?ialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Navbar: reemplaza hoverColor deprecated por IconButton.styleFrom(overlayColor) - Support: rediseño completo con tarjetas, secciones y manejo correcto de URLs vacías - Políticas/Términos: carga desde GET /settings/policies y muestra en dialog in-app Co-Authored-By: Claude Sonnet 4.6 --- lib/ui/shared/navbar.dart | 15 +- lib/ui/views/support_view.dart | 511 +++++++++++++++++++++------------ 2 files changed, 341 insertions(+), 185 deletions(-) diff --git a/lib/ui/shared/navbar.dart b/lib/ui/shared/navbar.dart index 552d1c7..7e95f56 100644 --- a/lib/ui/shared/navbar.dart +++ b/lib/ui/shared/navbar.dart @@ -39,8 +39,13 @@ class _NavbarState extends State { final iconColor = isDark ? Colors.white70 : const Color(0xFF475569); final bgColor = isDark ? const Color(0xFF1E293B) : Colors.white; final hoverColor = isDark - ? Colors.white.withOpacity(0.08) - : const Color(0xFF42A4EF).withOpacity(0.08); + ? Colors.white.withOpacity(0.10) + : const Color(0xFF42A4EF).withOpacity(0.10); + final overlayColor = WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.hovered)) return hoverColor; + if (states.contains(WidgetState.pressed)) return hoverColor.withOpacity(0.2); + return Colors.transparent; + }); final reputation = scoreProvider.reputation; final pendingCount = @@ -63,7 +68,7 @@ class _NavbarState extends State { if (size.width <= 700) IconButton( icon: Icon(Icons.menu, color: iconColor, size: 22), - hoverColor: hoverColor, + style: IconButton.styleFrom(overlayColor: hoverColor), onPressed: SideMenuProvider.openMenu, ), @@ -89,7 +94,7 @@ class _NavbarState extends State { // Toggle día/noche IconButton( tooltip: isDark ? 'Modo día' : 'Modo noche', - hoverColor: hoverColor, + style: IconButton.styleFrom(overlayColor: hoverColor), icon: Icon( isDark ? Icons.light_mode : Icons.dark_mode, color: isDark ? Colors.amber : iconColor, @@ -104,7 +109,7 @@ class _NavbarState extends State { children: [ IconButton( tooltip: 'Notificaciones', - hoverColor: hoverColor, + style: IconButton.styleFrom(overlayColor: hoverColor), icon: Icon( pendingCount > 0 ? Icons.notifications_active diff --git a/lib/ui/views/support_view.dart b/lib/ui/views/support_view.dart index 4d8c59c..a80dfba 100644 --- a/lib/ui/views/support_view.dart +++ b/lib/ui/views/support_view.dart @@ -1,9 +1,8 @@ +import 'package:flutter/material.dart'; import 'package:prosapp_web_app/models/setting.dart'; import 'package:prosapp_web_app/providers/settings_provider.dart'; +import 'package:prosapp_web_app/providers/theme_provider.dart'; import 'package:prosapp_web_app/services/api_service.dart'; -import 'package:prosapp_web_app/ui/cards/white_card.dart'; -import 'package:flutter/material.dart'; -import 'package:prosapp_web_app/ui/labels/custom_labels.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -11,30 +10,13 @@ class SupportView extends StatelessWidget { const SupportView({super.key}); Future _launchURL(String url) async { - final Uri _url = Uri.parse(url); - - if (await canLaunchUrl(_url)) { - await launchUrl( - _url, - mode: LaunchMode.externalApplication, - webOnlyWindowName: '_blank', // Para abrir en una nueva pestaña en web - ); - } else { - throw 'No se pudo abrir la URL $url'; + if (url.isEmpty) return; + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication, webOnlyWindowName: '_blank'); } } - Future _goWhatssApp(String number) async { - final url = 'https://api.whatsapp.com/send?phone=$number&text=Hola%21+soy+usuario+de+Prosapp+y+quisiera+conocer+mas+sobre+esta+app+%F0%9F%98%81'; - await _launchURL(url); - } - - Future _goEmail(String email) async { - final url = 'mailto:$email?subject=${Uri.encodeComponent(email)}'; - - await _launchURL(url); - } - Future _goSugerencias(BuildContext context) async { final controller = TextEditingController(); final api = ApiService.instance; @@ -48,10 +30,8 @@ class SupportView extends StatelessWidget { content: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text( - 'Cuéntanos cómo podemos mejorar ProsApp', - style: TextStyle(fontSize: 14), - ), + const Text('Cuéntanos cómo podemos mejorar ProsApp', + style: TextStyle(fontSize: 14)), const SizedBox(height: 16), TextField( controller: controller, @@ -112,163 +92,54 @@ class SupportView extends StatelessWidget { ); } - Future _goPoliticas(String urlText) async { - final url = urlText; - await _launchURL(url); - } + Future _showPolicy(BuildContext context, String title, String key, bool isDark) async { + final api = ApiService.instance; + String? content; + try { + final data = await api.get('/settings/policies') as Map; + content = data[key] as String?; + } catch (_) {} - Future _goTerminos(String urlText) async { - final url = urlText; - await _launchURL(url); - } + if (!context.mounted) return; - @override - Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; - - return Consumer( - builder: (context, settingsProvider, child) { - final settings = settingsProvider.settings ?? Setting.fromDocument({}); - - return ListView( - physics: const ClampingScrollPhysics(), - children: [ - const SizedBox(height: 10), - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), - child: WhiteCard( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 10, bottom: 30), - child: Center( - child: Text( - settings.tituloSoporte, - style: TextStyle( - fontSize: size.width > 500 ? 30 : 20, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - ), - ), - Padding( - padding: EdgeInsets.only( - left: size.width > 500 ? 30 : 0, - right: size.width > 500 ? 30 : 0, - bottom: 30, - ), - child: Text( - settings.parrafoSoporte, - style: CustomLabels.h3, - textAlign: TextAlign.center, - ), - ), - Center( - child: Column( - children: [ - const Text( - 'Horario de atención:', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.w400, - ), - ), - const SizedBox(height: 10), - Text(settings.diasSoporte, style: CustomLabels.h3), - Text(settings.horasSoporte, style: CustomLabels.h3), - ], - ), - ), - const SizedBox(height: 30), - Padding( - padding: EdgeInsets.only( - left: size.width > 500 ? 30 : 0, - right: size.width > 500 ? 30 : 0, - bottom: 30, - ), - child: Wrap( - alignment: WrapAlignment.center, - spacing: 15, - runSpacing: 15, - children: [ - _buildGridButton( - context, - icon: Icons.phone, - text: 'WhatsApp', - onPressed: () => - _goWhatssApp(settings.numeroSoporte), - ), - _buildGridButton( - context, - icon: Icons.email, - text: 'Email', - onPressed: () => _goEmail(settings.emailSoporte), - ), - _buildGridButton( - context, - icon: Icons.feedback, - text: 'Sugerencias', - onPressed: () => _goSugerencias(context), - ), - _buildGridButton( - context, - icon: Icons.privacy_tip, - text: 'Políticas de Privacidad', - onPressed: () => - _goPoliticas(settings.politicasPrivacidad), - ), - _buildGridButton( - context, - icon: Icons.article, - text: 'Términos y Condiciones', - onPressed: () => - _goTerminos(settings.terminosCondiciones), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ], - ); - }, - ); - } - - Widget _buildGridButton( - BuildContext context, { - required IconData icon, - required String text, - required VoidCallback onPressed, - }) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onPressed, + showDialog( + context: context, + builder: (_) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( - height: 110, - width: 180, - decoration: BoxDecoration( - color: Colors.blue[600], - borderRadius: BorderRadius.circular(10), - ), + constraints: const BoxConstraints(maxWidth: 600, maxHeight: 600), + padding: const EdgeInsets.all(24), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(icon, size: 40, color: Colors.white), - const SizedBox(height: 10), - Center( - child: Text( - textAlign: TextAlign.center, - text, - style: const TextStyle( - color: Colors.white, - fontSize: 18, + Row( + children: [ + Expanded( + child: Text(title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : const Color(0xFF0F172A), + )), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + const Divider(height: 24), + Expanded( + child: SingleChildScrollView( + child: Text( + (content != null && content.isNotEmpty) + ? content + : 'Contenido no disponible aún.', + style: TextStyle( + fontSize: 14, + height: 1.6, + color: isDark ? Colors.white70 : const Color(0xFF374151), + ), ), ), ), @@ -278,4 +149,284 @@ class SupportView extends StatelessWidget { ), ); } + + @override + Widget build(BuildContext context) { + final isDark = context.watch().isDark; + + return Consumer( + builder: (context, settingsProvider, _) { + final settings = settingsProvider.settings ?? Setting.fromDocument({}); + + final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white; + final borderColor = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB); + final textPrimary = isDark ? Colors.white : const Color(0xFF111827); + final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + + return ListView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 680), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + + // ── Header ── + Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF42A4EF), Color(0xFF1565C0)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + const Icon(Icons.support_agent_rounded, color: Colors.white, size: 44), + const SizedBox(height: 12), + Text( + settings.tituloSoporte, + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + if (settings.parrafoSoporte.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + settings.parrafoSoporte, + style: const TextStyle(color: Colors.white70, fontSize: 13), + textAlign: TextAlign.center, + ), + ], + ], + ), + ), + + const SizedBox(height: 16), + + // ── Horario ── + Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + decoration: BoxDecoration( + color: cardBg, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF42A4EF).withOpacity(0.1), + shape: BoxShape.circle, + ), + child: const Icon(Icons.schedule, color: Color(0xFF42A4EF), size: 20), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Horario de atención', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: textPrimary, + )), + const SizedBox(height: 2), + Text( + '${settings.diasSoporte} • ${settings.horasSoporte}', + style: TextStyle(fontSize: 12, color: textSecondary), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 16), + + // ── Contacto ── + Text('Contáctanos', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: textSecondary, + letterSpacing: 0.4, + )), + const SizedBox(height: 8), + + _ContactTile( + icon: Icons.chat_rounded, + iconColor: const Color(0xFF25D366), + label: 'WhatsApp', + subtitle: settings.numeroSoporte.isNotEmpty + ? '+${settings.numeroSoporte}' + : 'No configurado', + isDark: isDark, + cardBg: cardBg, + borderColor: borderColor, + onTap: settings.numeroSoporte.isNotEmpty + ? () => _launchURL( + 'https://api.whatsapp.com/send?phone=${settings.numeroSoporte}&text=${Uri.encodeComponent('Hola, soy usuario de ProsApp')}') + : null, + ), + + const SizedBox(height: 8), + + _ContactTile( + icon: Icons.email_outlined, + iconColor: const Color(0xFF42A4EF), + label: 'Email', + subtitle: settings.emailSoporte.isNotEmpty + ? settings.emailSoporte + : 'No configurado', + isDark: isDark, + cardBg: cardBg, + borderColor: borderColor, + onTap: settings.emailSoporte.isNotEmpty + ? () => _launchURL('mailto:${settings.emailSoporte}') + : null, + ), + + const SizedBox(height: 8), + + _ContactTile( + icon: Icons.lightbulb_outline, + iconColor: const Color(0xFFF59E0B), + label: 'Sugerencias', + subtitle: 'Ayúdanos a mejorar ProsApp', + isDark: isDark, + cardBg: cardBg, + borderColor: borderColor, + onTap: () => _goSugerencias(context), + ), + + const SizedBox(height: 20), + + // ── Legal ── + Text('Legal', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: textSecondary, + letterSpacing: 0.4, + )), + const SizedBox(height: 8), + + _ContactTile( + icon: Icons.shield_outlined, + iconColor: const Color(0xFF8B5CF6), + label: 'Políticas de Privacidad', + subtitle: 'Cómo protegemos tus datos', + isDark: isDark, + cardBg: cardBg, + borderColor: borderColor, + onTap: () => _showPolicy(context, 'Políticas de Privacidad', 'privacy', isDark), + ), + + const SizedBox(height: 8), + + _ContactTile( + icon: Icons.article_outlined, + iconColor: const Color(0xFF10B981), + label: 'Términos y Condiciones', + subtitle: 'Condiciones de uso de la plataforma', + isDark: isDark, + cardBg: cardBg, + borderColor: borderColor, + onTap: () => _showPolicy(context, 'Términos y Condiciones', 'terms', isDark), + ), + + const SizedBox(height: 24), + ], + ), + ), + ), + ], + ); + }, + ); + } +} + +class _ContactTile extends StatelessWidget { + final IconData icon; + final Color iconColor; + final String label; + final String subtitle; + final bool isDark; + final Color cardBg; + final Color borderColor; + final VoidCallback? onTap; + + const _ContactTile({ + required this.icon, + required this.iconColor, + required this.label, + required this.subtitle, + required this.isDark, + required this.cardBg, + required this.borderColor, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + final textPrimary = isDark ? Colors.white : const Color(0xFF111827); + final textSecondary = isDark ? const Color(0xFF94A3B8) : const Color(0xFF6B7280); + + return Material( + color: cardBg, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.12), + shape: BoxShape.circle, + ), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textPrimary, + )), + const SizedBox(height: 2), + Text(subtitle, + style: TextStyle(fontSize: 12, color: textSecondary)), + ], + ), + ), + if (onTap != null) + Icon(Icons.chevron_right, color: textSecondary, size: 20), + ], + ), + ), + ), + ); + } }