modificaciones en el sistema

This commit is contained in:
Kimberly
2026-05-23 09:30:42 -06:00
parent 7ebce39671
commit 476a6e08e4
5 changed files with 1690 additions and 296 deletions

View File

@@ -1,8 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'notificaciones_service.dart'; import 'notificaciones_service.dart';
// ← Lista de colonias disponibles (igual que en ruta_exclusiva.dart)
const List<String> _coloniasDisponibles = [
'Zona Centro',
'Las Arboledas',
'Trojes',
'San Juanico',
'Los Olivos',
'Rancho Seco',
'Las Insurgentes',
];
class PanelConfiguracionBottomSheet extends StatefulWidget { 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 notificarInicioRuta;
final bool notificarAproximacion; final bool notificarAproximacion;
final bool notificarRetrasosFallas; final bool notificarRetrasosFallas;
@@ -18,13 +30,19 @@ class PanelConfiguracionBottomSheet extends StatefulWidget {
}); });
@override @override
State<PanelConfiguracionBottomSheet> createState() => _PanelConfiguracionBottomSheetState(); State<PanelConfiguracionBottomSheet> createState() =>
_PanelConfiguracionBottomSheetState();
} }
class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottomSheet> { class _PanelConfiguracionBottomSheetState
extends State<PanelConfiguracionBottomSheet> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _direccionController = TextEditingController(); final _direccionController = TextEditingController();
String _etiquetaSeleccionada = 'Casa'; String _etiquetaSeleccionada = 'Casa';
String _coloniaSeleccionada = _coloniasDisponibles[0]; // ← NUEVO
final List<String> _opcionesEtiquetas = ['Casa', 'Trabajo', 'Otro']; final List<String> _opcionesEtiquetas = ['Casa', 'Trabajo', 'Otro'];
late bool _inicioRuta; late bool _inicioRuta;
@@ -59,46 +77,112 @@ class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottom
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Registrar Nuevo Domicilio', 'Registrar Nuevo Domicilio',
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
), ),
),
const SizedBox(height: 16), const SizedBox(height: 16),
Form( Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Etiqueta:', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.green[800])),
const SizedBox(height: 8), // ── Etiqueta ──
Row( Text(
children: _opcionesEtpciones(), 'Etiqueta:',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green[800],
), ),
),
const SizedBox(height: 8),
Row(children: _opcionesEtpciones()),
const SizedBox(height: 16), const SizedBox(height: 16),
// ── Dirección ──
TextFormField( TextFormField(
controller: _direccionController, controller: _direccionController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Dirección completa', labelText: 'Dirección completa',
prefixIcon: const Icon(Icons.location_on, color: Colors.green), prefixIcon: const Icon(Icons.location_on, color: Colors.green),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
validator: (value) { validator: (value) {
if (value == null || value.trim().isEmpty) return 'Ingresa una dirección'; if (value == null || value.trim().isEmpty) {
return 'Ingresa una dirección';
}
return null; return null;
}, },
), ),
const SizedBox(height: 12),
const SizedBox(height: 16),
// ── Selector de colonia ── NUEVO
Text(
'Colonia:',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green[800],
),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(12),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
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( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[700], backgroundColor: Colors.green[700],
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
widget.onDomicilioGuardado(_etiquetaSeleccionada, _direccionController.text); // ← pasa también la colonia
widget.onDomicilioGuardado(
_etiquetaSeleccionada,
_direccionController.text,
_coloniaSeleccionada,
);
Navigator.pop(context); Navigator.pop(context);
} }
}, },
@@ -108,23 +192,30 @@ class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottom
], ],
), ),
), ),
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 16.0), padding: EdgeInsets.symmetric(vertical: 16.0),
child: Divider(color: Colors.black), child: Divider(color: Colors.black),
), ),
Row( Row(
children: [ children: [
Icon(Icons.notifications_active_rounded, color: Colors.green[800]), Icon(Icons.notifications_active_rounded, color: Colors.green[800]),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Alertas Operativas Push', 'Alertas Operativas Push',
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SwitchListTile( SwitchListTile(
title: const Text('Inicio de Ruta', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), title: const Text('Inicio de Ruta',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
value: _inicioRuta, value: _inicioRuta,
activeThumbColor: Colors.green[700], activeThumbColor: Colors.green[700],
onChanged: (bool value) { onChanged: (bool value) {
@@ -132,8 +223,10 @@ class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottom
widget.onAlertasChanged(value, 'inicio'); widget.onAlertasChanged(value, 'inicio');
}, },
), ),
SwitchListTile( SwitchListTile(
title: const Text('Camión Próximo', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), title: const Text('Camión Próximo',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
value: _aproximacion, value: _aproximacion,
activeThumbColor: Colors.green[700], activeThumbColor: Colors.green[700],
onChanged: (bool value) { onChanged: (bool value) {
@@ -141,8 +234,10 @@ class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottom
widget.onAlertasChanged(value, 'aproximacion'); widget.onAlertasChanged(value, 'aproximacion');
}, },
), ),
SwitchListTile( SwitchListTile(
title: const Text('Imprevistos y Retrasos', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), title: const Text('Imprevistos y Retrasos',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600)),
value: _retrasosFallas, value: _retrasosFallas,
activeThumbColor: Colors.green[700], activeThumbColor: Colors.green[700],
onChanged: (bool value) { onChanged: (bool value) {
@@ -167,9 +262,7 @@ class _PanelConfiguracionBottomSheetState extends State<PanelConfiguracionBottom
selectedColor: Colors.green[100], selectedColor: Colors.green[100],
checkmarkColor: Colors.green[800], checkmarkColor: Colors.green[800],
onSelected: (bool selected) { onSelected: (bool selected) {
setState(() { setState(() => _etiquetaSeleccionada = etiqueta);
_etiquetaSeleccionada = etiqueta;
});
}, },
), ),
); );

View File

@@ -2,16 +2,20 @@ import 'package:flutter/material.dart';
import 'tarjetaeta.dart'; import 'tarjetaeta.dart';
import 'configuraciondomicilio.dart'; import 'configuraciondomicilio.dart';
import 'notificaciones_service.dart'; import 'notificaciones_service.dart';
import 'ruta_exclusiva.dart';
import 'guia_separacion.dart';
class Domicilio { class Domicilio {
final String id; final String id;
final String etiqueta; final String etiqueta;
final String direccion; final String direccion;
final String colonia; // ← NUEVO
Domicilio({ Domicilio({
required this.id, required this.id,
required this.etiqueta, required this.etiqueta,
required this.direccion, required this.direccion,
required this.colonia, // ← NUEVO
}); });
} }
@@ -31,11 +35,13 @@ class _GestionDomiciliosScreenState
id: '1', id: '1',
etiqueta: 'Casa', etiqueta: 'Casa',
direccion: 'Av. Reforma 222, CDMX', direccion: 'Av. Reforma 222, CDMX',
colonia: 'Zona Centro', // ← NUEVO
), ),
Domicilio( Domicilio(
id: '2', id: '2',
etiqueta: 'Trabajo', etiqueta: 'Trabajo',
direccion: 'Benito Juárez 45', direccion: 'Benito Juárez 45',
colonia: 'Las Arboledas', // ← NUEVO
), ),
]; ];
@@ -57,13 +63,15 @@ class _GestionDomiciliosScreenState
notificarInicioRuta: _notificarInicioRuta, notificarInicioRuta: _notificarInicioRuta,
notificarAproximacion: _notificarAproximacion, notificarAproximacion: _notificarAproximacion,
notificarRetrasosFallas: _notificarRetrasosFallas, notificarRetrasosFallas: _notificarRetrasosFallas,
onDomicilioGuardado: (String etiqueta, String direccion) { // ← ahora recibe también colonia
onDomicilioGuardado: (String etiqueta, String direccion, String colonia) {
setState(() { setState(() {
_misDomicilios.add( _misDomicilios.add(
Domicilio( Domicilio(
id: DateTime.now().toString(), id: DateTime.now().toString(),
etiqueta: etiqueta, etiqueta: etiqueta,
direccion: direccion, direccion: direccion,
colonia: colonia, // ← NUEVO
), ),
); );
}); });
@@ -108,16 +116,117 @@ class _GestionDomiciliosScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.grey[100], body: Stack(
children: [
appBar: AppBar( Container(
title: const Text('Mis Domicilios'), width: double.infinity,
backgroundColor: Colors.green, height: double.infinity,
foregroundColor: Colors.white, color: const Color(0xFF5BB8E8),
),
actions: [ Positioned(
IconButton( top: 80,
icon: const Icon(Icons.notifications_outlined), 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),
),
),
),
),
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)),
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', tooltip: 'Alertas y Notificaciones',
onPressed: () async { onPressed: () async {
final resultado = await Navigator.push( final resultado = await Navigator.push(
@@ -130,7 +239,6 @@ class _GestionDomiciliosScreenState
), ),
), ),
); );
if (resultado != null) { if (resultado != null) {
setState(() { setState(() {
_notificarInicioRuta = resultado['inicio']; _notificarInicioRuta = resultado['inicio'];
@@ -140,29 +248,88 @@ class _GestionDomiciliosScreenState
} }
}, },
), ),
],
), ),
body: Column( // ── Botón Mi Ruta ──
children: [ const SizedBox(width: 8),
Container(
// ← TARJETA ETA corregida decoration: BoxDecoration(
const Padding( color: const Color(0xFF1E88E5),
padding: EdgeInsets.all(16), borderRadius: BorderRadius.circular(12),
child: TarjetaEtaWidget(), // ← child bien cerrado aquí ),
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(),
),
);
},
),
),
],
),
),
), ),
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: Align( child: TarjetaEtaWidget(),
alignment: Alignment.centerLeft, ),
child: Text(
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),
),
),
const SizedBox(width: 8),
const Text(
'Lugares Registrados', 'Lugares Registrados',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
), ),
],
), ),
), ),
@@ -170,29 +337,54 @@ class _GestionDomiciliosScreenState
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
padding: const EdgeInsets.only(bottom: 100),
itemCount: _misDomicilios.length, itemCount: _misDomicilios.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final domicilio = _misDomicilios[index]; final domicilio = _misDomicilios[index];
return Card( final cardColors = [
margin: const EdgeInsets.symmetric( const Color(0xFF4CAF50),
horizontal: 16, const Color(0xFF1E88E5),
vertical: 8, 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),
), ),
shape: RoundedRectangleBorder( ],
borderRadius: BorderRadius.circular(15),
), ),
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 4),
leading: CircleAvatar( leading: CircleAvatar(
backgroundColor: Colors.green[100], backgroundColor: Colors.white.withValues(alpha: 0.3),
child: Icon( child: Icon(
_obtenerIcono(domicilio.etiqueta), _obtenerIcono(domicilio.etiqueta),
color: Colors.green, color: Colors.white,
), ),
), ),
title: Text(domicilio.etiqueta), title: Text(
subtitle: Text(domicilio.direccion), 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( trailing: IconButton(
icon: const Icon(Icons.delete, color: Colors.red), icon: const Icon(Icons.delete, color: Colors.white),
onPressed: () { onPressed: () {
setState(() { setState(() {
_misDomicilios.removeAt(index); _misDomicilios.removeAt(index);
@@ -206,14 +398,108 @@ class _GestionDomiciliosScreenState
), ),
], ],
), ),
],
),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: _mostrarFormularioAgregar, onPressed: _mostrarFormularioAgregar,
backgroundColor: Colors.green, backgroundColor: const Color(0xFFE65100),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: const StadiumBorder(),
icon: const Icon(Icons.add), 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;
}

