fix: navbar hover gris (overlayColor) + soporte UI/UX mejorado + políticas en dialog
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d359974de5
commit
97d8535d54
@@ -39,8 +39,13 @@ class _NavbarState extends State<Navbar> {
|
||||
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<Color?>((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<Navbar> {
|
||||
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<Navbar> {
|
||||
// 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<Navbar> {
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: 'Notificaciones',
|
||||
hoverColor: hoverColor,
|
||||
style: IconButton.styleFrom(overlayColor: hoverColor),
|
||||
icon: Icon(
|
||||
pendingCount > 0
|
||||
? Icons.notifications_active
|
||||
|
||||
+331
-180
@@ -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<void> _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<void> _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<void> _goEmail(String email) async {
|
||||
final url = 'mailto:$email?subject=${Uri.encodeComponent(email)}';
|
||||
|
||||
await _launchURL(url);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _goPoliticas(String urlText) async {
|
||||
final url = urlText;
|
||||
await _launchURL(url);
|
||||
}
|
||||
Future<void> _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<String, dynamic>;
|
||||
content = data[key] as String?;
|
||||
} catch (_) {}
|
||||
|
||||
Future<void> _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<SettingsProvider>(
|
||||
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<ThemeProvider>().isDark;
|
||||
|
||||
return Consumer<SettingsProvider>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user