From 476a6e08e4c0e3c1f2a314ce7b13a410d33867db Mon Sep 17 00:00:00 2001 From: Kimberly Date: Sat, 23 May 2026 09:30:42 -0600 Subject: [PATCH] modificaciones en el sistema --- lib/configuraciondomicilio.dart | 139 +++++-- lib/domicilios.dart | 454 ++++++++++++++++++----- lib/guia_separacion.dart | 359 ++++++++++++++++++ lib/login.dart | 636 ++++++++++++++++++++++---------- lib/ruta_exclusiva.dart | 398 ++++++++++++++++++++ 5 files changed, 1690 insertions(+), 296 deletions(-) create mode 100644 lib/guia_separacion.dart create mode 100644 lib/ruta_exclusiva.dart diff --git a/lib/configuraciondomicilio.dart b/lib/configuraciondomicilio.dart index 1449443..138ae3d 100644 --- a/lib/configuraciondomicilio.dart +++ b/lib/configuraciondomicilio.dart @@ -1,8 +1,20 @@ import 'package:flutter/material.dart'; import 'notificaciones_service.dart'; +// ← Lista de colonias disponibles (igual que en ruta_exclusiva.dart) +const List _coloniasDisponibles = [ + 'Zona Centro', + 'Las Arboledas', + 'Trojes', + 'San Juanico', + 'Los Olivos', + 'Rancho Seco', + 'Las Insurgentes', +]; + class PanelConfiguracionBottomSheet extends StatefulWidget { - final Function(String etiqueta, String direccion) onDomicilioGuardado; + // ← colonia agregada al callback + final Function(String etiqueta, String direccion, String colonia) onDomicilioGuardado; final bool notificarInicioRuta; final bool notificarAproximacion; final bool notificarRetrasosFallas; @@ -18,13 +30,19 @@ class PanelConfiguracionBottomSheet extends StatefulWidget { }); @override - State createState() => _PanelConfiguracionBottomSheetState(); + State createState() => + _PanelConfiguracionBottomSheetState(); } -class _PanelConfiguracionBottomSheetState extends State { - final _formKey = GlobalKey(); +class _PanelConfiguracionBottomSheetState + extends State { + + final _formKey = GlobalKey(); final _direccionController = TextEditingController(); + String _etiquetaSeleccionada = 'Casa'; + String _coloniaSeleccionada = _coloniasDisponibles[0]; // ← NUEVO + final List _opcionesEtiquetas = ['Casa', 'Trabajo', 'Otro']; late bool _inicioRuta; @@ -34,8 +52,8 @@ class _PanelConfiguracionBottomSheetState extends State( + value: _coloniaSeleccionada, + isExpanded: true, + icon: const Icon(Icons.arrow_drop_down, color: Colors.green), + items: _coloniasDisponibles.map((colonia) { + return DropdownMenuItem( + value: colonia, + child: Text(colonia), + ); + }).toList(), + onChanged: (value) { + setState(() { + _coloniaSeleccionada = value!; + }); + }, + ), + ), + ), + + const SizedBox(height: 16), + + // ── Botón guardar ── SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.green[700], foregroundColor: Colors.white, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), onPressed: () { if (_formKey.currentState!.validate()) { - widget.onDomicilioGuardado(_etiquetaSeleccionada, _direccionController.text); + // ← pasa también la colonia + widget.onDomicilioGuardado( + _etiquetaSeleccionada, + _direccionController.text, + _coloniaSeleccionada, + ); Navigator.pop(context); } }, @@ -108,23 +192,30 @@ class _PanelConfiguracionBottomSheetState extends State _etiquetaSeleccionada = etiqueta); }, ), ); diff --git a/lib/domicilios.dart b/lib/domicilios.dart index 9aec26d..1d7a9bd 100644 --- a/lib/domicilios.dart +++ b/lib/domicilios.dart @@ -2,16 +2,20 @@ import 'package:flutter/material.dart'; import 'tarjetaeta.dart'; import 'configuraciondomicilio.dart'; import 'notificaciones_service.dart'; +import 'ruta_exclusiva.dart'; +import 'guia_separacion.dart'; class Domicilio { final String id; final String etiqueta; final String direccion; + final String colonia; // ← NUEVO Domicilio({ required this.id, required this.etiqueta, required this.direccion, + required this.colonia, // ← NUEVO }); } @@ -31,11 +35,13 @@ class _GestionDomiciliosScreenState id: '1', etiqueta: 'Casa', direccion: 'Av. Reforma 222, CDMX', + colonia: 'Zona Centro', // ← NUEVO ), Domicilio( id: '2', etiqueta: 'Trabajo', direccion: 'Benito Juárez 45', + colonia: 'Las Arboledas', // ← NUEVO ), ]; @@ -57,13 +63,15 @@ class _GestionDomiciliosScreenState notificarInicioRuta: _notificarInicioRuta, notificarAproximacion: _notificarAproximacion, notificarRetrasosFallas: _notificarRetrasosFallas, - onDomicilioGuardado: (String etiqueta, String direccion) { + // ← ahora recibe también colonia + onDomicilioGuardado: (String etiqueta, String direccion, String colonia) { setState(() { _misDomicilios.add( Domicilio( id: DateTime.now().toString(), etiqueta: etiqueta, direccion: direccion, + colonia: colonia, // ← NUEVO ), ); }); @@ -108,112 +116,390 @@ class _GestionDomiciliosScreenState @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.grey[100], - - appBar: AppBar( - title: const Text('Mis Domicilios'), - backgroundColor: Colors.green, - foregroundColor: Colors.white, - - actions: [ - IconButton( - icon: const Icon(Icons.notifications_outlined), - tooltip: 'Alertas y Notificaciones', - onPressed: () async { - final resultado = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AlertasNotificacionesScreen( - notificarInicioRuta: _notificarInicioRuta, - notificarAproximacion: _notificarAproximacion, - notificarRetrasosFallas: _notificarRetrasosFallas, - ), - ), - ); - - if (resultado != null) { - setState(() { - _notificarInicioRuta = resultado['inicio']; - _notificarAproximacion = resultado['aproximacion']; - _notificarRetrasosFallas = resultado['retrasos']; - }); - } - }, - ), - ], - ), - - body: Column( + body: Stack( children: [ - // ← TARJETA ETA corregida - const Padding( - padding: EdgeInsets.all(16), - child: TarjetaEtaWidget(), // ← child bien cerrado aquí + Container( + width: double.infinity, + height: double.infinity, + color: const Color(0xFF5BB8E8), ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 16), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - 'Lugares Registrados', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + Positioned( + top: 80, + left: -40, + right: -40, + child: Container( + height: MediaQuery.of(context).size.height * 0.75, + decoration: BoxDecoration( + color: const Color(0xFF4AA8D8).withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(200), + ), + ), + ), + + Positioned( + bottom: 80, + right: -10, + child: CustomPaint( + size: const Size(160, 130), + painter: _MountainPainter(color: const Color(0xFF2E7D32)), + ), + ), + Positioned( + bottom: 80, + right: 60, + child: CustomPaint( + size: const Size(120, 100), + painter: _MountainPainter(color: const Color(0xFF388E3C)), + ), + ), + + Positioned( + bottom: 0, left: 0, right: 0, + child: Container(height: 90, color: const Color(0xFF4CAF50)), + ), + + Positioned( + bottom: 30, left: 0, right: 0, + child: Container( + height: 18, + color: const Color(0xFF424242), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 12, + (_) => Container(width: 20, height: 3, color: Colors.white), ), ), ), ), - const SizedBox(height: 10), + Positioned(bottom: 45, left: 20, + child: _buildHouse(const Color(0xFF90A4AE), 40)), + Positioned(bottom: 45, left: 80, + child: _buildHouse(const Color(0xFF795548), 35)), + Positioned(bottom: 45, right: 20, + child: _buildHouse(const Color(0xFFFFCC80), 45)), - Expanded( - child: ListView.builder( - itemCount: _misDomicilios.length, - itemBuilder: (context, index) { - final domicilio = _misDomicilios[index]; - return Card( - margin: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, + Positioned(bottom: 33, left: 110, + child: _buildTruck(const Color(0xFFE53935))), + Positioned(bottom: 33, right: 80, + child: _buildTruck(const Color(0xFFE53935))), + + Column( + children: [ + + Column( + children: [ + Container(height: 10, color: const Color(0xFFE8534A)), + Container(height: 8, color: const Color(0xFFF5A623)), + Container(height: 8, color: const Color(0xFFFFD700)), + Container(height: 8, color: const Color(0xFF4CAF50)), + ], + ), + + SafeArea( + bottom: false, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + + const Text( + 'ECOUBICEL.', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w900, + color: Colors.black87, + letterSpacing: 1.5, + ), + ), + const SizedBox(width: 8), + const Icon(Icons.recycling, color: Color(0xFF2E7D32), size: 28), + const Spacer(), + + // ── Botón notificaciones ── + Container( + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.notifications_outlined, color: Colors.white), + tooltip: 'Alertas y Notificaciones', + onPressed: () async { + final resultado = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AlertasNotificacionesScreen( + notificarInicioRuta: _notificarInicioRuta, + notificarAproximacion: _notificarAproximacion, + notificarRetrasosFallas: _notificarRetrasosFallas, + ), + ), + ); + if (resultado != null) { + setState(() { + _notificarInicioRuta = resultado['inicio']; + _notificarAproximacion = resultado['aproximacion']; + _notificarRetrasosFallas = resultado['retrasos']; + }); + } + }, + ), + ), + + // ── Botón Mi Ruta ── + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + color: const Color(0xFF1E88E5), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.map_outlined, color: Colors.white), + tooltip: 'Mi Ruta', + onPressed: () { + // ← usa colonia real del primer domicilio + final domicilio = _misDomicilios.isNotEmpty + ? _misDomicilios[0] + : null; + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RutaExclusivaScreen( + direccionUsuario: domicilio?.direccion ?? 'Sin dirección', + coloniaUsuario: domicilio?.colonia ?? '', // ← CAMBIADO + ), + ), + ); + }, + ), + ), + const SizedBox(width: 8), + Container( + decoration: BoxDecoration( + color: const Color(0xFF558B2F), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.eco_outlined, color: Colors.white), + tooltip: 'Guía de Separación', + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const GuiaSeparacionScreen(), + ), + ); + }, + ), + ), + ], ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - child: ListTile( - leading: CircleAvatar( - backgroundColor: Colors.green[100], - child: Icon( - _obtenerIcono(domicilio.etiqueta), - color: Colors.green, + ), + ), + + const Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: TarjetaEtaWidget(), + ), + + const SizedBox(height: 12), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + children: [ + Container( + width: 5, + height: 22, + decoration: BoxDecoration( + color: const Color(0xFF4CAF50), + borderRadius: BorderRadius.circular(4), ), ), - title: Text(domicilio.etiqueta), - subtitle: Text(domicilio.direccion), - trailing: IconButton( - icon: const Icon(Icons.delete, color: Colors.red), - onPressed: () { - setState(() { - _misDomicilios.removeAt(index); - }); - }, + const SizedBox(width: 8), + const Text( + 'Lugares Registrados', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), ), - ), - ); - }, - ), + ], + ), + ), + + const SizedBox(height: 10), + + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 100), + itemCount: _misDomicilios.length, + itemBuilder: (context, index) { + final domicilio = _misDomicilios[index]; + final cardColors = [ + const Color(0xFF4CAF50), + const Color(0xFF1E88E5), + const Color(0xFFE65100), + ]; + final cardColor = cardColors[index % cardColors.length]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: cardColor, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: cardColor.withValues(alpha: 0.4), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, vertical: 4), + leading: CircleAvatar( + backgroundColor: Colors.white.withValues(alpha: 0.3), + child: Icon( + _obtenerIcono(domicilio.etiqueta), + color: Colors.white, + ), + ), + title: Text( + domicilio.etiqueta, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + // ← muestra colonia como subtítulo + subtitle: Text( + '${domicilio.direccion} • ${domicilio.colonia}', + style: const TextStyle(color: Colors.white70), + ), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.white), + onPressed: () { + setState(() { + _misDomicilios.removeAt(index); + }); + }, + ), + ), + ); + }, + ), + ), + ], ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _mostrarFormularioAgregar, - backgroundColor: Colors.green, + backgroundColor: const Color(0xFFE65100), foregroundColor: Colors.white, + shape: const StadiumBorder(), icon: const Icon(Icons.add), - label: const Text('Agregar'), + label: const Text( + 'Agregar', + style: TextStyle(fontWeight: FontWeight.bold), + ), ), ); } + + Widget _buildHouse(Color color, double size) { + return CustomPaint( + size: Size(size, size * 0.9), + painter: _HousePainter(color: color), + ); + } + + Widget _buildTruck(Color color) { + return Container( + width: 36, + height: 18, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(3), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 12, + height: 14, + margin: const EdgeInsets.only(right: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.7), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(3), + topLeft: Radius.circular(2), + ), + ), + ), + ], + ), + ); + } +} + +class _MountainPainter extends CustomPainter { + final Color color; + _MountainPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path() + ..moveTo(0, size.height) + ..lineTo(size.width / 2, 0) + ..lineTo(size.width, size.height) + ..close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(_) => false; +} + +class _HousePainter extends CustomPainter { + final Color color; + _HousePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + canvas.drawRect( + Rect.fromLTWH(0, size.height * 0.4, size.width, size.height * 0.6), + paint, + ); + final roof = Paint()..color = const Color(0xFFB71C1C); + final path = Path() + ..moveTo(0, size.height * 0.4) + ..lineTo(size.width / 2, 0) + ..lineTo(size.width, size.height * 0.4) + ..close(); + canvas.drawPath(path, roof); + canvas.drawRect( + Rect.fromLTWH(size.width * 0.25, size.height * 0.55, + size.width * 0.2, size.height * 0.2), + Paint()..color = const Color(0xFFB3E5FC), + ); + canvas.drawRect( + Rect.fromLTWH(size.width * 0.55, size.height * 0.65, + size.width * 0.2, size.height * 0.35), + Paint()..color = const Color(0xFF5D4037), + ); + } + + @override + bool shouldRepaint(_) => false; } \ No newline at end of file diff --git a/lib/guia_separacion.dart b/lib/guia_separacion.dart new file mode 100644 index 0000000..58ce559 --- /dev/null +++ b/lib/guia_separacion.dart @@ -0,0 +1,359 @@ +import 'package:flutter/material.dart'; + +// ─── DATOS DE CATEGORÍAS (sin conexión, todo local) ───────────── +const List> _categorias = [ + { + 'nombre' : 'Orgánicos', + 'icono' : '🍃', + 'color' : Color(0xFF2E7D32), + 'colorFondo': Color(0xFFE8F5E9), + 'descripcion': 'Residuos de origen natural que se descomponen fácilmente.', + 'ejemplos': [ + '🥦 Restos de frutas y verduras', + '🥚 Cáscaras de huevo', + '☕ Café molido y filtros de té', + '🌿 Pasto, hojas y flores', + '🍖 Restos de comida cocinada', + ], + 'noIncluir': [ + '🚫 Aceites o grasas', + '🚫 Plásticos o envases', + '🚫 Papel de baño usado', + ], + 'consejo': 'Deposítalos en bolsa VERDE. Son ideales para compostar y nutrir la tierra.', + }, + { + 'nombre' : 'Reciclables', + 'icono' : '♻️', + 'color' : Color(0xFF1565C0), + 'colorFondo': Color(0xFFE3F2FD), + 'descripcion': 'Materiales que pueden transformarse en nuevos productos.', + 'ejemplos': [ + '🧴 Botellas y envases de plástico (PET)', + '📦 Cajas y cartón limpio', + '🗞️ Papel y periódico', + '🥫 Latas de aluminio y acero', + '🍶 Botellas y frascos de vidrio', + ], + 'noIncluir': [ + '🚫 Papel sucio o con grasa', + '🚫 Vidrio roto sin envolver', + '🚫 Plásticos con comida', + ], + 'consejo': 'Deposítalos en bolsa AZUL. Enjuaga los envases antes de separarlos.', + }, + { + 'nombre' : 'Sanitarios', + 'icono' : '🚽', + 'color' : Color(0xFF6D4C41), + 'colorFondo': Color(0xFFEFEBE9), + 'descripcion': 'Residuos que por higiene no pueden mezclarse con reciclables.', + 'ejemplos': [ + '🧻 Papel higiénico usado', + '👶 Pañales desechables', + '🩹 Curitas y vendas usadas', + '😷 Cubrebocas desechables', + '🧼 Toallas húmedas y servilletas', + ], + 'noIncluir': [ + '🚫 Medicamentos (van en especiales)', + '🚫 Jeringas o material médico cortante', + ], + 'consejo': 'Deposítalos en bolsa NEGRA bien cerrada. No mezclar con reciclables.', + }, + { + 'nombre' : 'Especiales', + 'icono' : '⚠️', + 'color' : Color(0xFFE65100), + 'colorFondo': Color(0xFFFFF3E0), + 'descripcion': 'Residuos peligrosos que requieren manejo especial.', + 'ejemplos': [ + '🔋 Pilas y baterías', + '💊 Medicamentos caducados', + '💡 Focos y luminarias', + '📱 Electrónicos en desuso', + '🛢️ Aceite vegetal o de motor usado', + ], + 'noIncluir': [ + '🚫 NO van en el camión regular', + '🚫 NO tirar a la basura común', + ], + 'consejo': '⚠️ Llévalos al punto ECORED más cercano. ¡Nunca al camión recolector!', + }, +]; + +// ════════════════════════════════════════════════════════════════ +// PANTALLA GUÍA DE SEPARACIÓN +// ════════════════════════════════════════════════════════════════ +class GuiaSeparacionScreen extends StatefulWidget { + const GuiaSeparacionScreen({super.key}); + + @override + State createState() => _GuiaSeparacionScreenState(); +} + +class _GuiaSeparacionScreenState extends State { + + // Controla qué categoría está expandida (-1 = ninguna) + int _expandida = -1; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[100], + + appBar: AppBar( + title: const Text('Guía de Separación'), + backgroundColor: const Color(0xFF2E7D32), + foregroundColor: Colors.white, + ), + + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + + // ── ENCABEZADO ──────────────────────────────────── + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF2E7D32), + borderRadius: BorderRadius.circular(16), + ), + child: const Row( + children: [ + Text('♻️', style: TextStyle(fontSize: 36)), + SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Separa correctamente', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 4), + Text( + 'Toca cada categoría para ver qué residuos incluye. Funciona sin conexión.', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // ── CATEGORÍAS ──────────────────────────────────── + ..._categorias.asMap().entries.map((entry) { + final index = entry.key; + final categoria = entry.value; + final expandida = _expandida == index; + final color = categoria['color'] as Color; + final colorFondo = categoria['colorFondo'] as Color; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.15), + blurRadius: 8, + offset: const Offset(0, 3), + ), + ], + ), + child: Column( + children: [ + + // ── Header de categoría (siempre visible) ── + GestureDetector( + onTap: () { + setState(() { + _expandida = expandida ? -1 : index; + }); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, vertical: 14), + decoration: BoxDecoration( + color: color, + borderRadius: expandida + ? const BorderRadius.vertical( + top: Radius.circular(16)) + : BorderRadius.circular(16), + ), + child: Row( + children: [ + Text( + categoria['icono'] as String, + style: const TextStyle(fontSize: 28), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + categoria['nombre'] as String, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + categoria['descripcion'] as String, + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + ), + ), + ], + ), + ), + Icon( + expandida + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: Colors.white, + size: 28, + ), + ], + ), + ), + ), + + // ── Contenido expandible ── + if (expandida) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: colorFondo, + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(16), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + // Sí incluir + Text( + '✅ Sí incluir:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 8), + ...(categoria['ejemplos'] as List).map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Text( + e, + style: const TextStyle(fontSize: 13), + ), + ), + ), + + const SizedBox(height: 12), + + // No incluir + Text( + '🚫 No incluir:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 8), + ...(categoria['noIncluir'] as List).map( + (e) => Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Text( + e, + style: const TextStyle(fontSize: 13), + ), + ), + ), + + const SizedBox(height: 12), + + // Consejo + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: color.withValues(alpha: 0.3), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('💡', style: TextStyle(fontSize: 16)), + const SizedBox(width: 8), + Expanded( + child: Text( + categoria['consejo'] as String, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + }), + + const SizedBox(height: 8), + + // ── PIE DE PÁGINA ───────────────────────────────── + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + ), + child: const Row( + children: [ + Icon(Icons.wifi_off, color: Colors.grey, size: 18), + SizedBox(width: 8), + Expanded( + child: Text( + 'Esta guía funciona completamente sin conexión a internet.', + style: TextStyle(fontSize: 12, color: Colors.grey), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/login.dart b/lib/login.dart index 6cb4b4b..7ab12da 100644 --- a/lib/login.dart +++ b/lib/login.dart @@ -11,18 +11,13 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final _formKey = GlobalKey(); - final TextEditingController emailController = - TextEditingController(); - - final TextEditingController passwordController = - TextEditingController(); + final TextEditingController emailController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); bool ocultarPassword = true; void iniciarSesion() { if (_formKey.currentState!.validate()) { - - // Simulación de login exitoso ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Inicio de sesión exitoso'), @@ -30,12 +25,10 @@ class _LoginScreenState extends State { ), ); - // Navegar a la pantalla principal Navigator.pushReplacement( context, MaterialPageRoute( - builder: (context) => - const GestionDomiciliosScreen(), + builder: (context) => const GestionDomiciliosScreen(), ), ); } @@ -44,190 +37,455 @@ class _LoginScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Colors.white, + body: Stack( + children: [ + // ── Fondo azul cielo ── + Container( + width: double.infinity, + height: double.infinity, + color: const Color(0xFF5BB8E8), + ), - body: Center( - child: SingleChildScrollView( - padding: const EdgeInsets.all(25), + // ── Barras de colores en la parte superior ── + Column( + children: [ + Container(height: 10, color: const Color(0xFFE8534A)), + Container(height: 8, color: const Color(0xFFF5A623)), + Container(height: 8, color: const Color(0xFFFFD700)), + Container(height: 8, color: const Color(0xFF4CAF50)), + ], + ), - child: Form( - key: _formKey, - - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - - children: [ - - // Logo - CircleAvatar( - radius: 50, - backgroundColor: Colors.green.shade100, - - child: Icon( - Icons.recycling, - size: 60, - color: Colors.green.shade700, - ), - ), - - const SizedBox(height: 20), - - const Text( - 'EcoRecolección', - style: TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - ), - ), - - const SizedBox(height: 10), - - Text( - 'Inicia sesión para continuar', - style: TextStyle( - color: Colors.grey.shade600, - fontSize: 16, - ), - ), - - const SizedBox(height: 40), - - // Campo correo - TextFormField( - controller: emailController, - - decoration: InputDecoration( - labelText: 'Correo Electrónico', - hintText: 'ejemplo@correo.com', - - prefixIcon: const Icon(Icons.email), - - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(15), - ), - ), - - validator: (value) { - if (value == null || value.isEmpty) { - return 'Ingresa tu correo'; - } - - if (!value.contains('@')) { - return 'Correo inválido'; - } - - return null; - }, - ), - - const SizedBox(height: 20), - - // Campo contraseña - TextFormField( - controller: passwordController, - obscureText: ocultarPassword, - - decoration: InputDecoration( - labelText: 'Contraseña', - - prefixIcon: - const Icon(Icons.lock), - - suffixIcon: IconButton( - icon: Icon( - ocultarPassword - ? Icons.visibility_off - : Icons.visibility, - ), - - onPressed: () { - setState(() { - ocultarPassword = - !ocultarPassword; - }); - }, - ), - - border: OutlineInputBorder( - borderRadius: - BorderRadius.circular(15), - ), - ), - - validator: (value) { - if (value == null || value.isEmpty) { - return 'Ingresa tu contraseña'; - } - - if (value.length < 6) { - return 'Mínimo 6 caracteres'; - } - - return null; - }, - ), - - const SizedBox(height: 30), - - // Botón iniciar sesión - SizedBox( - width: double.infinity, - height: 55, - - child: ElevatedButton( - onPressed: iniciarSesion, - - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(15), - ), - ), - - child: const Text( - 'Iniciar Sesión', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - - const SizedBox(height: 20), - - // Registro - Row( - mainAxisAlignment: - MainAxisAlignment.center, - - children: [ - const Text( - '¿No tienes cuenta?', - ), - - TextButton( - onPressed: () {}, - - child: const Text( - 'Registrarse', - style: TextStyle( - color: Colors.green, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ], + // ── Forma orgánica azul oscuro detrás del contenido ── + Positioned( + top: 60, + left: -40, + right: -40, + child: Container( + height: 420, + decoration: BoxDecoration( + color: const Color(0xFF4AA8D8).withValues(alpha:0.6), + borderRadius: BorderRadius.circular(200), + ), ), ), - ), + + // ── Montañas verdes atrás ── + Positioned( + bottom: 80, + right: -10, + child: CustomPaint( + size: const Size(160, 130), + painter: _MountainPainter(color: const Color(0xFF2E7D32)), + ), + ), + Positioned( + bottom: 80, + right: 60, + child: CustomPaint( + size: const Size(120, 100), + painter: _MountainPainter(color: const Color(0xFF388E3C)), + ), + ), + + // ── Suelo verde ── + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + height: 90, + color: const Color(0xFF4CAF50), + ), + ), + + // ── Carretera ── + Positioned( + bottom: 30, + left: 0, + right: 0, + child: Container( + height: 18, + color: const Color(0xFF424242), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate( + 12, + (_) => Container( + width: 20, + height: 3, + color: Colors.white, + ), + ), + ), + ), + ), + + // ── Casas pequeñas ── + Positioned( + bottom: 45, + left: 20, + child: _buildHouse(const Color(0xFF90A4AE), 40), + ), + Positioned( + bottom: 45, + left: 80, + child: _buildHouse(const Color(0xFF795548), 35), + ), + Positioned( + bottom: 45, + right: 20, + child: _buildHouse(const Color(0xFFFFCC80), 45), + ), + + // ── Camiones de basura ── + Positioned( + bottom: 33, + left: 110, + child: _buildTruck(const Color(0xFFE53935)), + ), + Positioned( + bottom: 33, + right: 80, + child: _buildTruck(const Color(0xFFE53935)), + ), + + // ── Basura (montón gris izquierda) ── + Positioned( + bottom: 90, + left: 10, + child: CustomPaint( + size: const Size(60, 35), + painter: _TrashPilePainter(), + ), + ), + + // ── Contenido principal ── + SafeArea( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: Form( + key: _formKey, + child: Column( + children: [ + const SizedBox(height: 50), + + // Título ECOUBICEL + const Text( + 'ECOUBICEL.', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.w900, + color: Colors.black87, + letterSpacing: 2, + ), + ), + + const SizedBox(height: 16), + + // Botes de basura + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildTrashCan(const Color(0xFF2E7D32), Icons.recycling), + const SizedBox(width: 8), + _buildTrashCan(const Color(0xFFF9A825), Icons.recycling), + const SizedBox(width: 8), + _buildTrashCan(const Color(0xFFC62828), Icons.recycling), + const SizedBox(width: 12), + // Ícono de reciclaje + Icon( + Icons.recycling, + size: 48, + color: Colors.green.shade600, + ), + ], + ), + + const SizedBox(height: 28), + + // Campo correo — barra verde + _buildColorField( + controller: emailController, + hint: 'Correo Electrónico', + icon: Icons.email, + color: const Color(0xFF4CAF50), + obscure: false, + validator: (value) { + if (value == null || value.isEmpty) return 'Ingresa tu correo'; + if (!value.contains('@')) return 'Correo inválido'; + return null; + }, + ), + + const SizedBox(height: 12), + + // Campo contraseña — barra azul + _buildColorField( + controller: passwordController, + hint: 'Contraseña', + icon: Icons.lock, + color: const Color(0xFF1E88E5), + obscure: ocultarPassword, + suffixIcon: IconButton( + icon: Icon( + ocultarPassword ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () => setState(() => ocultarPassword = !ocultarPassword), + ), + validator: (value) { + if (value == null || value.isEmpty) return 'Ingresa tu contraseña'; + if (value.length < 6) return 'Mínimo 6 caracteres'; + return null; + }, + ), + + const SizedBox(height: 12), + + // Botón iniciar sesión — barra naranja + SizedBox( + width: double.infinity, + height: 48, + child: ElevatedButton( + onPressed: iniciarSesion, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFE65100), + foregroundColor: Colors.white, + shape: const StadiumBorder(), + elevation: 3, + ), + child: const Text( + 'Iniciar Sesión', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + const SizedBox(height: 12), + + // Botón registrarse — barra gris + SizedBox( + width: double.infinity, + height: 48, + child: OutlinedButton( + onPressed: () {}, + style: OutlinedButton.styleFrom( + foregroundColor: Colors.white, + side: const BorderSide(color: Colors.white54, width: 2), + backgroundColor: Colors.white24, + shape: const StadiumBorder(), + ), + child: const Text( + '¿No tienes cuenta? Registrarse', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + + const SizedBox(height: 120), + ], + ), + ), + ), + ), + ), + ], ), ); } + + // Widget bote de basura + Widget _buildTrashCan(Color color, IconData icon) { + return Container( + width: 52, + height: 64, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow(color: Colors.black26, blurRadius: 4, offset: const Offset(2, 2)), + ], + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + height: 8, + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + color: color.withValues(alpha:0.7), + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 4), + Icon(icon, color: Colors.white, size: 28), + Container( + height: 6, + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(3), + ), + ), + ], + ), + ); + } + + // Campo de texto con fondo de color tipo barra + Widget _buildColorField({ + required TextEditingController controller, + required String hint, + required IconData icon, + required Color color, + required bool obscure, + Widget? suffixIcon, + required String? Function(String?) validator, + }) { + return TextFormField( + controller: controller, + obscureText: obscure, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600), + decoration: InputDecoration( + hintText: hint, + hintStyle: const TextStyle(color: Colors.white70), + prefixIcon: Icon(icon, color: Colors.white), + suffixIcon: suffixIcon, + filled: true, + fillColor: color, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: BorderSide.none, + ), + errorStyle: const TextStyle(color: Colors.yellow), + contentPadding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + ), + validator: validator, + ); + } + + // Casa pequeña + Widget _buildHouse(Color color, double size) { + return CustomPaint( + size: Size(size, size * 0.9), + painter: _HousePainter(color: color), + ); + } + + // Camión pequeño + Widget _buildTruck(Color color) { + return Container( + width: 36, + height: 18, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(3), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: 12, + height: 14, + margin: const EdgeInsets.only(right: 2, top: 0), + decoration: BoxDecoration( + color: color.withValues(alpha:0.7), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(3), + topLeft: Radius.circular(2), + ), + ), + ), + ], + ), + ); + } +} + +// Pintor de montañas +class _MountainPainter extends CustomPainter { + final Color color; + _MountainPainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path() + ..moveTo(0, size.height) + ..lineTo(size.width / 2, 0) + ..lineTo(size.width, size.height) + ..close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(_) => false; +} + +// Pintor de casa +class _HousePainter extends CustomPainter { + final Color color; + _HousePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + // Cuerpo + canvas.drawRect( + Rect.fromLTWH(0, size.height * 0.4, size.width, size.height * 0.6), + paint, + ); + // Techo + final roof = Paint()..color = const Color(0xFFB71C1C); + final path = Path() + ..moveTo(0, size.height * 0.4) + ..lineTo(size.width / 2, 0) + ..lineTo(size.width, size.height * 0.4) + ..close(); + canvas.drawPath(path, roof); + // Ventana + canvas.drawRect( + Rect.fromLTWH(size.width * 0.25, size.height * 0.55, size.width * 0.2, size.height * 0.2), + Paint()..color = const Color(0xFFB3E5FC), + ); + // Puerta + canvas.drawRect( + Rect.fromLTWH(size.width * 0.55, size.height * 0.65, size.width * 0.2, size.height * 0.35), + Paint()..color = const Color(0xFF5D4037), + ); + } + + @override + bool shouldRepaint(_) => false; +} + +// Pintor de montón de basura +class _TrashPilePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = const Color(0xFF757575); + canvas.drawOval( + Rect.fromLTWH(0, size.height * 0.3, size.width, size.height * 0.7), + paint, + ); + canvas.drawOval( + Rect.fromLTWH(size.width * 0.1, 0, size.width * 0.5, size.height * 0.6), + Paint()..color = const Color(0xFF616161), + ); + canvas.drawOval( + Rect.fromLTWH(size.width * 0.5, size.height * 0.1, size.width * 0.4, size.height * 0.5), + Paint()..color = const Color(0xFF9E9E9E), + ); + } + + @override + bool shouldRepaint(_) => false; } \ No newline at end of file diff --git a/lib/ruta_exclusiva.dart b/lib/ruta_exclusiva.dart new file mode 100644 index 0000000..386bb64 --- /dev/null +++ b/lib/ruta_exclusiva.dart @@ -0,0 +1,398 @@ +import 'package:flutter/material.dart'; +import 'camion_estado.dart'; + +// ─── DATOS DE COLONIAS (de tu colonias-rutas.json) ────────────── +const List> _coloniasRutas = [ + {'colonia': 'Zona Centro', 'routeId': 'RUTA-01', 'horario': 'Matutino (06:30 - 07:15)'}, + {'colonia': 'Las Arboledas', 'routeId': 'RUTA-01', 'horario': 'Matutino (07:00 - 07:30)'}, + {'colonia': 'Trojes', 'routeId': 'RUTA-13', 'horario': 'Matutino (06:40 - 07:10)'}, + {'colonia': 'San Juanico', 'routeId': 'RUTA-03', 'horario': 'Matutino (06:45 - 07:15)'}, + {'colonia': 'Los Olivos', 'routeId': 'RUTA-04', 'horario': 'Matutino (07:00 - 07:40)'}, + {'colonia': 'Rancho Seco', 'routeId': 'RUTA-05', 'horario': 'Vespertino (14:15 - 15:00)'}, + {'colonia': 'Las Insurgentes', 'routeId': 'RUTA-12', 'horario': 'Matutino (06:35 - 07:10)'}, +]; + +// ════════════════════════════════════════════════════════════════ +// PANTALLA RUTA EXCLUSIVA +// ════════════════════════════════════════════════════════════════ +class RutaExclusivaScreen extends StatefulWidget { + final String direccionUsuario; // ← viene desde domicilios.dart + final String coloniaUsuario; // ← viene desde domicilios.dart + + const RutaExclusivaScreen({ + super.key, + required this.direccionUsuario, + required this.coloniaUsuario, + }); + + @override + State createState() => _RutaExclusivaScreenState(); +} + +class _RutaExclusivaScreenState extends State { + + Map? _rutaAsignada; // solo la ruta del usuario + bool _accesoValidado = false; + bool _cargando = true; + + @override + void initState() { + super.initState(); + camionEstado.addListener(_actualizar); + _validarAcceso(); + } + + void _actualizar() { + if (mounted) setState(() {}); + } + + @override + void dispose() { + camionEstado.removeListener(_actualizar); + super.dispose(); + } + + // ── Valida que la colonia del usuario exista en las rutas ──── + void _validarAcceso() async { + await Future.delayed(const Duration(milliseconds: 800)); // simula carga + + final coloniaLower = widget.coloniaUsuario.trim().toLowerCase(); + + final encontrada = _coloniasRutas.firstWhere( + (r) => r['colonia']!.toLowerCase() == coloniaLower, + orElse: () => {}, + ); + + setState(() { + _cargando = false; + if (encontrada.isNotEmpty) { + _rutaAsignada = encontrada; + _accesoValidado = true; + } else { + _accesoValidado = false; + } + }); + } + + // ── Color según etapa del camión ───────────────────────────── + Color _colorEtapa() { + final id = camionEstado.positionId; + if (id <= 1) return Colors.grey; + if (id <= 3) return Colors.blue; + if (id <= 5) return Colors.orange; + if (id == 6) return Colors.green; + return Colors.grey; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey[100], + + appBar: AppBar( + title: const Text('Mi Ruta Asignada'), + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + + body: _cargando + ? _buildCargando() + : _accesoValidado + ? _buildRutaValidada() + : _buildSinAcceso(), + ); + } + + // ── Pantalla de carga ──────────────────────────────────────── + Widget _buildCargando() { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.green), + SizedBox(height: 16), + Text( + 'Validando tu dirección...', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ], + ), + ); + } + + // ── Sin acceso: colonia no registrada ──────────────────────── + Widget _buildSinAcceso() { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + + Icon(Icons.lock_outline, size: 80, color: Colors.red[300]), + + const SizedBox(height: 20), + + const Text( + 'Dirección no reconocida', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + + const SizedBox(height: 12), + + Text( + 'Tu colonia "${widget.coloniaUsuario}" no está registrada en el sistema de rutas. Actualiza tu domicilio o contacta al municipio.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + + const SizedBox(height: 28), + + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back), + label: const Text('Actualizar domicilio'), + ), + ], + ), + ), + ); + } + + // ── Ruta validada: solo info de SU ruta ────────────────────── + Widget _buildRutaValidada() { + final ruta = _rutaAsignada!; + final etapa = CamionEstado.etapas[camionEstado.positionId] ?? ''; + final etaInfo = CamionEstado.etaInfo[camionEstado.positionId]!; + final color = _colorEtapa(); + final minutos = etaInfo['minutos'] as int; + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + + // ── BADGE DE ACCESO VALIDADO ────────────────────── + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.green), + ), + child: Row( + children: [ + const Icon(Icons.verified_user, color: Colors.green), + const SizedBox(width: 10), + Expanded( + child: Text( + 'Acceso validado para ${widget.coloniaUsuario}. Solo ves la información de tu ruta.', + style: const TextStyle( + fontSize: 13, + color: Colors.green, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 16), + + // ── CARD: DATOS DE LA RUTA ──────────────────────── + Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + Row( + children: [ + const Icon(Icons.route, color: Colors.green, size: 28), + const SizedBox(width: 10), + Text( + ruta['routeId']!, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + + const SizedBox(height: 16), + + _filaInfo(Icons.location_on, 'Colonia', ruta['colonia']!), + _filaInfo(Icons.schedule, 'Horario', ruta['horario']!), + _filaInfo(Icons.home, 'Dirección', widget.direccionUsuario), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // ── CARD: ESTADO EN TIEMPO REAL ─────────────────── + Card( + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + + const Row( + children: [ + Icon(Icons.local_shipping, color: Colors.green, size: 28), + SizedBox(width: 10), + Text( + 'Estado en tiempo real', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + + const SizedBox(height: 16), + + // Barra de progreso + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: LinearProgressIndicator( + value: camionEstado.positionId / 8, + minHeight: 12, + backgroundColor: Colors.grey[200], + valueColor: AlwaysStoppedAnimation(color), + ), + ), + + const SizedBox(height: 12), + + // Etapa actual + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha:0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withValues(alpha:0.3)), + ), + child: Text( + etapa, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ), + + const SizedBox(height: 12), + + // ETA + if (minutos > 0) + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.green[50], + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + const Icon(Icons.timer, color: Colors.green), + const SizedBox(width: 10), + Text( + 'Llega en aproximadamente $minutos minutos', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 16), + + // ── AVISO PRIVACIDAD ────────────────────────────── + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue.shade200), + ), + child: const Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.shield, color: Colors.blue), + SizedBox(width: 10), + Expanded( + child: Text( + 'Visión de túnel activa: No puedes ver rutas, horarios ni ubicaciones de otras colonias.', + style: TextStyle(fontSize: 13, color: Colors.blue), + ), + ), + ], + ), + ), + ], + ); + } + + // ── Widget auxiliar para filas de info ─────────────────────── + Widget _filaInfo(IconData icono, String label, String valor) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icono, color: Colors.green, size: 20), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey[500]), + ), + Text( + valor, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file