From 649752a6a4cd74bc0c0253fcfdc83a892da05c62 Mon Sep 17 00:00:00 2001 From: 25030248hasel Date: Sat, 23 May 2026 08:22:39 -0600 Subject: [PATCH] feat: creacion de interfaz xon simulacion de rutas --- .vscode/settings.json | 12 + .../screens/home_screen_placeholder.dart | 713 +++++------------- .../presentation/screens/login_screen.dart | 17 +- .../presentation/screens/register_screen.dart | 12 +- 4 files changed, 236 insertions(+), 518 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2fff7ef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "yaml.customTags": [ + "!upload scalar", + "!remove scalar", + "!keep scalar", + "!erase scalar", + "!jwt scalar" + ], + "yaml.schemas": { + "https://raw.githubusercontent.com/doanthuanthanh88/testapi6/main/schema.json": "*.yaml" + } +} \ No newline at end of file diff --git a/lib/features/auth/presentation/screens/home_screen_placeholder.dart b/lib/features/auth/presentation/screens/home_screen_placeholder.dart index 5ee44a3..1e62f87 100644 --- a/lib/features/auth/presentation/screens/home_screen_placeholder.dart +++ b/lib/features/auth/presentation/screens/home_screen_placeholder.dart @@ -1,533 +1,238 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; -import '../../../../core/theme/app_theme.dart'; -import '../bloc/auth_bloc.dart'; -import '../bloc/auth_event.dart'; -import '../bloc/auth_state.dart'; - -/// Pantalla principal post-login — MVP WasteNotify. -/// -/// Cascarón de la pantalla de inicio. En fases futuras contendrá: -/// - ETA de llegada del camión (sin mapa, solo tiempo estimado) -/// - Notificaciones programadas -/// - Historial de recolecciones -/// - Panel de operador (si role == 'operator') -/// -/// RESTRICCIÓN DE PRIVACIDAD: Esta pantalla NO mostrará mapas de rutas -/// ni la posición GPS del vehículo. Solo tiempo estimado de llegada. -class HomeScreenPlaceholder extends StatelessWidget { +class HomeScreenPlaceholder extends StatefulWidget { const HomeScreenPlaceholder({super.key}); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final user = state is AuthAuthenticated ? state.user : null; - final isOperator = user?.role == 'operator'; + State createState() => _HomeScreenPlaceholderState(); +} - return Scaffold( - backgroundColor: AppTheme.warmWhite, - appBar: AppBar( - title: Row( - children: [ - const Icon(Icons.recycling_rounded, - color: AppTheme.leafGreen, size: 22), - const SizedBox(width: 8), - RichText( - text: const TextSpan( - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: AppTheme.charcoal, - ), - children: [ - TextSpan(text: 'Waste'), - TextSpan( - text: 'Notify', - style: TextStyle(color: AppTheme.leafGreen), - ), - ], - ), - ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Icons.logout_rounded), - tooltip: 'Cerrar sesión', - onPressed: () { - context.read().add(const AuthLogoutRequested()); - }, - ), - ], - ), - body: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.all(20), - sliver: SliverList( - delegate: SliverChildListDelegate([ - // --- Bienvenida --- - _WelcomeCard(user: user), - const SizedBox(height: 20), +class _HomeScreenPlaceholderState extends State { + // 1. Catálogo de Colonias tipado fuertemente con tu nuevo modelo (Mapeo de tu JSON) + final List _coloniasData = [ + ColoniaModel(colonia: "Zona Centro", routeId: "RUTA-01", horarioEstimado: "Matutino (06:30 - 07:15)"), + ColoniaModel(colonia: "Las Arboledas", routeId: "RUTA-01", horarioEstimado: "Matutino (07:00 - 07:30)"), + ColoniaModel(colonia: "Trojes", routeId: "RUTA-13", horarioEstimado: "Matutino (06:40 - 07:10)"), + ColoniaModel(colonia: "San Juanico", routeId: "RUTA-03", horarioEstimado: "Matutino (06:45 - 07:15)"), + ColoniaModel(colonia: "Los Olivos", routeId: "RUTA-04", horarioEstimado: "Matutino (07:00 - 07:40)"), + ColoniaModel(colonia: "Rancho Seco", routeId: "RUTA-05", horarioEstimado: "Vespertino (14:15 - 15:00)"), + ColoniaModel(colonia: "Las Insurgentes", routeId: "RUTA-12", horarioEstimado: "Matutino (06:35 - 07:10)") + ]; - // --- ETA Principal (cascarón) --- - const _EtaCard(), - const SizedBox(height: 20), + // 2. Diccionario de Telemetría Satelital de Celaya (Mapeo de tus imágenes) + final Map> _routesTelemetry = { + "RUTA-01": { + "name": "Zona Centro - Las Arboledas", + "truckId": 101, + "positions": [ + {"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Salida Relleno Sanitario", "mins": "45"}, + {"positionId": 2, "lat": 20.5185, "lng": -100.8450, "speed": 45, "desc": "En trayecto principal", "mins": "30"}, + {"positionId": 3, "lat": 20.5215, "lng": -100.8142, "speed": 22, "desc": "Ingresando a zona Centro", "mins": "20"}, + {"positionId": 4, "lat": 20.5212, "lng": -100.8175, "speed": 15, "desc": "Punto Previo Destino (<15 min)", "mins": "12"}, + {"positionId": 5, "lat": 20.5210, "lng": -100.8210, "speed": 0, "desc": "Recolección Activa de Residuos", "mins": "5"}, + {"positionId": 6, "lat": 20.5235, "lng": -100.8212, "speed": 18, "desc": "Avanzando sector Arboledas", "mins": "3"}, + {"positionId": 7, "lat": 20.5260, "lng": -100.8215, "speed": 20, "desc": "Última parada del circuito", "mins": "1"}, + {"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 40, "desc": "Retorno al Basurero Municipal", "mins": "0"} + ] + }, + "RUTA-03": { + "name": "Sector Poniente - San Juanico", + "truckId": 103, + "positions": [ + {"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Salida Base de Monitoreo", "mins": "40"}, + {"positionId": 2, "lat": 20.5250, "lng": -100.8510, "speed": 42, "desc": "Vía Rápida Poniente", "mins": "25"}, + {"positionId": 3, "lat": 20.5290, "lng": -100.8320, "speed": 20, "desc": "Eje Norponiente", "mins": "18"}, + {"positionId": 4, "lat": 20.5315, "lng": -100.8355, "speed": 15, "desc": "Avenida San Juanico", "mins": "10"}, + {"positionId": 5, "lat": 20.5340, "lng": -100.8390, "speed": 0, "desc": "Vaciado de Contenedores Urbano", "mins": "6"}, + {"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 35, "desc": "Retorno General", "mins": "0"} + ] + }, + "RUTA-04": { + "name": "Oriente - Los Olivos", + "truckId": 104, + "positions": [ + {"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Encendido de Unidad", "mins": "50"}, + {"positionId": 4, "lat": 20.5320, "lng": -100.7850, "speed": 12, "desc": "Proximidad Los Olivos", "mins": "14"}, + {"positionId": 5, "lat": 20.5350, "lng": -100.7790, "speed": 0, "desc": "Recolección Casa por Casa", "mins": "8"}, + {"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 48, "desc": "Retorno a Relleno Sanitario", "mins": "0"} + ] + } + }; - // --- Mensaje preventivo --- - const _PreventiveMessageCard(), - const SizedBox(height: 20), + int _activePositionIndex = 0; + + // 📍 Historial tipado de forma segura con el modelo de notificaciones push + final List _pushNotificationsLog = []; + + String _userColonia = "Zona Centro"; + bool _isInitialized = false; - // --- Próximas funcionalidades --- - _UpcomingFeatures(isOperator: isOperator), - const SizedBox(height: 20), + @override + void initState() { + super.initState(); + _userColonia = "Zona Centro"; // Forzado de respaldo seguro + _isInitialized = true; + + WidgetsBinding.instance.addPostFrameCallback((_) { + _triggerNotificationCheck(); + }); + } - // --- Info de sesión (debug MVP) --- - if (user != null) _SessionDebugCard(user: user), - ]), - ), - ), - ], - ), + @override + void didChangeDependencies() { + super.didChangeDependencies(); + try { + final uri = GoRouterState.of(context).uri; + final String? coloniaParam = uri.queryParameters['colonia']; + + if (coloniaParam != null && + coloniaParam.isNotEmpty && + _coloniasData.any((e) => e.colonia.toLowerCase() == coloniaParam.toLowerCase())) { + + final elementoEncontrado = _coloniasData.firstWhere( + (e) => e.colonia.toLowerCase() == coloniaParam.toLowerCase() ); - }, - ); + setState(() { + _userColonia = elementoEncontrado.colonia; + }); + } + } catch (e) { + // Manejo silencioso de GoRouter + } } -} -// --------------------------------------------------------------------------- -// Sub-widgets de la pantalla home -// --------------------------------------------------------------------------- + // Lógica interactiva que dispara las notificaciones basadas en tu modelo real + void _triggerNotificationCheck() { + final String currentRouteId = _getColoniaInfo().routeId; + if (!_routesTelemetry.containsKey(currentRouteId)) return; -class _WelcomeCard extends StatelessWidget { - final dynamic user; + final telemetry = _routesTelemetry[currentRouteId]!; + final currentPos = (telemetry["positions"] as List)[_activePositionIndex]; + final int pId = currentPos["positionId"]; - const _WelcomeCard({required this.user}); + NotificationModel? newAlert; - @override - Widget build(BuildContext context) { - final roleLabel = user?.role == 'operator' ? 'Operador' : 'Ciudadano'; - final identifier = user?.email ?? ''; + if (pId == 2) { + newAlert = NotificationModel( + triggerEvent: "ROUTE_START", + condition: "Cuando positionId cambia de 1 a 2", + pushPayload: PushPayloadModel( + title: "¡Ruta Iniciada!", + body: "El camión recolector ha salido del Relleno Sanitario rumbo a tu sector. Asegúrate de tener listos tus residuos." + ) + ); + } else if (pId == 4) { + newAlert = NotificationModel( + triggerEvent: "TRUCK_PROXIMITY", + condition: "Cuando positionId llega a 4 (punto previo al destino)", + pushPayload: PushPayloadModel( + title: "Camión Cercano", + body: "El camión está a menos de 15 minutos de tu domicilio. Es momento de sacar tus bolsas a la acera." + ) + ); + } else if (pId == 8) { + newAlert = NotificationModel( + triggerEvent: "ROUTE_COMPLETED", + condition: "Cuando positionId llega a 8 (retorno al basurero)", + pushPayload: PushPayloadModel( + title: "Servicio Finalizado", + body: "El camión de tu sector ha concluido su jornada de recolección diaria." + ) + ); + } - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [AppTheme.leafGreen, AppTheme.forestGreen], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + if (newAlert != null) { + setState(() { + _pushNotificationsLog.insert(0, newAlert!); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("🔔 Push: \${newAlert.pushPayload.title}"), + backgroundColor: pId == 4 ? Colors.amber.shade900 : (pId == 8 ? Colors.blue.shade700 : Colors.green.shade700), + duration: const Duration(seconds: 2), ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: AppTheme.leafGreen.withValues(alpha: 0.3), - blurRadius: 16, - offset: const Offset(0, 6), - ), - ], - ), - child: Row( - children: [ - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(13), - ), - child: const Icon( - Icons.person_rounded, - color: Colors.white, - size: 30, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '¡Bienvenido!', - style: const TextStyle( - color: Colors.white70, - fontSize: 13, - ), - ), - Text( - identifier, - style: const TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.w700, - ), - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 3), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - roleLabel, - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ], - ), + ); + } + } + + ColoniaModel _getColoniaInfo() { + return _coloniasData.firstWhere( + (element) => element.colonia == _userColonia, + orElse: () => _coloniasData.first, ); } -} -class _EtaCard extends StatelessWidget { - const _EtaCard(); + void _nextSimulationStep() { + final String currentRouteId = _getColoniaInfo().routeId; + if (!_routesTelemetry.containsKey(currentRouteId)) return; + + final positionsList = _routesTelemetry[currentRouteId]!["positions"] as List; + if (_activePositionIndex < positionsList.length - 1) { + setState(() { + _activePositionIndex++; + }); + _triggerNotificationCheck(); + } + } @override Widget build(BuildContext context) { - return Card( - child: Padding( - padding: const EdgeInsets.all(20), + final coloniaInfo = _getColoniaInfo(); + final String routeId = coloniaInfo.routeId; + + final telemetry = _routesTelemetry[routeId]!; + final currentPositionData = (telemetry["positions"] as List)[_activePositionIndex]; + + return Scaffold( + backgroundColor: const Color(0xFFF5F5F5), + appBar: AppBar( + title: Row( + children: const [ + Icon(Icons.recycling, color: Color(0xFF2E7D32)), + SizedBox(width: 8), + Text('WasteNotify', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.black)), + ], + ), + backgroundColor: Colors.white, + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.logout, color: Colors.black), + onPressed: () => context.go('/login'), + ) + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppTheme.lightMint, - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.schedule_rounded, - color: AppTheme.leafGreen, - size: 22, - ), - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'Tiempo estimado de llegada', - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w700, - color: AppTheme.charcoal, - ), - ), - ), - ], - ), - const SizedBox(height: 20), - Center( - child: Column( + // --- 1. TARJETA DE BIENVENIDA --- + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF1B5E20), + borderRadius: BorderRadius.circular(20), + ), + child: Row( children: [ - Container( - width: 110, - height: 110, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: AppTheme.lightMint, - width: 6, - ), - color: Colors.white, - ), - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.hourglass_empty_rounded, - color: AppTheme.midGray, - size: 28, - ), - SizedBox(height: 4), - Text( - '— min', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.w800, - color: AppTheme.midGray, - ), - ), - ], - ), + const CircleAvatar( + backgroundColor: Colors.white24, + radius: 24, + child: Icon(Icons.person, color: Colors.white, size: 28), ), - const SizedBox(height: 14), - Text( - 'Notificaciones activas próximamente', - style: TextStyle( - fontSize: 13, - color: AppTheme.midGray, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 5), - decoration: BoxDecoration( - color: AppTheme.lightMint, - borderRadius: BorderRadius.circular(20), - ), - child: const Text( - 'Fase 2 — En desarrollo', - style: TextStyle( - fontSize: 11, - color: AppTheme.leafGreen, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ], + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('¡Bienvenido!', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)),child: const Text('Ciudadano', style: TextStyle(color: Colors.white, fontSize: 12)),)],)],),),const SizedBox(height: 16),// --- 2. RELOJ DE TIEMPO ESTIMADO INTERACTIVO ---Card(color: Colors.white,shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),elevation: 1,child: Padding(padding: const EdgeInsets.all(20.0),child: Column(children: [Row(children: const [Icon(Icons.access_time, color: Color(0xFF2E7D32)),SizedBox(width: 10),Text('Tiempo estimado de llegada', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),],),const SizedBox(height: 16),Container(width: 110,height: 110,decoration: BoxDecoration(shape: BoxShape.circle,border: Border.all(color: const Color(0xFFE0E0E0), width: 3),),child: Column(mainAxisAlignment: MainAxisAlignment.center,children: [Text('${currentPositionData["mins"]}',style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black),),Text('— min',style: TextStyle(fontSize: 14, color: Colors.grey.shade600),)],),),const SizedBox(height: 12),Text('Estado GPS: ${currentPositionData["desc"]}',textAlign: TextAlign.center,style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontStyle: FontStyle.italic),),],),),),const SizedBox(height: 16),// --- 3. BOTÓN DE SIMULACIÓN PARA EL MVP ---ElevatedButton.icon(style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF2E7D32),padding: const EdgeInsets.symmetric(vertical: 14),shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),),onPressed: _activePositionIndex < (telemetry["positions"] as List).length - 1 ? _nextSimulationStep : null,icon: const Icon(Icons.play_arrow, color: Colors.white),label: const Text('Simular Avance del Camión (GPS)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),),const SizedBox(height: 16),// --- 4. BANDEJA DE ALERTAS REALES RECIBIDAS (PARSED) ---if (_pushNotificationsLog.isNotEmpty) ...[const Text('🔔 Alertas Push en Vivo', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black)),const SizedBox(height: 8),..._pushNotificationsLog.map((log) {final bool isWarning = log.triggerEvent == "TRUCK_PROXIMITY";final bool isDone = log.triggerEvent == "ROUTE_COMPLETED";final Color cardColor = isWarning ? Colors.amber.shade900 : (isDone ? Colors.blue.shade700 : Colors.green.shade700);return Card(margin: const EdgeInsets.symmetric(vertical: 4),shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),child: ListTile(leading: CircleAvatar(backgroundColor: cardColor.withOpacity(0.12),child: Icon(isWarning ? Icons.notification_important : (isDone ? Icons.check_circle : Icons.local_shipping),color: cardColor)),title: Text(log.pushPayload.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),subtitle: Text(log.pushPayload.body, style: const TextStyle(fontSize: 12)),),);}),const SizedBox(height: 16),],// --- 5. PANEL DE RECOMENDACIONES ---Container(padding: const EdgeInsets.all(16),decoration: BoxDecoration(color: const Color(0xFFFFF3E0),borderRadius: BorderRadius.circular(16),border: Border.all(color: const Color(0xFFFFE0B2)),),child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Row(children: const [Icon(Icons.campaign, color: Colors.orange, size: 22),SizedBox(width: 8),Text('Recuerda siempre', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.brown, fontSize: 14)),],),const SizedBox(height: 12),_buildBulletRow(Icons.delete_outline, 'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".'),const SizedBox(height: 8),_buildBulletRow(Icons.block, 'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.'),const SizedBox(height: 8),_buildBulletRow(Icons.eco_outlined, 'Separar tus residuos hace más eficiente la recolección. ¡Gracias!'),],),),const SizedBox(height: 20),],),),);}Widget _buildBulletRow(IconData icon, String text) {return Row(crossAxisAlignment: CrossAxisAlignment.start,children: [Icon(icon, size: 16, color: Colors.brown.shade700),const SizedBox(width: 10),Expanded(child: Text(text, style: TextStyle(fontSize: 13, color: Colors.brown.shade900, height: 1.2))),],);}}// =========================================================================// 📍 MODELO 1: Catálogo de Colonias (Renombrado para evitar duplicados)// =========================================================================class ColoniaModel {String colonia;String routeId;String horarioEstimado;ColoniaModel({required this.colonia,required this.routeId,required this.horarioEstimado,});factory ColoniaModel.fromJson(Map json) => ColoniaModel(colonia: json["colonia"],routeId: json["routeId"],horarioEstimado: json["horarioEstimado"],);Map toJson() => {"colonia": colonia,"routeId": routeId,"horarioEstimado": horarioEstimado,};}// =========================================================================// 📍 MODELO 2: Sistema de Notificaciones Alertas Push// =========================================================================class NotificationModel {String triggerEvent;String condition;PushPayloadModel pushPayload;NotificationModel({required this.triggerEvent,required this.condition,required this.pushPayload,});factory NotificationModel.fromJson(Map json) => NotificationModel(triggerEvent: json["triggerEvent"],condition: json["condition"],pushPayload: PushPayloadModel.fromJson(json["pushPayload"]),);Map toJson() => {"triggerEvent": triggerEvent,"condition": condition,"pushPayload": pushPayload.toJson(),};}class PushPayloadModel {String title;String body;PushPayloadModel({required this.title,required this.body,});factory PushPayloadModel.fromJson(Map json) => PushPayloadModel(title: json["title"],body: json["body"],);Map toJson() => {"title": title,"body": body,};} + ]) ), - ), - ); - } -} - -class _PreventiveMessageCard extends StatelessWidget { - const _PreventiveMessageCard(); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.alertAmber.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(14), - border: Border.all( - color: AppTheme.alertAmber.withValues(alpha: 0.3), - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.campaign_outlined, - color: AppTheme.alertAmber, size: 18), - SizedBox(width: 8), - Text( - 'Recuerda siempre', - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 13, - color: AppTheme.earthBrown, - ), - ), - ], - ), - const SizedBox(height: 10), - _ReminderItem( - '🚮', - 'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".', - ), - _ReminderItem( - '🚫', - 'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.', - ), - _ReminderItem( - '🌱', - 'Separar tus residuos hace más eficiente la recolección. ¡Gracias!', - ), - ], - ), - ); - } -} - -class _ReminderItem extends StatelessWidget { - final String emoji; - final String text; - - const _ReminderItem(this.emoji, this.text); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(emoji, style: const TextStyle(fontSize: 14)), - const SizedBox(width: 8), - Expanded( - child: Text( - text, - style: const TextStyle( - fontSize: 12.5, - color: AppTheme.earthBrown, - height: 1.4, - ), - ), - ), - ], - ), - ); - } -} - -class _UpcomingFeatures extends StatelessWidget { - final bool isOperator; - - const _UpcomingFeatures({required this.isOperator}); - - @override - Widget build(BuildContext context) { - final features = [ - (Icons.notifications_active_outlined, 'Alertas push de recolección', - 'Fase 2'), - (Icons.history_rounded, 'Historial de notificaciones', 'Fase 2'), - (Icons.settings_outlined, 'Configurar zona y horario', 'Fase 3'), - if (isOperator) ...[ - (Icons.bar_chart_rounded, 'Panel de rutas completadas', 'Fase 3'), - (Icons.group_outlined, 'Gestión de sectores', 'Fase 4'), - ], - ]; - - return Card( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Próximamente en WasteNotify', - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: AppTheme.charcoal, - ), - ), - const SizedBox(height: 12), - ...features.map((f) => _FeatureRow( - icon: f.$1, - label: f.$2, - phase: f.$3, - )), - ], - ), - ), - ); - } -} - -class _FeatureRow extends StatelessWidget { - final IconData icon; - final String label; - final String phase; - - const _FeatureRow({ - required this.icon, - required this.label, - required this.phase, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Row( - children: [ - Icon(icon, size: 18, color: AppTheme.mintGreen), - const SizedBox(width: 12), - Expanded( - child: Text( - label, - style: const TextStyle(fontSize: 13, color: AppTheme.charcoal), - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: AppTheme.lightGray, - borderRadius: BorderRadius.circular(20), - ), - child: Text( - phase, - style: const TextStyle( - fontSize: 10, - color: AppTheme.midGray, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ); - } -} - -class _SessionDebugCard extends StatelessWidget { - final dynamic user; - - const _SessionDebugCard({required this.user}); - - @override - Widget build(BuildContext context) { - final token = user?.token ?? ''; - final tokenPreview = - token.length > 40 ? '${token.substring(0, 40)}…' : token; - - return Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: const Color(0xFFF3E5F5), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: const Color(0xFFCE93D8), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Row( - children: [ - Icon(Icons.developer_mode_rounded, - size: 14, color: Color(0xFF7B1FA2)), - SizedBox(width: 6), - Text( - 'Debug — Sesión JWT (solo MVP)', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: Color(0xFF7B1FA2), - ), - ), - ], - ), - const SizedBox(height: 6), - Text( - 'Token: $tokenPreview', - style: const TextStyle( - fontSize: 10.5, - fontFamily: 'monospace', - color: Color(0xFF4A148C), - ), - ), - Text( - 'Role: ${user?.role} | Expira: ${user?.expiresAt?.toLocal().toString().substring(0, 16)}', - style: const TextStyle( - fontSize: 10.5, - fontFamily: 'monospace', - color: Color(0xFF4A148C), - ), - ), - ], - ), - ); - } -} + ); + } +} \ No newline at end of file diff --git a/lib/features/auth/presentation/screens/login_screen.dart b/lib/features/auth/presentation/screens/login_screen.dart index 5ea0ad6..7f27c33 100644 --- a/lib/features/auth/presentation/screens/login_screen.dart +++ b/lib/features/auth/presentation/screens/login_screen.dart @@ -63,13 +63,12 @@ class _LoginScreenState extends State ); // 1. Obtener la conexión por el cable USB Mapped + // 1. Obtener la conexión final conn = await MySqlService().getConnection(); - // 2. Ejecutar la búsqueda con los nombres exactos de tus columnas - - // 2. Modificamos el SELECT para buscar SOLO por email + // 2. Modificamos el SELECT para traer también la columna 'colonia' final result = await conn.execute( - "SELECT email, contrasena_hash, rol FROM usuarios WHERE email = :email", + "SELECT email, contrasena_hash, rol, colonia FROM usuarios WHERE email = :email", { "email": _identifierController.text.trim(), }, @@ -82,20 +81,18 @@ class _LoginScreenState extends State final contrasenaEnBd = usuarioEncontrado['contrasena_hash']; final email = usuarioEncontrado['email']; final rol = usuarioEncontrado['rol']; + final colonia = usuarioEncontrado[ + 'colonia']; // 📍 Extraemos la colonia real de la BD - // 3. Validamos la contraseña directamente en Flutter comparando textos limpios if (contrasenaEnBd?.trim() == _passwordController.text.trim()) { - if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("¡Bienvenido de nuevo, $email ($rol)!")), ); - // 📍 CORRECCIÓN DEFINITIVA: Usamos GoRouter nativo en lugar de Navigator - // Esto elimina por completo el conflicto de aserción en el árbol de widgets - context.go('/home?colonia=Centro'); - + // 📍 Enviamos la colonia real extraída de MySQL directamente a la URL del Home + context.go('/home?colonia=$colonia'); } else { // Contraseña mal mapeada en la BD ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/auth/presentation/screens/register_screen.dart b/lib/features/auth/presentation/screens/register_screen.dart index 0a55b4e..3b8ec0c 100644 --- a/lib/features/auth/presentation/screens/register_screen.dart +++ b/lib/features/auth/presentation/screens/register_screen.dart @@ -1,3 +1,4 @@ +import 'dart:math'; // import '../../../../core/network/mysql_service.dart'; // 📍 Ajusta las carpetas '../' según tu proyecto import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -129,15 +130,18 @@ class _RegisterScreenState extends State { '=== DEPURACIÓN: ¡Conexión obtenida! Estado connected: ${conn.connected}'); print('=== DEPURACIÓN: Intentando ejecutar INSERT ==='); + final randomPhone = + '461${1000000 + Random().nextInt(9000000)}'; // 📍 ENVIANDO LA COLONIA REAL A TU NUEVA COLUMNA DE MYSQL final result = await conn.execute( - "INSERT INTO usuarios (email, telefono, contrasena_hash, rol, is_verified) VALUES (:email, :telefono, :contrasena_hash, :rol, :is_verified)", + "INSERT INTO usuarios (email, telefono, contrasena_hash, rol, is_verified, colonia) VALUES (:email, :telefono, :contrasena_hash, :rol, :is_verified, :colonia)", { "email": _emailController.text.trim(), - "telefono": "4611234567", + "telefono": randomPhone, // 📍 Genera un teléfono simulado para cumplir con el esquema de tu tabla "contrasena_hash": _passwordController.text, - "rol": - "Ciudadano", // 📍 CORRECCIÓN: Cambia "ciudadano" por "Ciudadano" (con mayúscula) + "rol": "Ciudadano", "is_verified": 1, + "colonia": + _selectedColonia, // 📍 Envía la colonia seleccionada en el Dropdown }, );