359
lib/guia_separacion.dart Normal file
View File

@@ -0,0 +1,359 @@
import 'package:flutter/material.dart';
// ─── DATOS DE CATEGORÍAS (sin conexión, todo local) ─────────────
const List<Map<String, dynamic>> _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<GuiaSeparacionScreen> createState() => _GuiaSeparacionScreenState();
}
class _GuiaSeparacionScreenState extends State<GuiaSeparacionScreen> {
// 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<String>).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<String>).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),
],
),
);
}
}

View File

@@ -11,18 +11,13 @@ class LoginScreen extends StatefulWidget {
class _LoginScreenState extends State<LoginScreen> { class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController emailController = final TextEditingController emailController = TextEditingController();
TextEditingController(); final TextEditingController passwordController = TextEditingController();
final TextEditingController passwordController =
TextEditingController();
bool ocultarPassword = true; bool ocultarPassword = true;
void iniciarSesion() { void iniciarSesion() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
// Simulación de login exitoso
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Inicio de sesión exitoso'), content: Text('Inicio de sesión exitoso'),
@@ -30,12 +25,10 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
); );
// Navegar a la pantalla principal
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) => const GestionDomiciliosScreen(),
const GestionDomiciliosScreen(),
), ),
); );
} }
@@ -44,190 +37,455 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Colors.white, body: Stack(
children: [
// ── Fondo azul cielo ──
Container(
width: double.infinity,
height: double.infinity,
color: const Color(0xFF5BB8E8),
),
body: Center( // ── 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)),
],
),
// ── 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: SingleChildScrollView(
padding: const EdgeInsets.all(25), child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 50),
// Logo // Título ECOUBICEL
CircleAvatar(
radius: 50,
backgroundColor: Colors.green.shade100,
child: Icon(
Icons.recycling,
size: 60,
color: Colors.green.shade700,
),
),
const SizedBox(height: 20),
const Text( const Text(
'EcoRecolección', 'ECOUBICEL.',
style: TextStyle( style: TextStyle(
fontSize: 28, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w900,
color: Colors.black87,
letterSpacing: 2,
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 16),
Text( // Botes de basura
'Inicia sesión para continuar', Row(
style: TextStyle( mainAxisAlignment: MainAxisAlignment.center,
color: Colors.grey.shade600, children: [
fontSize: 16, _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: 40), const SizedBox(height: 28),
// Campo correo // Campo correo — barra verde
TextFormField( _buildColorField(
controller: emailController, controller: emailController,
hint: 'Correo Electrónico',
decoration: InputDecoration( icon: Icons.email,
labelText: 'Correo Electrónico', color: const Color(0xFF4CAF50),
hintText: 'ejemplo@correo.com', obscure: false,
prefixIcon: const Icon(Icons.email),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(15),
),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) return 'Ingresa tu correo';
return 'Ingresa tu correo'; if (!value.contains('@')) return 'Correo inválido';
}
if (!value.contains('@')) {
return 'Correo inválido';
}
return null; return null;
}, },
), ),
const SizedBox(height: 20), const SizedBox(height: 12),
// Campo contraseña // Campo contraseña — barra azul
TextFormField( _buildColorField(
controller: passwordController, controller: passwordController,
obscureText: ocultarPassword, hint: 'Contraseña',
icon: Icons.lock,
decoration: InputDecoration( color: const Color(0xFF1E88E5),
labelText: 'Contraseña', obscure: ocultarPassword,
prefixIcon:
const Icon(Icons.lock),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(
ocultarPassword ocultarPassword ? Icons.visibility_off : Icons.visibility,
? Icons.visibility_off color: Colors.white70,
: Icons.visibility,
), ),
onPressed: () => setState(() => ocultarPassword = !ocultarPassword),
onPressed: () {
setState(() {
ocultarPassword =
!ocultarPassword;
});
},
), ),
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(15),
),
),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) return 'Ingresa tu contraseña';
return 'Ingresa tu contraseña'; if (value.length < 6) return 'Mínimo 6 caracteres';
}
if (value.length < 6) {
return 'Mínimo 6 caracteres';
}
return null; return null;
}, },
), ),
const SizedBox(height: 30), const SizedBox(height: 12),
// Botón iniciar sesión // Botón iniciar sesión — barra naranja
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 55, height: 48,
child: ElevatedButton( child: ElevatedButton(
onPressed: iniciarSesion, onPressed: iniciarSesion,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.green, backgroundColor: const Color(0xFFE65100),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: const StadiumBorder(),
shape: RoundedRectangleBorder( elevation: 3,
borderRadius:
BorderRadius.circular(15),
), ),
),
child: const Text( child: const Text(
'Iniciar Sesión', 'Iniciar Sesión',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 12),
// Registro // Botón registrarse — barra gris
Row( SizedBox(
mainAxisAlignment: width: double.infinity,
MainAxisAlignment.center, height: 48,
child: OutlinedButton(
children: [
const Text(
'¿No tienes cuenta?',
),
TextButton(
onPressed: () {}, onPressed: () {},
style: OutlinedButton.styleFrom(
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.white54, width: 2),
backgroundColor: Colors.white24,
shape: const StadiumBorder(),
),
child: const Text( child: const Text(
'Registrarse', '¿No tienes cuenta? Registrarse',
style: TextStyle( style: TextStyle(
color: Colors.green, fontSize: 14,
fontWeight: FontWeight.bold, 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;
}

398
lib/ruta_exclusiva.dart Normal file
View File

@@ -0,0 +1,398 @@
import 'package:flutter/material.dart';
import 'camion_estado.dart';
// ─── DATOS DE COLONIAS (de tu colonias-rutas.json) ──────────────
const List<Map<String, String>> _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<RutaExclusivaScreen> createState() => _RutaExclusivaScreenState();
}
class _RutaExclusivaScreenState extends State<RutaExclusivaScreen> {
Map<String, String>? _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>(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,
),
),
],
),
],
),
);
}
}