import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:dio/dio.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_widgets.dart'; import '../../core/services/auth_controller.dart'; import '../../core/models/auth_state.dart'; import '../../core/constants/auth_constants.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../core/models/colonia.dart'; import '../home/colonias_data.dart'; import '../addresses/colonias_provider.dart'; const Map _cpToColonia = { '38000': 'Zona Centro', '38060': 'Las Arboledas', '38027': 'San Juanico', '38037': 'Los Olivos', '38090': 'Rancho Seco', '38080': 'Las Insurgentes', '38086': 'Trojes', }; class RegisterPage extends ConsumerStatefulWidget { const RegisterPage({super.key}); @override ConsumerState createState() => _RegisterPageState(); } class _RegisterPageState extends ConsumerState { final _pageController = PageController(); int _currentPage = 0; final _step1FormKey = GlobalKey(); // Paso 1 final _nameCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); final _telefonoCtrl = TextEditingController(); final _passCtrl = TextEditingController(); bool _obscurePass = true; // Paso 2 final _mapController = MapController(); final _cpCtrl = TextEditingController(); final _calleCtrl = TextEditingController(); Colonia? _selectedColonia; LatLng? _selectedLocation; String _tipoInmueble = 'Casa'; bool _whatsappNotif = false; @override void initState() { super.initState(); ref.listenManual>(authControllerProvider, ( prev, next, ) { if (!mounted) return; if (next is AsyncError) { String errorMessage = 'Ocurrió un error inesperado'; final error = next.error; if (error is DioException) { if (error.response?.data != null && error.response?.data is Map) { errorMessage = error.response!.data['detail'] ?? 'Error al registrarse'; } else { errorMessage = 'Error de conexión con el servidor'; } } else { errorMessage = error.toString(); } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: AppTheme.danger, behavior: SnackBarBehavior.floating, ), ); } }); } @override void dispose() { _pageController.dispose(); _nameCtrl.dispose(); _emailCtrl.dispose(); _telefonoCtrl.dispose(); _passCtrl.dispose(); _calleCtrl.dispose(); _cpCtrl.dispose(); _mapController.dispose(); super.dispose(); } void _nextPage() { if (!(_step1FormKey.currentState?.validate() ?? false)) return; _pageController.nextPage( duration: const Duration(milliseconds: 350), curve: Curves.easeInOut, ); setState(() => _currentPage = 1); } Future _fetchStreetName(LatLng latlng) async { setState(() => _selectedLocation = latlng); try { final dio = Dio(); final response = await dio.get( 'https://nominatim.openstreetmap.org/reverse', queryParameters: { 'lat': latlng.latitude, 'lon': latlng.longitude, 'format': 'json', 'addressdetails': 1, }, options: kIsWeb ? null : Options(headers: {'User-Agent': 'com.onlineshack.recolecta'}), ); if (response.data != null && response.data['address'] != null) { final address = response.data['address']; final road = address['road'] ?? address['pedestrian'] ?? address['street'] ?? ''; final houseNumber = address['house_number'] ?? ''; if (road.isNotEmpty) { setState(() { _calleCtrl.text = '$road $houseNumber'.trim(); }); } } } catch (e) { debugPrint('Aviso: Error al obtener nombre de la calle de OSM: $e'); } } void _validarCP(String cp, List colonias) { if (cp.length != 5) { if (_selectedColonia != null) { setState(() { _selectedColonia = null; _selectedLocation = null; _calleCtrl.clear(); }); } return; } final nombre = _cpToColonia[cp]; if (nombre == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Código postal fuera de nuestra zona de servicio actual.', ), backgroundColor: AppTheme.danger, behavior: SnackBarBehavior.floating, ), ); setState(() { _selectedColonia = null; _selectedLocation = null; }); return; } final backendC = colonias .where((c) => c.nombre.toLowerCase() == nombre.toLowerCase()) .firstOrNull; if (backendC == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Esta colonia aún no tiene horarios configurados.'), backgroundColor: AppTheme.danger, behavior: SnackBarBehavior.floating, ), ); setState(() { _selectedColonia = null; _selectedLocation = null; }); return; } setState(() { _selectedColonia = backendC; _selectedLocation = kColoniasCoordinates[nombre]; }); FocusScope.of(context).unfocus(); } void _onRegister() { final auth = ref.read(authControllerProvider.notifier); auth.register( name: _nameCtrl.text, email: _emailCtrl.text, phone: _telefonoCtrl.text, password: _passCtrl.text, addressCalle: _calleCtrl.text, addressColonia: _selectedColonia?.nombre, addressLabel: _tipoInmueble, addressLat: _selectedLocation?.latitude, addressLng: _selectedLocation?.longitude, ); } @override Widget build(BuildContext context) { final loading = ref.watch(authControllerProvider).isLoading; final coloniasAsync = ref.watch(coloniasProvider); final coloniasList = coloniasAsync.value ?? []; return Scaffold( backgroundColor: AppTheme.background, body: Column( children: [ _buildPageHeader(context), Expanded( child: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), children: [ _buildStep1(context), _buildStep2(context, loading, coloniasList), ], ), ), ], ), bottomNavigationBar: _buildBottomControls(context, loading), ); } Widget _buildPageHeader(BuildContext context) { return Container( padding: EdgeInsets.fromLTRB( 20, MediaQuery.of(context).padding.top + 12, 20, 20, ), decoration: const BoxDecoration( gradient: LinearGradient( colors: [Color(0xFF4A0E26), Color(0xFF9B1B4A)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.only( bottomLeft: Radius.circular(28), bottomRight: Radius.circular(28), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 36, height: 36, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: const Icon( Icons.arrow_back, color: Colors.white, size: 20, ), ), ), const SizedBox(width: 14), Text( _currentPage == 0 ? 'Crear cuenta' : 'Mi dirección', style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: Colors.white, ), ), ], ), const SizedBox(height: 16), _HorizontalStepIndicator(current: _currentPage, total: 2), ], ), ); } Widget _buildStep1(BuildContext context) { return Form( key: _step1FormKey, child: ListView( padding: const EdgeInsets.fromLTRB(24, 24, 24, 40), children: [ const Text( 'Crea tu cuenta', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), const SizedBox(height: 6), const Text( 'Ingresa tus datos para registrarte.', style: TextStyle(fontSize: 15, color: AppTheme.textSecondary), ), const SizedBox(height: 24), AppFormCard( icon: Icons.person_outline, title: 'Información de cuenta', child: Column( children: [ AppFormField( controller: _nameCtrl, label: 'Nombre completo', validator: (val) => val!.isEmpty ? 'Ingresa tu nombre completo' : null, ), const SizedBox(height: 14), AppFormField( controller: _emailCtrl, label: 'Correo electrónico', hint: 'tu@correo.com', keyboardType: TextInputType.emailAddress, validator: (v) { if (v == null || v.trim().isEmpty) return 'Ingresa tu correo'; final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+'); if (!emailRegex.hasMatch(v.trim())) { return 'Ingresa un correo válido'; } return null; }, ), const SizedBox(height: 14), _PhoneField(controller: _telefonoCtrl), const SizedBox(height: 14), AppFormField( label: 'Contraseña', hint: '••••••••', controller: _passCtrl, obscureText: _obscurePass, validator: (v) { if (v == null || v.isEmpty) return 'Ingresa una contraseña'; if (v.length < 6) return 'Mínimo 6 caracteres'; return null; }, suffix: IconButton( icon: Icon( _obscurePass ? Icons.visibility_outlined : Icons.visibility_off_outlined, size: 18, color: AppTheme.textSecondary, ), onPressed: () => setState(() => _obscurePass = !_obscurePass), ), ), ], ), ), const SizedBox(height: 20), Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ const Text( '¿Ya tienes cuenta? ', style: TextStyle(fontSize: 13, color: AppTheme.textSecondary), ), GestureDetector( onTap: () => context.go('/login'), child: const Text( 'Inicia sesión', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: AppTheme.primary, ), ), ), ], ), ), ], ), ); } Widget _buildStep2( BuildContext context, bool loading, List coloniasList, ) { return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 24, 24, 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Tu dirección', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), const SizedBox(height: 6), const Text( 'Ubica tu domicilio para recibir alertas.', style: TextStyle(fontSize: 15, color: AppTheme.textSecondary), ), const SizedBox(height: 24), AppFormCard( icon: Icons.home_outlined, title: 'Dirección de tu casa', child: Column( children: [ const Text( 'Tipo de inmueble', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary, ), ), Row( children: [ Expanded( child: Material( color: Colors.transparent, child: RadioListTile( title: const Text( 'Casa', style: TextStyle(fontSize: 14), ), value: 'Casa', groupValue: _tipoInmueble, onChanged: (v) => setState(() => _tipoInmueble = v!), ), ), ), Expanded( child: Material( color: Colors.transparent, child: RadioListTile( title: const Text( 'Negocio', style: TextStyle(fontSize: 14), ), value: 'Negocio', groupValue: _tipoInmueble, onChanged: (v) => setState(() => _tipoInmueble = v!), ), ), ), ], ), const SizedBox(height: 8), AppFormField( label: 'Código Postal', hint: 'Ej. 38000', controller: _cpCtrl, keyboardType: TextInputType.number, onChanged: (v) => _validarCP(v, coloniasList), ), if (_selectedColonia != null) ...[ const SizedBox(height: 14), Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppTheme.primaryLight.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(AppTheme.radiusSm), border: Border.all(color: AppTheme.primaryMid), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon( Icons.check_circle_outline, color: AppTheme.primary, size: 18, ), const SizedBox(width: 8), Expanded( child: Text( 'Colonia: ${_selectedColonia!.nombre}', style: const TextStyle( fontWeight: FontWeight.w600, color: AppTheme.primaryDark, ), ), ), ], ), const SizedBox(height: 8), Text( 'Horario ${_selectedColonia!.turno?.toLowerCase() ?? 'asignado'}', style: const TextStyle( fontSize: 13, color: AppTheme.textPrimary, ), ), Text( _selectedColonia!.horarioEstimado ?? 'Sin horario especificado', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: AppTheme.textPrimary, ), ), ], ), ), const SizedBox(height: 14), AppFormField( label: 'Calle y número', hint: 'Av. Insurgentes 245', controller: _calleCtrl, ), const SizedBox(height: 16), const Text( 'Toca el mapa para ubicar tu casa exacta:', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary, ), ), const SizedBox(height: 8), Container( height: 220, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppTheme.radiusMd), border: Border.all(color: AppTheme.border), boxShadow: AppTheme.softShadow, ), clipBehavior: Clip.hardEdge, child: FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: _selectedLocation ?? const LatLng(20.5222, -100.8123), initialZoom: 15.0, onTap: (_, latlng) => _fetchStreetName(latlng), ), children: [ TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.onlineshack.recolecta', ), if (_selectedLocation != null) MarkerLayer( markers: [ Marker( point: _selectedLocation!, width: 40, height: 40, child: const Icon( Icons.location_on, color: AppTheme.danger, size: 40, ), ), ], ), ], ), ), ] else ...[ const SizedBox(height: 24), const Center( child: Text( 'Ingresa un código postal con servicio\npara asignar tu colonia.', textAlign: TextAlign.center, style: TextStyle( color: AppTheme.textSecondary, fontSize: 13, ), ), ), ], ], ), ), const SizedBox(height: 16), AppFormCard( icon: Icons.document_scanner_outlined, title: 'Verificación de Domicilio', child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Para prevenir abusos, requerimos validar tu dirección con un recibo (luz o agua). ' 'Por privacidad, la imagen será borrada inmediatamente después de la lectura.', style: TextStyle( fontSize: 13, color: AppTheme.textSecondary, height: 1.4, ), ), const SizedBox(height: 14), SizedBox( width: double.infinity, child: OutlinedButton.icon( icon: const Icon( Icons.upload_file, color: AppTheme.primary, ), label: const Text( 'Escanear recibo (OCR)', style: TextStyle(color: AppTheme.primary), ), style: OutlinedButton.styleFrom( side: const BorderSide(color: AppTheme.primary), ), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Abriendo cámara... (Próximamente)'), ), ); }, ), ), ], ), ), const SizedBox(height: 16), AppFormCard( icon: Icons.chat_outlined, title: 'Notificaciones Externas', child: Material( color: Colors.transparent, child: CheckboxListTile( contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, activeColor: AppTheme.primary, value: _whatsappNotif, onChanged: (v) => setState(() => _whatsappNotif = v ?? false), title: const Text( 'Recibir alertas del camión vía WhatsApp', style: TextStyle(fontSize: 14, color: AppTheme.textPrimary), ), ), ), ), const SizedBox(height: 16), const Center( child: Text( 'Al registrarte aceptas los Términos de Servicio\ny la Política de Privacidad.', textAlign: TextAlign.center, style: TextStyle( fontSize: 11, color: AppTheme.textSecondary, height: 1.5, ), ), ), ], ), ); } Widget _buildBottomControls(BuildContext context, bool isLoading) { return Container( padding: EdgeInsets.fromLTRB( 24, 12, 24, MediaQuery.of(context).padding.bottom + 16, ), decoration: const BoxDecoration( color: AppTheme.background, border: Border(top: BorderSide(color: AppTheme.border, width: 0.5)), ), child: _currentPage == 0 ? SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: _nextPage, child: const Text('Continuar'), ), ) : SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: isLoading ? null : _onRegister, child: isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text('Crear mi cuenta'), ), ), ); } } // ── Indicador horizontal de pasos ───────────────────────────────────────────── class _HorizontalStepIndicator extends StatelessWidget { final int current; final int total; const _HorizontalStepIndicator({required this.current, required this.total}); @override Widget build(BuildContext context) { return Row( children: List.generate(total, (i) { final isCompleted = i < current; final isActive = i == current; return Expanded( child: Row( children: [ _StepCircle( index: i + 1, isCompleted: isCompleted, isActive: isActive, ), if (i < total - 1) Expanded( child: Container( height: 2, color: isCompleted ? Colors.white.withValues(alpha: 0.8) : Colors.white.withValues(alpha: 0.3), ), ), ], ), ); }), ); } } class _StepCircle extends StatelessWidget { final int index; final bool isCompleted; final bool isActive; const _StepCircle({ required this.index, required this.isCompleted, required this.isActive, }); @override Widget build(BuildContext context) { return Column( children: [ AnimatedContainer( duration: const Duration(milliseconds: 250), width: 32, height: 32, decoration: BoxDecoration( shape: BoxShape.circle, color: (isCompleted || isActive) ? Colors.white : Colors.white.withValues(alpha: 0.2), border: Border.all( color: Colors.white.withValues(alpha: 0.6), width: 1.5, ), ), child: Center( child: isCompleted ? const Icon(Icons.check, size: 16, color: AppTheme.primary) : Text( '$index', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: isActive ? AppTheme.primary : Colors.white.withValues(alpha: 0.6), ), ), ), ), const SizedBox(height: 4), Text( index == 1 ? 'Cuenta' : 'Dirección', style: TextStyle( fontSize: 10, color: isActive || isCompleted ? Colors.white : Colors.white.withValues(alpha: 0.5), fontWeight: isActive ? FontWeight.w600 : FontWeight.normal, ), ), ], ); } } // ── Campo de teléfono con lada ──────────────────────────────────────────────── class _PhoneField extends StatelessWidget { final TextEditingController controller; const _PhoneField({required this.controller}); static const _ladas = [(flag: '🇲🇽', code: '+52', name: 'México')]; @override Widget build(BuildContext context) { final lada = _ladas.first; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Teléfono', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.textSecondary, ), ), const SizedBox(height: 6), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 50, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: AppTheme.background, borderRadius: BorderRadius.circular(AppTheme.radiusSm), border: Border.all(color: AppTheme.border), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(lada.flag, style: const TextStyle(fontSize: 20)), const SizedBox(width: 6), Text( lada.code, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppTheme.textPrimary, ), ), ], ), ), const SizedBox(width: 8), Expanded( child: TextFormField( controller: controller, keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), _PhoneInputFormatter(), ], style: const TextStyle( fontSize: 14, color: AppTheme.textPrimary, ), decoration: InputDecoration( hintText: '000-000-0000', hintStyle: const TextStyle( color: AppTheme.textSecondary, fontSize: 14, ), filled: true, fillColor: AppTheme.background, contentPadding: const EdgeInsets.symmetric( horizontal: 14, vertical: 15, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusSm), borderSide: const BorderSide(color: AppTheme.border), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusSm), borderSide: const BorderSide(color: AppTheme.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusSm), borderSide: const BorderSide( color: AppTheme.primary, width: 1.5, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusSm), borderSide: const BorderSide(color: AppTheme.danger), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusSm), borderSide: const BorderSide( color: AppTheme.danger, width: 1.5, ), ), ), validator: (v) { if (v == null || v.isEmpty) return null; final digits = v.replaceAll('-', ''); if (digits.length != 10) return 'Ingresa exactamente 10 dígitos'; return null; }, ), ), ], ), ], ); } } class _PhoneInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { final digits = newValue.text.replaceAll(RegExp(r'\D'), ''); final String formatted; if (digits.length <= 3) { formatted = digits; } else if (digits.length <= 6) { formatted = '${digits.substring(0, 3)}-${digits.substring(3)}'; } else { formatted = '${digits.substring(0, 3)}-${digits.substring(3, 6)}-${digits.substring(6)}'; } return TextEditingValue( text: formatted, selection: TextSelection.collapsed(offset: formatted.length), ); } } // ── Opción radio ────────────────────────────────────────────────────────────── class _RadioOption extends StatelessWidget { final int value, groupValue; final String label, sublabel; final ValueChanged onChanged; const _RadioOption({ required this.value, required this.groupValue, required this.label, required this.sublabel, required this.onChanged, }); @override Widget build(BuildContext context) { final selected = value == groupValue; return GestureDetector( onTap: () => onChanged(value), child: Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), decoration: BoxDecoration( color: selected ? AppTheme.primaryLight : AppTheme.background, borderRadius: BorderRadius.circular(AppTheme.radiusSm), border: Border.all( color: selected ? AppTheme.primary : AppTheme.border, width: selected ? 1.5 : 0.5, ), ), child: Row( children: [ Container( width: 18, height: 18, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: selected ? AppTheme.primary : AppTheme.border, width: 2, ), ), child: selected ? Center( child: Container( width: 8, height: 8, decoration: const BoxDecoration( shape: BoxShape.circle, color: AppTheme.primary, ), ), ) : null, ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: selected ? AppTheme.primaryDark : AppTheme.textPrimary, ), ), Text( sublabel, style: TextStyle( fontSize: 11, color: selected ? AppTheme.primary : AppTheme.textSecondary, ), ), ], ), ], ), ), ); } }