diff --git a/lib/providers/auth_provider.dart b/lib/providers/auth_provider.dart index b1cfb1e..bdfe268 100644 --- a/lib/providers/auth_provider.dart +++ b/lib/providers/auth_provider.dart @@ -81,7 +81,14 @@ class AuthProvider extends ChangeNotifier { userAverageScore = await _loadAverageScore(user!.id); authStatus = AuthStatus.authenticated; notifyListeners(); - NavigationService.replaceTo(Flurorouter.dashboardRoute); + // Usuario nuevo: el backend usa el teléfono como nombre por defecto + final name = user!.name.trim(); + final isNewUser = name.isEmpty || name == phoneNumber || name == phoneNumber.replaceAll('+57', ''); + if (isNewUser) { + NavigationService.replaceTo(Flurorouter.setupNameRoute); + } else { + NavigationService.replaceTo(Flurorouter.dashboardRoute); + } } catch (e) { authStatus = AuthStatus.notAuthenticated; notifyListeners(); @@ -89,6 +96,17 @@ class AuthProvider extends ChangeNotifier { } } + Future updateName(String name) async { + try { + await _api.patch('/users/me', {'name': name.trim()}); + user = await _fetchMe(); + notifyListeners(); + } catch (_) { + NotificationsService.showSnackBarError('Error al guardar el nombre'); + rethrow; + } + } + Future verifyPhoneNumberForLink(String phoneNumber) async { try { await _api.post('/auth/send-otp', {'phone': phoneNumber}); diff --git a/lib/router/dashboard_handlers.dart b/lib/router/dashboard_handlers.dart index d2cbf96..26a6804 100644 --- a/lib/router/dashboard_handlers.dart +++ b/lib/router/dashboard_handlers.dart @@ -21,6 +21,7 @@ import 'package:prosapp_web_app/ui/views/service_view.dart'; import 'package:prosapp_web_app/ui/views/services_history_view.dart'; import 'package:prosapp_web_app/ui/views/services_requests_view.dart'; import 'package:prosapp_web_app/ui/views/services_view.dart'; +import 'package:prosapp_web_app/ui/views/setup_name_view.dart'; import 'package:prosapp_web_app/ui/views/support_view.dart'; import 'package:provider/provider.dart'; @@ -40,6 +41,17 @@ class DashboardHandlers { }, ); + static Handler setupName = Handler( + handlerFunc: (context, params) { + final authProvider = Provider.of(context!); + if (authProvider.authStatus == AuthStatus.authenticated) { + return const SetupNameView(); + } else { + return const LoginView(); + } + }, + ); + static Handler support = Handler( handlerFunc: (context, params) { final authProvider = Provider.of(context!); diff --git a/lib/router/router.dart b/lib/router/router.dart index 64f01ca..a940ef2 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -17,6 +17,9 @@ class Flurorouter { static String dashboardRoute = "/dashboard"; static String supportRoute = "/dashboard/support"; + // Onboarding + static String setupNameRoute = "/dashboard/setup-name"; + // Users static String profileRoute = "/dashboard/profile"; static String phoneRoute = "/dashboard/phone"; @@ -68,6 +71,13 @@ class Flurorouter { transitionType: TransitionType.none, ); + // Onboarding + router.define( + setupNameRoute, + handler: DashboardHandlers.setupName, + transitionType: TransitionType.none, + ); + // Dashboard Routes router.define( dashboardRoute, diff --git a/lib/ui/shared/widgets/navbar_avatar.dart b/lib/ui/shared/widgets/navbar_avatar.dart index 9e2a6dd..8018ac7 100644 --- a/lib/ui/shared/widgets/navbar_avatar.dart +++ b/lib/ui/shared/widgets/navbar_avatar.dart @@ -10,23 +10,68 @@ class NavbarAvatar extends StatelessWidget { @override Widget build(BuildContext context) { final user = Provider.of(context).user!; - - final image = (user.picture == '' || user.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: user.picture!, - ); + final hasPicture = user.picture != null && user.picture!.isNotEmpty; return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( onTap: () => NavigationService.replaceTo(Flurorouter.profileRoute), child: SizedBox( - width: 40, - height: 40, - child: ClipOval(child: image), + width: 36, + height: 36, + child: ClipOval( + child: hasPicture + ? Image.network( + user.picture!, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _InitialsAvatar(name: user.name), + ) + : _InitialsAvatar(name: user.name), + ), + ), + ), + ); + } +} + +class _InitialsAvatar extends StatelessWidget { + final String name; + const _InitialsAvatar({required this.name}); + + String get _initials { + final parts = name.trim().split(RegExp(r'\s+')); + if (parts.isEmpty || parts.first.isEmpty) return '?'; + if (parts.length == 1) return parts.first[0].toUpperCase(); + return (parts.first[0] + parts.last[0]).toUpperCase(); + } + + Color get _bgColor { + final colors = [ + const Color(0xFF6366F1), // indigo + const Color(0xFF8B5CF6), // violet + const Color(0xFFEC4899), // pink + const Color(0xFF14B8A6), // teal + const Color(0xFFF59E0B), // amber + const Color(0xFF10B981), // emerald + const Color(0xFFEF4444), // red + const Color(0xFF3B82F6), // blue + ]; + final index = name.isEmpty ? 0 : name.codeUnitAt(0) % colors.length; + return colors[index]; + } + + @override + Widget build(BuildContext context) { + return Container( + color: _bgColor, + child: Center( + child: Text( + _initials, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), ), ), ); diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 2690300..8e90da3 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -250,6 +250,27 @@ class _ProfileViewForm extends StatelessWidget { } } +Widget _buildInitials(String name) { + final parts = name.trim().split(RegExp(r'\s+')); + final initials = parts.length >= 2 + ? '${parts.first[0]}${parts.last[0]}'.toUpperCase() + : (parts.first.isEmpty ? '?' : parts.first[0].toUpperCase()); + final colors = [ + const Color(0xFF6366F1), const Color(0xFF8B5CF6), + const Color(0xFFEC4899), const Color(0xFF14B8A6), + const Color(0xFFF59E0B), const Color(0xFF10B981), + const Color(0xFFEF4444), const Color(0xFF3B82F6), + ]; + final bg = name.isEmpty ? colors[0] : colors[name.codeUnitAt(0) % colors.length]; + return Container( + color: bg, + child: Center( + child: Text(initials, + style: const TextStyle(color: Colors.white, fontSize: 48, fontWeight: FontWeight.bold)), + ), + ); +} + class _AvatarContainer extends StatelessWidget { final bool containerFull; @@ -260,14 +281,18 @@ class _AvatarContainer extends StatelessWidget { final user = Provider.of(context).user!; final profileFormProvider = Provider.of(context); - final image = (profileFormProvider.user!.picture == '' || - profileFormProvider.user!.picture == null) - ? const Image(image: AssetImage('no-image.jpg')) - : FadeInImage.assetNetwork( - placeholder: 'loader.gif', - fit: BoxFit.cover, - image: profileFormProvider.user!.picture!, - ); + final picUrl = profileFormProvider.user!.picture; + final hasPicture = picUrl != null && picUrl.isNotEmpty; + final userName = profileFormProvider.user!.name; + + Widget avatarImage; + if (hasPicture) { + avatarImage = Image.network(picUrl, fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildInitials(userName)); + } else { + avatarImage = _buildInitials(userName); + } + final image = avatarImage; return WhiteCard( width: containerFull ? null : 250, diff --git a/lib/ui/views/setup_name_view.dart b/lib/ui/views/setup_name_view.dart new file mode 100644 index 0000000..87b54f4 --- /dev/null +++ b/lib/ui/views/setup_name_view.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:prosapp_web_app/providers/auth_provider.dart'; +import 'package:prosapp_web_app/router/router.dart'; +import 'package:prosapp_web_app/services/navigation_service.dart'; +import 'package:provider/provider.dart'; + +class SetupNameView extends StatefulWidget { + const SetupNameView({super.key}); + + @override + State createState() => _SetupNameViewState(); +} + +class _SetupNameViewState extends State { + final _nameController = TextEditingController(); + bool _loading = false; + String? _error; + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _save() async { + final name = _nameController.text.trim(); + if (name.length < 3) { + setState(() => _error = 'Ingresa al menos 3 caracteres'); + return; + } + setState(() { _loading = true; _error = null; }); + try { + await context.read().updateName(name); + NavigationService.replaceTo(Flurorouter.dashboardRoute); + } catch (_) { + setState(() => _loading = false); + } + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: const Color(0xFF42A4EF).withOpacity(0.12), + shape: BoxShape.circle, + ), + child: const Icon(Icons.waving_hand_rounded, + color: Color(0xFF42A4EF), size: 34), + ), + const SizedBox(height: 20), + const Text( + '¡Bienvenido a ProsApp!', + style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + '¿Cómo te llamas? Usaremos tu nombre para personalizar tu experiencia.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.white54 : Colors.grey[600], + ), + ), + const SizedBox(height: 32), + TextField( + controller: _nameController, + autofocus: true, + textCapitalization: TextCapitalization.words, + onSubmitted: (_) => _save(), + decoration: InputDecoration( + labelText: 'Tu nombre completo', + hintText: 'Ej: Juan Pérez', + prefixIcon: const Icon(Icons.person_outline), + errorText: _error, + filled: true, + fillColor: isDark + ? const Color(0xFF1E293B) + : const Color(0xFFF5F8FF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: isDark + ? const Color(0xFF334155) + : const Color(0xFFE0E7FF), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: const BorderSide( + color: Color(0xFF42A4EF), width: 2), + ), + ), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _loading ? null : _save, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF42A4EF), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + elevation: 0, + ), + child: _loading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, strokeWidth: 2), + ) + : const Text('Continuar', + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.w600)), + ), + ), + ], + ), + ), + ), + ); + } +}