424 lines
19 KiB
Dart
424 lines
19 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../../core/router/app_router.dart';
|
|
|
|
class HomeScreenPlaceholder extends StatefulWidget {
|
|
const HomeScreenPlaceholder({super.key});
|
|
|
|
@override
|
|
State<HomeScreenPlaceholder> createState() => _HomeScreenPlaceholderState();
|
|
}
|
|
|
|
class _HomeScreenPlaceholderState extends State<HomeScreenPlaceholder> {
|
|
// 1. Catálogo de Colonias tipado fuertemente con tu nuevo modelo (Mapeo de tu JSON)
|
|
final List<ColoniaModel> _coloniasData = [
|
|
ColoniaModel(colonia: "Zona Centro", routeId: "RUTA-01", horarioEstimado: "Matutino (06:30 - 07:15)"),
|
|
ColoniaModel(colonia: "Las Arboledas", routeId: "RUTA-01", horarioEstimado: "Matutino (07:00 - 07:30)"),
|
|
ColoniaModel(colonia: "Trojes", routeId: "RUTA-13", horarioEstimado: "Matutino (06:40 - 07:10)"),
|
|
ColoniaModel(colonia: "San Juanico", routeId: "RUTA-03", horarioEstimado: "Matutino (06:45 - 07:15)"),
|
|
ColoniaModel(colonia: "Los Olivos", routeId: "RUTA-04", horarioEstimado: "Matutino (07:00 - 07:40)"),
|
|
ColoniaModel(colonia: "Rancho Seco", routeId: "RUTA-05", horarioEstimado: "Vespertino (14:15 - 15:00)"),
|
|
ColoniaModel(colonia: "Las Insurgentes", routeId: "RUTA-12", horarioEstimado: "Matutino (06:35 - 07:10)")
|
|
];
|
|
|
|
// 2. Diccionario de Telemetría Satelital de Celaya (Mapeo de tus imágenes)
|
|
final Map<String, Map<String, dynamic>> _routesTelemetry = {
|
|
"RUTA-01": {
|
|
"name": "Zona Centro - Las Arboledas",
|
|
"truckId": 101,
|
|
"positions": [
|
|
{"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Salida Relleno Sanitario", "mins": "45"},
|
|
{"positionId": 2, "lat": 20.5185, "lng": -100.8450, "speed": 45, "desc": "En trayecto principal", "mins": "30"},
|
|
{"positionId": 3, "lat": 20.5215, "lng": -100.8142, "speed": 22, "desc": "Ingresando a zona Centro", "mins": "20"},
|
|
{"positionId": 4, "lat": 20.5212, "lng": -100.8175, "speed": 15, "desc": "Punto Previo Destino (<15 min)", "mins": "12"},
|
|
{"positionId": 5, "lat": 20.5210, "lng": -100.8210, "speed": 0, "desc": "Recolección Activa de Residuos", "mins": "5"},
|
|
{"positionId": 6, "lat": 20.5235, "lng": -100.8212, "speed": 18, "desc": "Avanzando sector Arboledas", "mins": "3"},
|
|
{"positionId": 7, "lat": 20.5260, "lng": -100.8215, "speed": 20, "desc": "Última parada del circuito", "mins": "1"},
|
|
{"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 40, "desc": "Retorno al Basurero Municipal", "mins": "0"}
|
|
]
|
|
},
|
|
"RUTA-03": {
|
|
"name": "Sector Poniente - San Juanico",
|
|
"truckId": 103,
|
|
"positions": [
|
|
{"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Salida Base de Monitoreo", "mins": "40"},
|
|
{"positionId": 2, "lat": 20.5250, "lng": -100.8510, "speed": 42, "desc": "Vía Rápida Poniente", "mins": "25"},
|
|
{"positionId": 3, "lat": 20.5290, "lng": -100.8320, "speed": 20, "desc": "Eje Norponiente", "mins": "18"},
|
|
{"positionId": 4, "lat": 20.5315, "lng": -100.8355, "speed": 15, "desc": "Avenida San Juanico", "mins": "10"},
|
|
{"positionId": 5, "lat": 20.5340, "lng": -100.8390, "speed": 0, "desc": "Vaciado de Contenedores Urbano", "mins": "6"},
|
|
{"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 35, "desc": "Retorno General", "mins": "0"}
|
|
]
|
|
},
|
|
"RUTA-04": {
|
|
"name": "Oriente - Los Olivos",
|
|
"truckId": 104,
|
|
"positions": [
|
|
{"positionId": 1, "lat": 20.5111, "lng": -100.9037, "speed": 0, "desc": "Encendido de Unidad", "mins": "50"},
|
|
{"positionId": 4, "lat": 20.5320, "lng": -100.7850, "speed": 12, "desc": "Proximidad Los Olivos", "mins": "14"},
|
|
{"positionId": 5, "lat": 20.5350, "lng": -100.7790, "speed": 0, "desc": "Recolección Casa por Casa", "mins": "8"},
|
|
{"positionId": 8, "lat": 20.5111, "lng": -100.9037, "speed": 48, "desc": "Retorno a Relleno Sanitario", "mins": "0"}
|
|
]
|
|
}
|
|
};
|
|
|
|
int _activePositionIndex = 0;
|
|
|
|
// 📍 Historial tipado de forma segura con el modelo de notificaciones push
|
|
final List<NotificationModel> _pushNotificationsLog = [];
|
|
|
|
String _userColonia = "Zona Centro";
|
|
bool _isInitialized = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_userColonia = "Zona Centro"; // Forzado de respaldo seguro
|
|
_isInitialized = true;
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
_triggerNotificationCheck();
|
|
});
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
try {
|
|
final uri = GoRouterState.of(context).uri;
|
|
final String? coloniaParam = uri.queryParameters['colonia'];
|
|
|
|
if (coloniaParam != null &&
|
|
coloniaParam.isNotEmpty &&
|
|
_coloniasData.any((e) => e.colonia.toLowerCase() == coloniaParam.toLowerCase())) {
|
|
|
|
final elementoEncontrado = _coloniasData.firstWhere(
|
|
(e) => e.colonia.toLowerCase() == coloniaParam.toLowerCase()
|
|
);
|
|
setState(() {
|
|
_userColonia = elementoEncontrado.colonia;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Manejo silencioso de GoRouter
|
|
}
|
|
}
|
|
|
|
// Lógica interactiva que dispara las notificaciones basadas en tu modelo real
|
|
void _triggerNotificationCheck() {
|
|
final String currentRouteId = _getColoniaInfo().routeId;
|
|
if (!_routesTelemetry.containsKey(currentRouteId)) return;
|
|
|
|
final telemetry = _routesTelemetry[currentRouteId]!;
|
|
final currentPos = (telemetry["positions"] as List)[_activePositionIndex];
|
|
final int pId = currentPos["positionId"];
|
|
|
|
NotificationModel? newAlert;
|
|
|
|
if (pId == 2) {
|
|
newAlert = NotificationModel(
|
|
triggerEvent: "ROUTE_START",
|
|
condition: "Cuando positionId cambia de 1 a 2",
|
|
pushPayload: PushPayloadModel(
|
|
title: "¡Ruta Iniciada!",
|
|
body: "El camión recolector ha salido del Relleno Sanitario rumbo a tu sector. Asegúrate de tener listos tus residuos."
|
|
)
|
|
);
|
|
} else if (pId == 4) {
|
|
newAlert = NotificationModel(
|
|
triggerEvent: "TRUCK_PROXIMITY",
|
|
condition: "Cuando positionId llega a 4 (punto previo al destino)",
|
|
pushPayload: PushPayloadModel(
|
|
title: "Camión Cercano",
|
|
body: "El camión está a menos de 15 minutos de tu domicilio. Es momento de sacar tus bolsas a la acera."
|
|
)
|
|
);
|
|
} else if (pId == 8) {
|
|
newAlert = NotificationModel(
|
|
triggerEvent: "ROUTE_COMPLETED",
|
|
condition: "Cuando positionId llega a 8 (retorno al basurero)",
|
|
pushPayload: PushPayloadModel(
|
|
title: "Servicio Finalizado",
|
|
body: "El camión de tu sector ha concluido su jornada de recolección diaria."
|
|
)
|
|
);
|
|
}
|
|
|
|
if (newAlert != null) {
|
|
setState(() {
|
|
_pushNotificationsLog.insert(0, newAlert!);
|
|
});
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('🔔 Push: ${newAlert.pushPayload.title}'),
|
|
backgroundColor: pId == 4 ? Colors.amber.shade900 : (pId == 8 ? Colors.blue.shade700 : Colors.green.shade700),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
ColoniaModel _getColoniaInfo() {
|
|
return _coloniasData.firstWhere(
|
|
(element) => element.colonia == _userColonia,
|
|
orElse: () => _coloniasData.first,
|
|
);
|
|
}
|
|
|
|
void _nextSimulationStep() {
|
|
final String currentRouteId = _getColoniaInfo().routeId;
|
|
if (!_routesTelemetry.containsKey(currentRouteId)) return;
|
|
|
|
final positionsList = _routesTelemetry[currentRouteId]!["positions"] as List;
|
|
if (_activePositionIndex < positionsList.length - 1) {
|
|
setState(() {
|
|
_activePositionIndex++;
|
|
});
|
|
_triggerNotificationCheck();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final coloniaInfo = _getColoniaInfo();
|
|
final String routeId = coloniaInfo.routeId;
|
|
|
|
final telemetry = _routesTelemetry[routeId]!;
|
|
final currentPositionData = (telemetry["positions"] as List)[_activePositionIndex];
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF5F5F5),
|
|
appBar: AppBar(
|
|
title: Row(
|
|
children: const [
|
|
Icon(Icons.recycling, color: Color(0xFF2E7D32)),
|
|
SizedBox(width: 8),
|
|
Text('WasteNotify', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.black)),
|
|
],
|
|
),
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.logout, color: Colors.black),
|
|
onPressed: () => context.go('/login'),
|
|
)
|
|
],
|
|
),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: 0,
|
|
onDestinationSelected: (index) {
|
|
if (index == 1) {
|
|
context.go(AppRoutes.addresses);
|
|
}
|
|
},
|
|
destinations: const [
|
|
NavigationDestination(
|
|
icon: Icon(Icons.home_outlined),
|
|
selectedIcon: Icon(Icons.home),
|
|
label: 'Inicio',
|
|
),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.location_on_outlined),
|
|
selectedIcon: Icon(Icons.location_on),
|
|
label: 'Direcciones',
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// --- 1. TARJETA DE BIENVENIDA ---
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF1B5E20),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const CircleAvatar(
|
|
backgroundColor: Colors.white24,
|
|
radius: 24,
|
|
child: Icon(Icons.person, color: Colors.white, size: 28),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('¡Bienvenido!', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 6),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
|
decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(12)),
|
|
child: const Text('Ciudadano', style: TextStyle(color: Colors.white, fontSize: 12)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
Card(
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
child: ListTile(
|
|
leading: const CircleAvatar(
|
|
backgroundColor: Color(0xFFE8F5E9),
|
|
child: Icon(Icons.add_location_alt, color: Color(0xFF2E7D32)),
|
|
),
|
|
title: const Text('Agregar direcciones'),
|
|
subtitle: const Text('Abre el panel para registrar tus domicilios y vincularlos a tu colonia.'),
|
|
trailing: const Icon(Icons.chevron_right),
|
|
onTap: () => context.go(AppRoutes.addresses),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// --- 2. RELOJ DE TIEMPO ESTIMADO INTERACTIVO ---
|
|
Card(
|
|
color: Colors.white,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
|
elevation: 1,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20.0),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: const [
|
|
Icon(Icons.access_time, color: Color(0xFF2E7D32)),
|
|
SizedBox(width: 10),
|
|
Text('Tiempo estimado de llegada', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Container(
|
|
width: 110,
|
|
height: 110,
|
|
decoration: BoxDecoration(shape: BoxShape.circle, border: Border.all(color: const Color(0xFFE0E0E0), width: 3)),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text('${currentPositionData["mins"]}', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Colors.black)),
|
|
Text('— min', style: TextStyle(fontSize: 14, color: Colors.grey.shade600)),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text('Estado GPS: ${currentPositionData["desc"]}', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: Colors.grey.shade600, fontStyle: FontStyle.italic)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// --- 3. BOTÓN DE SIMULACIÓN PARA EL MVP ---
|
|
ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF2E7D32), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14))),
|
|
onPressed: _activePositionIndex < (telemetry['positions'] as List).length - 1 ? _nextSimulationStep : null,
|
|
icon: const Icon(Icons.play_arrow, color: Colors.white),
|
|
label: const Text('Simular Avance del Camión (GPS)', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// --- 4. BANDEJA DE ALERTAS REALES RECIBIDAS (PARSED) ---
|
|
if (_pushNotificationsLog.isNotEmpty) ...[
|
|
const Text('🔔 Alertas Push en Vivo', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.black)),
|
|
const SizedBox(height: 8),
|
|
..._pushNotificationsLog.map((log) {
|
|
final bool isWarning = log.triggerEvent == 'TRUCK_PROXIMITY';
|
|
final bool isDone = log.triggerEvent == 'ROUTE_COMPLETED';
|
|
final Color cardColor = isWarning ? Colors.amber.shade900 : (isDone ? Colors.blue.shade700 : Colors.green.shade700);
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(vertical: 4),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
child: ListTile(
|
|
leading: CircleAvatar(backgroundColor: cardColor.withOpacity(0.12), child: Icon(isWarning ? Icons.notification_important : (isDone ? Icons.check_circle : Icons.local_shipping), color: cardColor)),
|
|
title: Text(log.pushPayload.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
|
|
subtitle: Text(log.pushPayload.body, style: const TextStyle(fontSize: 12)),
|
|
),
|
|
);
|
|
}).toList(),
|
|
const SizedBox(height: 16),
|
|
],
|
|
|
|
// --- 5. PANEL DE RECOMENDACIONES ---
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(color: const Color(0xFFFFF3E0), borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFFFE0B2))),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(children: const [Icon(Icons.campaign, color: Colors.orange, size: 22), SizedBox(width: 8), Text('Recuerda siempre', style: TextStyle(fontWeight: FontWeight.bold, color: Colors.brown, fontSize: 14))]),
|
|
const SizedBox(height: 12),
|
|
_buildBulletRow(Icons.delete_outline, 'Saca la basura SOLO cuando recibas la alerta de "próxima llegada".'),
|
|
const SizedBox(height: 8),
|
|
_buildBulletRow(Icons.block, 'Nunca persigas ni te acerques al camión. El sistema te avisará a tiempo.'),
|
|
const SizedBox(height: 8),
|
|
_buildBulletRow(Icons.eco_outlined, 'Separar tus residuos hace más eficiente la recolección. ¡Gracias!'),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBulletRow(IconData icon, String text) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(icon, size: 16, color: Colors.brown.shade700),
|
|
const SizedBox(width: 10),
|
|
Expanded(child: Text(text, style: TextStyle(fontSize: 13, color: Colors.brown.shade900, height: 1.2))),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 📍 MODELO 1: Catálogo de Colonias
|
|
// =========================================================================
|
|
class ColoniaModel {
|
|
String colonia;
|
|
String routeId;
|
|
String horarioEstimado;
|
|
|
|
ColoniaModel({required this.colonia, required this.routeId, required this.horarioEstimado});
|
|
|
|
factory ColoniaModel.fromJson(Map<String, dynamic> json) => ColoniaModel(colonia: json['colonia'], routeId: json['routeId'], horarioEstimado: json['horarioEstimado']);
|
|
|
|
Map<String, dynamic> toJson() => {'colonia': colonia, 'routeId': routeId, 'horarioEstimado': horarioEstimado};
|
|
}
|
|
|
|
// =========================================================================
|
|
// 📍 MODELO 2: Sistema de Notificaciones Alertas Push
|
|
// =========================================================================
|
|
class NotificationModel {
|
|
String triggerEvent;
|
|
String condition;
|
|
PushPayloadModel pushPayload;
|
|
|
|
NotificationModel({required this.triggerEvent, required this.condition, required this.pushPayload});
|
|
|
|
factory NotificationModel.fromJson(Map<String, dynamic> json) => NotificationModel(triggerEvent: json['triggerEvent'], condition: json['condition'], pushPayload: PushPayloadModel.fromJson(json['pushPayload']));
|
|
|
|
Map<String, dynamic> toJson() => {'triggerEvent': triggerEvent, 'condition': condition, 'pushPayload': pushPayload.toJson()};
|
|
}
|
|
|
|
class PushPayloadModel {
|
|
String title;
|
|
String body;
|
|
|
|
PushPayloadModel({required this.title, required this.body});
|
|
|
|
factory PushPayloadModel.fromJson(Map<String, dynamic> json) => PushPayloadModel(title: json['title'], body: json['body']);
|
|
|
|
Map<String, dynamic> toJson() => {'title': title, 'body': body};
|
|
}
|