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 _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(); _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); } // Llama a la API de OpenStreetMap (Nominatim) para obtener la calle automáticamente 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(); // Cierra el teclado } Future _register() async { if (_calleCtrl.text.trim().isEmpty || _selectedColonia == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Ingresa tu calle y selecciona una colonia'), behavior: SnackBarBehavior.floating, ), ); return; } final phoneDigits = _telefonoCtrl.text.replaceAll(RegExp(r'\D'), ''); final phone = phoneDigits.isNotEmpty ? '+52$phoneDigits' : ''; final calle = _calleCtrl.text.trim(); final colonia = _selectedColonia!.nombre; final lat = _selectedLocation?.latitude; final lng = _selectedLocation?.longitude; try { await ref .read(authControllerProvider.notifier) .register( email: _emailCtrl.text.trim(), phone: phone, password: _passCtrl.text, addressCalle: calle, addressColonia: colonia, addressLabel: 'Mi Casa', addressLat: lat, addressLng: lng, ); // Guardado silencioso de la dirección tras un registro exitoso _postAddressInBackground(calle, colonia, lat, lng); } catch (_) { // El error ya es manejado por el listener y muestra el SnackBar } } Future _postAddressInBackground( String calle, String colonia, double? lat, double? lng, ) async { try { const storage = FlutterSecureStorage(); await Future.delayed( const Duration(milliseconds: 800), ); // Esperar a que se guarde el JWT final token = await storage.read(key: authTokenStorageKey) ?? ''; if (token.isNotEmpty) { final dio = Dio( BaseOptions( baseUrl: const String.fromEnvironment( 'API_BASE_URL', defaultValue: 'http://localhost:8000', ), headers: {'Authorization': 'Bearer $token'}, ), ); await dio.post( '/addresses', data: {'label': 'Mi Casa', 'calle': calle, 'colonia': colonia}, ); } } catch (e) { debugPrint('Aviso: No se pudo crear la dirección: $e'); } } @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, appBar: AppBar( backgroundColor: Colors.transparent, elevation: 0, iconTheme: const IconThemeData(color: AppTheme.textPrimary), title: Text( _currentPage == 0 ? 'Crear cuenta' : 'Mi dirección', style: const TextStyle(color: AppTheme.textPrimary, fontSize: 16), ), bottom: PreferredSize( preferredSize: const Size.fromHeight(8), child: _StepIndicator(current: _currentPage, total: 2), ), ), body: PageView( controller: _pageController, physics: const NeverScrollableScrollPhysics(), children: [ _Step1( formKey: _step1FormKey, emailCtrl: _emailCtrl, telefonoCtrl: _telefonoCtrl, passCtrl: _passCtrl, obscurePass: _obscurePass, onTogglePass: () => setState(() => _obscurePass = !_obscurePass), onNext: _nextPage, ), _Step2( mapController: _mapController, cpCtrl: _cpCtrl, calleCtrl: _calleCtrl, selectedColonia: _selectedColonia, selectedLocation: _selectedLocation, tipoInmueble: _tipoInmueble, whatsappNotif: _whatsappNotif, loading: loading, onTipoChanged: (v) => setState(() => _tipoInmueble = v), onCPChanged: (v) => _validarCP(v, coloniasList), onLocationChanged: _fetchStreetName, onWhatsappChanged: (v) => setState(() => _whatsappNotif = v ?? false), onRegister: _register, ), ], ), ); } } // ── Indicador de pasos ──────────────────────────────────────────────────────── class _StepIndicator extends StatelessWidget { final int current; final int total; const _StepIndicator({required this.current, required this.total}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 6), child: Row( children: List.generate(total, (i) { final active = i <= current; return Expanded( child: Container( margin: EdgeInsets.only(right: i < total - 1 ? 6 : 0), height: 4, decoration: BoxDecoration( color: active ? AppTheme.primary : AppTheme.border, borderRadius: BorderRadius.circular(4), ), ), ); }), ), ); } } // ── Paso 1: Cuenta ──────────────────────────────────────────────────────────── class _Step1 extends StatelessWidget { final GlobalKey formKey; final TextEditingController emailCtrl, telefonoCtrl, passCtrl; final bool obscurePass; final VoidCallback onTogglePass; final VoidCallback onNext; const _Step1({ required this.formKey, required this.emailCtrl, required this.telefonoCtrl, required this.passCtrl, required this.obscurePass, required this.onTogglePass, required this.onNext, }); @override Widget build(BuildContext context) { return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Form( key: formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), AppFormCard( icon: Icons.person_outline, title: 'Información de cuenta', child: Column( children: [ AppFormField( label: 'Correo electrónico', hint: 'tu@correo.com', controller: emailCtrl, 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: onTogglePass, ), ), ], ), ), const SizedBox(height: 28), SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: onNext, child: const Row( mainAxisSize: MainAxisSize.min, children: [ Text('Siguiente'), SizedBox(width: 8), Icon(Icons.arrow_forward, size: 18), ], ), ), ), 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, ), ), ), ], ), ), ], ), ), ); } } // ── Paso 2: Dirección ───────────────────────────────────────────────────────── class _Step2 extends StatelessWidget { final MapController mapController; final TextEditingController cpCtrl; final TextEditingController calleCtrl; final Colonia? selectedColonia; final LatLng? selectedLocation; final String tipoInmueble; final bool whatsappNotif; final bool loading; final ValueChanged onTipoChanged; final ValueChanged onCPChanged; final ValueChanged onLocationChanged; final ValueChanged onWhatsappChanged; final VoidCallback onRegister; const _Step2({ required this.mapController, required this.cpCtrl, required this.calleCtrl, required this.selectedColonia, required this.selectedLocation, required this.tipoInmueble, required this.whatsappNotif, required this.loading, required this.onTipoChanged, required this.onCPChanged, required this.onLocationChanged, required this.onWhatsappChanged, required this.onRegister, }); @override Widget build(BuildContext context) { // Usamos el centro original de la colonia para los límites estáticos final baseCenter = selectedColonia != null ? kColoniasCoordinates[selectedColonia!.nombre] ?? const LatLng(20.5222, -100.8123) : const LatLng(20.5222, -100.8123); final mapCenter = selectedLocation ?? baseCenter; return SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), 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) => onTipoChanged(v!), ), ), ), Expanded( child: Material( color: Colors.transparent, child: RadioListTile( title: const Text( 'Negocio', style: TextStyle(fontSize: 14), ), value: 'Negocio', groupValue: tipoInmueble, onChanged: (v) => onTipoChanged(v!), ), ), ), ], ), const SizedBox(height: 8), AppFormField( label: 'Código Postal', hint: 'Ej. 38000', controller: cpCtrl, keyboardType: TextInputType.number, onChanged: onCPChanged, ), 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: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(AppTheme.radiusSm), border: Border.all(color: AppTheme.border), ), clipBehavior: Clip.hardEdge, child: FlutterMap( mapController: mapController, options: MapOptions( initialCenter: mapCenter, initialZoom: 15.0, onTap: (_, latlng) => onLocationChanged(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), // ── Sección OCR (Privacidad por diseño) ── 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), ), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Abriendo cámara... (Próximamente)'), ), ); }, ), ), ], ), ), const SizedBox(height: 16), // ── Sección WhatsApp ── AppFormCard( icon: Icons.chat_outlined, title: 'Notificaciones Externas', child: Column( children: [ Material( color: Colors.transparent, child: CheckboxListTile( contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, activeColor: AppTheme.primary, value: whatsappNotif, onChanged: onWhatsappChanged, title: const Text( 'Recibir alertas del camión vía WhatsApp (Próximamente)', style: TextStyle( fontSize: 14, color: AppTheme.textPrimary, ), ), ), ), ], ), ), const SizedBox(height: 28), SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: loading ? null : onRegister, child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: loading ? const SizedBox( key: ValueKey('loading'), width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Row( key: ValueKey('text'), mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.check, size: 18), SizedBox(width: 8), Flexible( child: Text('Registrarme', overflow: TextOverflow.ellipsis), ), ], ), ), ), ), 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, ), ), ), ], ), ); } } // ── Campo de teléfono con lada ──────────────────────────────────────────────── // Muestra +52 🇲🇽 fijo (escalable a selector multi-país en el futuro). // Formatea la entrada como 000-000-0000 y valida exactamente 10 dígitos. class _PhoneField extends StatelessWidget { final TextEditingController controller; const _PhoneField({required this.controller}); // Países disponibles (lista para escalamiento futuro) 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: [ // Selector de lada (por ahora solo +52) 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), // Número (solo dígitos, formato 000-000-0000) 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; // opcional final digits = v.replaceAll('-', ''); if (digits.length != 10) return 'Ingresa exactamente 10 dígitos'; return null; }, ), ), ], ), ], ); } } // Formatea dígitos en tiempo real: 4611234567 → 461-123-4567 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, ), ), ], ), ], ), ), ); } }