930 lines
35 KiB
Dart
930 lines
35 KiB
Dart
// ================================================================
|
|
// lib/screens/home_screen.dart (v2)
|
|
// ================================================================
|
|
//
|
|
// CAMBIOS v2:
|
|
// - Muestra el nombre del usuario (guardado en prefs)
|
|
// - Botón de cerrar sesión con confirmación (dialog)
|
|
// - Botón de cambiar contraseña en el menú
|
|
// - Datos del usuario cargados desde prefs (más rápido, sin esperar API)
|
|
// ================================================================
|
|
|
|
import 'dart:async';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import '../services/api_service.dart';
|
|
|
|
class HomeScreen extends StatefulWidget {
|
|
const HomeScreen({super.key});
|
|
|
|
@override
|
|
State<HomeScreen> createState() => _HomeScreenState();
|
|
}
|
|
|
|
class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
|
|
int? _usuarioId;
|
|
String _nombreUsuario = '';
|
|
ETAInfo? _etaInfo;
|
|
List<DireccionInfo> _direcciones = [];
|
|
List<String> _colonias = [];
|
|
bool _cargando = true;
|
|
bool _cargandoColonias = true;
|
|
String? _error;
|
|
String? _errorColonias;
|
|
|
|
Timer? _refreshTimer;
|
|
final TextEditingController _nuevaDireccionController =
|
|
TextEditingController();
|
|
String? _nuevaColoniaSeleccionada;
|
|
|
|
late AnimationController _pulseController;
|
|
late Animation<double> _pulseAnimation;
|
|
|
|
final ApiService _apiService = ApiService();
|
|
|
|
// ----------------------------------------------------------------
|
|
// LIFECYCLE
|
|
// ----------------------------------------------------------------
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_pulseController =
|
|
AnimationController(vsync: this, duration: const Duration(seconds: 2))
|
|
..repeat(reverse: true);
|
|
_pulseAnimation = Tween<double>(begin: 1.0, end: 1.05).animate(
|
|
CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
if (_usuarioId == null) {
|
|
final args = ModalRoute.of(context)?.settings.arguments;
|
|
if (args is int) {
|
|
_usuarioId = args;
|
|
_inicializar();
|
|
} else {
|
|
_cargarDesdeStorage();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_pulseController.dispose();
|
|
_refreshTimer?.cancel();
|
|
_nuevaDireccionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// INICIALIZACIÓN
|
|
// ----------------------------------------------------------------
|
|
|
|
Future<void> _inicializar() async {
|
|
// Carga el nombre desde prefs inmediatamente (sin esperar la API)
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (mounted) {
|
|
setState(() => _nombreUsuario = prefs.getString('nombre') ?? '');
|
|
}
|
|
_cargarETA();
|
|
_iniciarAutoRefresh();
|
|
_registrarFCMToken();
|
|
_cargarUsuario();
|
|
_cargarColonias();
|
|
}
|
|
|
|
Future<void> _cargarDesdeStorage() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final id = prefs.getInt('usuario_id');
|
|
if (id != null) {
|
|
_usuarioId = id;
|
|
_inicializar();
|
|
} else {
|
|
if (mounted) Navigator.pushReplacementNamed(context, '/');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// CARGA DE DATOS
|
|
// ----------------------------------------------------------------
|
|
|
|
Future<void> _cargarETA() async {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_cargando = true;
|
|
_error = null;
|
|
});
|
|
|
|
try {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final usuarioId = prefs.getInt('usuario_id') ?? 1;
|
|
final etaInfo = await _apiService.obtenerETA(usuarioId);
|
|
if (mounted) {
|
|
setState(() {
|
|
_etaInfo = etaInfo;
|
|
_cargando = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error backend ETA: $e');
|
|
// Mock de emergencia para demos/hackathon
|
|
if (mounted) {
|
|
setState(() {
|
|
_etaInfo = ETAInfo(
|
|
usuarioId: 1,
|
|
colonia: "Centro",
|
|
rutaNombre: "Ruta Poniente - Camión #4",
|
|
rutaStatus: "EN_RUTA",
|
|
gpsOk: true,
|
|
etaTexto: "12 minutos aprox.",
|
|
etaMinutos: 12,
|
|
mensajePreventivo:
|
|
"⚠️ El camión está a 3 cuadras. ¡Prepara tus bolsas!",
|
|
);
|
|
_cargando = false;
|
|
_error = null;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _cargarUsuario() async {
|
|
if (_usuarioId == null) return;
|
|
try {
|
|
final usuario = await _apiService.obtenerUsuario(_usuarioId!);
|
|
if (mounted) {
|
|
setState(() {
|
|
_direcciones = usuario.direcciones;
|
|
// Actualizar nombre si el API devuelve uno diferente
|
|
if (usuario.nombre.isNotEmpty) _nombreUsuario = usuario.nombre;
|
|
});
|
|
// Guardar nombre actualizado en prefs
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('nombre', usuario.nombre);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error cargando usuario: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _cargarColonias() async {
|
|
try {
|
|
final colonias = await _apiService.obtenerColonias();
|
|
if (mounted) {
|
|
setState(() {
|
|
_colonias = colonias;
|
|
_cargandoColonias = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
setState(() {
|
|
_colonias = [
|
|
'Zona Centro',
|
|
'Las Arboledas',
|
|
'Trojes',
|
|
'San Juanico',
|
|
'Los Olivos',
|
|
'Rancho Seco',
|
|
'Las Insurgentes'
|
|
];
|
|
_cargandoColonias = false;
|
|
_errorColonias = 'Sin conexión. Usando lista local.';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _iniciarAutoRefresh() {
|
|
_refreshTimer =
|
|
Timer.periodic(const Duration(seconds: 60), (_) => _cargarETA());
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// FIREBASE
|
|
// ----------------------------------------------------------------
|
|
Future<void> _registrarFCMToken() async {
|
|
try {
|
|
final messaging = FirebaseMessaging.instance;
|
|
final settings = await messaging.requestPermission(
|
|
alert: true, sound: true, badge: true);
|
|
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
|
|
final token = await messaging.getToken();
|
|
if (token != null && _usuarioId != null) {
|
|
await _apiService.registrarFcmToken(_usuarioId!, token);
|
|
}
|
|
}
|
|
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
|
if (message.notification != null && mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text('🚛 ${message.notification!.body}'),
|
|
backgroundColor: Colors.green.shade700,
|
|
duration: const Duration(seconds: 5),
|
|
));
|
|
_cargarETA();
|
|
}
|
|
});
|
|
} catch (e) {
|
|
debugPrint('Error FCM: $e');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// CERRAR SESIÓN — Con dialog de confirmación
|
|
// ----------------------------------------------------------------
|
|
Future<void> _confirmarCerrarSesion() async {
|
|
final confirmar = await showDialog<bool>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Cerrar sesión'),
|
|
content: const Text('¿Seguro que quieres cerrar sesión?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(false),
|
|
child: const Text('Cancelar'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(ctx).pop(true),
|
|
style:
|
|
ElevatedButton.styleFrom(backgroundColor: Colors.red.shade700),
|
|
child: const Text('Cerrar sesión',
|
|
style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirmar == true) {
|
|
_refreshTimer?.cancel();
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.clear();
|
|
if (mounted) Navigator.pushReplacementNamed(context, '/');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// CAMBIAR CONTRASEÑA
|
|
// ----------------------------------------------------------------
|
|
Future<void> _mostrarCambioPassword() async {
|
|
final actualCtrl = TextEditingController();
|
|
final nuevoCtrl = TextEditingController();
|
|
bool mostrarActual = false;
|
|
bool mostrarNuevo = false;
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setDialogState) => AlertDialog(
|
|
title: const Text('Cambiar contraseña'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
TextField(
|
|
controller: actualCtrl,
|
|
obscureText: !mostrarActual,
|
|
decoration: InputDecoration(
|
|
labelText: 'Contraseña actual',
|
|
prefixIcon: const Icon(Icons.lock_outline),
|
|
suffixIcon: IconButton(
|
|
icon: Icon(mostrarActual
|
|
? Icons.visibility_off
|
|
: Icons.visibility),
|
|
onPressed: () =>
|
|
setDialogState(() => mostrarActual = !mostrarActual),
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: nuevoCtrl,
|
|
obscureText: !mostrarNuevo,
|
|
decoration: InputDecoration(
|
|
labelText: 'Nueva contraseña',
|
|
hintText: 'Mínimo 6 caracteres',
|
|
prefixIcon: const Icon(Icons.lock_reset),
|
|
suffixIcon: IconButton(
|
|
icon: Icon(mostrarNuevo
|
|
? Icons.visibility_off
|
|
: Icons.visibility),
|
|
onPressed: () =>
|
|
setDialogState(() => mostrarNuevo = !mostrarNuevo),
|
|
),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(10)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(ctx).pop(),
|
|
child: const Text('Cancelar'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
if (_usuarioId == null) return;
|
|
final actual = actualCtrl.text;
|
|
final nuevo = nuevoCtrl.text;
|
|
if (actual.isEmpty || nuevo.isEmpty) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Completa ambos campos.')),
|
|
);
|
|
return;
|
|
}
|
|
if (nuevo.length < 6) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text(
|
|
'La nueva contraseña debe tener al menos 6 caracteres.')),
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
await _apiService.actualizarPassword(
|
|
_usuarioId!, actual, nuevo);
|
|
if (mounted) {
|
|
Navigator.of(ctx).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content:
|
|
Text('✅ Contraseña actualizada correctamente.'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
final msg = e.toString().replaceFirst('Exception: ', '');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(msg),
|
|
backgroundColor: Colors.red.shade700),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: const Text('Guardar'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
actualCtrl.dispose();
|
|
nuevoCtrl.dispose();
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// AGREGAR DIRECCIÓN
|
|
// ----------------------------------------------------------------
|
|
Future<void> _agregarDireccion() async {
|
|
if (_usuarioId == null) return;
|
|
final messenger = ScaffoldMessenger.of(context);
|
|
final direccion = _nuevaDireccionController.text.trim();
|
|
if (_nuevaColoniaSeleccionada == null) {
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('Selecciona una colonia.')));
|
|
return;
|
|
}
|
|
if (direccion.isEmpty) {
|
|
messenger
|
|
.showSnackBar(const SnackBar(content: Text('Ingresa la dirección.')));
|
|
return;
|
|
}
|
|
try {
|
|
await _apiService.agregarDireccion(
|
|
_usuarioId!, _nuevaColoniaSeleccionada!, direccion);
|
|
_nuevaDireccionController.clear();
|
|
_nuevaColoniaSeleccionada = null;
|
|
await _cargarUsuario();
|
|
await _cargarETA();
|
|
if (mounted) {
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('✅ Dirección agregada.')));
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
messenger.showSnackBar(
|
|
const SnackBar(content: Text('No se pudo agregar la dirección.')));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _mostrarAgregarDireccionDialog() async {
|
|
if (_cargandoColonias) await _cargarColonias();
|
|
if (!mounted) return;
|
|
_nuevaDireccionController.clear();
|
|
_nuevaColoniaSeleccionada = null;
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Agregar nueva dirección'),
|
|
content: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
if (_errorColonias != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: Text(_errorColonias!,
|
|
style: TextStyle(
|
|
color: Colors.orange.shade700, fontSize: 12)),
|
|
),
|
|
TextField(
|
|
controller: _nuevaDireccionController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Dirección', hintText: 'Calle, número'),
|
|
),
|
|
const SizedBox(height: 16),
|
|
DropdownButtonFormField<String>(
|
|
value: _nuevaColoniaSeleccionada,
|
|
hint: const Text('Selecciona tu colonia'),
|
|
items: _colonias
|
|
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
|
|
.toList(),
|
|
onChanged: (v) => setState(() => _nuevaColoniaSeleccionada = v),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancelar')),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final navigator = Navigator.of(context);
|
|
await _agregarDireccion();
|
|
if (mounted) navigator.pop();
|
|
},
|
|
child: const Text('Guardar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ================================================================
|
|
// HELPERS DE UI
|
|
// ================================================================
|
|
|
|
Color _colorSegunETA(int eta) {
|
|
if (eta <= 5) return const Color(0xFFB71C1C);
|
|
if (eta <= 15) return const Color(0xFFF57F17);
|
|
if (eta <= 30) return const Color(0xFF1B5E20);
|
|
return const Color(0xFF1A237E);
|
|
}
|
|
|
|
String _emojiSegunETA(int eta) {
|
|
if (eta <= 5) return '🔴';
|
|
if (eta <= 15) return '🟡';
|
|
if (eta <= 30) return '🟢';
|
|
return '🔵';
|
|
}
|
|
|
|
// ================================================================
|
|
// BUILD
|
|
// ================================================================
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
body: AnimatedContainer(
|
|
duration: const Duration(milliseconds: 800),
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: _etaInfo != null
|
|
? [
|
|
_colorSegunETA(_etaInfo!.etaMinutos),
|
|
_colorSegunETA(_etaInfo!.etaMinutos).withValues(alpha: 0.7)
|
|
]
|
|
: [const Color(0xFF2E7D32), const Color(0xFF1B5E20)],
|
|
),
|
|
),
|
|
child: SafeArea(
|
|
child: _cargando
|
|
? const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
CircularProgressIndicator(color: Colors.white),
|
|
SizedBox(height: 16),
|
|
Text('Consultando estado del camión...',
|
|
style:
|
|
TextStyle(color: Colors.white70, fontSize: 16)),
|
|
]))
|
|
: _error != null
|
|
? Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.wifi_off_rounded,
|
|
size: 80, color: Colors.white54),
|
|
const SizedBox(height: 16),
|
|
Text(_error!,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 18)),
|
|
const SizedBox(height: 32),
|
|
ElevatedButton.icon(
|
|
onPressed: _cargarETA,
|
|
icon: const Icon(Icons.refresh),
|
|
label: const Text('Reintentar'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.red.shade700),
|
|
),
|
|
]),
|
|
))
|
|
: _buildUIConDatos(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildUIConDatos() {
|
|
if (_etaInfo == null) return const SizedBox.shrink();
|
|
final eta = _etaInfo!;
|
|
|
|
return SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
// ── HEADER ──────────────────────────────────────────────
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
// Saludo con nombre
|
|
Flexible(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (_nombreUsuario.isNotEmpty)
|
|
Text(
|
|
'Hola, ${_nombreUsuario.split(' ').first} 👋',
|
|
style: const TextStyle(
|
|
color: Colors.white70, fontSize: 13),
|
|
),
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.location_on,
|
|
color: Colors.white70, size: 16),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
eta.colonia,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w700,
|
|
fontSize: 16),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// Menú de acciones
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => Navigator.pushNamed(context, '/routes'),
|
|
icon: const Icon(Icons.list_alt, color: Colors.white70),
|
|
tooltip: 'Ver rutas',
|
|
),
|
|
// Menú de 3 puntos con opciones extra
|
|
PopupMenuButton<String>(
|
|
icon: const Icon(Icons.more_vert, color: Colors.white70),
|
|
onSelected: (value) {
|
|
if (value == 'password') _mostrarCambioPassword();
|
|
if (value == 'logout') _confirmarCerrarSesion();
|
|
},
|
|
itemBuilder: (_) => [
|
|
const PopupMenuItem(
|
|
value: 'password',
|
|
child: Row(children: [
|
|
Icon(Icons.lock_reset, size: 20),
|
|
SizedBox(width: 10),
|
|
Text('Cambiar contraseña'),
|
|
]),
|
|
),
|
|
const PopupMenuDivider(),
|
|
const PopupMenuItem(
|
|
value: 'logout',
|
|
child: Row(children: [
|
|
Icon(Icons.logout, size: 20, color: Colors.red),
|
|
SizedBox(width: 10),
|
|
Text('Cerrar sesión',
|
|
style: TextStyle(color: Colors.red)),
|
|
]),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
// ── INFO DE RUTA ─────────────────────────────────────────
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Flexible(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(eta.rutaNombre,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Text('Status: ${eta.rutaStatus}',
|
|
style: const TextStyle(
|
|
color: Colors.white70, fontSize: 13)),
|
|
],
|
|
),
|
|
),
|
|
const Icon(Icons.local_shipping_rounded,
|
|
color: Colors.white70, size: 24),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── ALERTA GPS ───────────────────────────────────────────
|
|
if (!eta.gpsOk)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(14),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade700.withValues(alpha: 0.2),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: Colors.red.shade300),
|
|
),
|
|
child: const Row(children: [
|
|
Icon(Icons.gps_off, color: Colors.white70),
|
|
SizedBox(width: 10),
|
|
Expanded(
|
|
child: Text(
|
|
'GPS del camión desconectado. Recibirás una alerta si persiste.',
|
|
style: TextStyle(color: Colors.white, fontSize: 14))),
|
|
]),
|
|
),
|
|
),
|
|
|
|
// ── CÍRCULO ETA ──────────────────────────────────────────
|
|
const SizedBox(height: 32),
|
|
ScaleTransition(
|
|
scale: _pulseAnimation,
|
|
child: Container(
|
|
width: 220,
|
|
height: 220,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: Colors.white.withValues(alpha: 0.15),
|
|
border: Border.all(color: Colors.white, width: 3),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(_emojiSegunETA(eta.etaMinutos),
|
|
style: const TextStyle(fontSize: 48)),
|
|
const SizedBox(height: 4),
|
|
Text('${eta.etaMinutos}',
|
|
style: const TextStyle(
|
|
fontSize: 64,
|
|
fontWeight: FontWeight.w900,
|
|
color: Colors.white,
|
|
height: 1)),
|
|
const Text('minutos',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
color: Colors.white70,
|
|
fontWeight: FontWeight.w300)),
|
|
]),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
|
child: Text(eta.etaTexto,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w500)),
|
|
),
|
|
|
|
// ── MENSAJE PREVENTIVO ───────────────────────────────────
|
|
const SizedBox(height: 32),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 24),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.2),
|
|
blurRadius: 20,
|
|
offset: const Offset(0, 8))
|
|
],
|
|
),
|
|
child: Text(
|
|
eta.mensajePreventivo,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
fontSize: 22,
|
|
fontWeight: FontWeight.w800,
|
|
color: _colorSegunETA(eta.etaMinutos),
|
|
height: 1.3),
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── DIRECCIONES REGISTRADAS ──────────────────────────────
|
|
const SizedBox(height: 24),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.14),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Direcciones registradas',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 12),
|
|
if (_direcciones.isEmpty)
|
|
const Text('No tienes direcciones registradas aún.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 14))
|
|
else
|
|
for (final d in _direcciones)
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(d.colonia,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 2),
|
|
Text(d.direccion,
|
|
style: const TextStyle(
|
|
color: Colors.white70, fontSize: 13)),
|
|
]),
|
|
),
|
|
const SizedBox(height: 10),
|
|
ElevatedButton.icon(
|
|
onPressed: _mostrarAgregarDireccionDialog,
|
|
icon: const Icon(Icons.add_location_alt_outlined),
|
|
label: const Text('Agregar dirección'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white.withValues(alpha: 0.18),
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(14)),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── INFO DE SEPARACIÓN ───────────────────────────────────
|
|
const SizedBox(height: 20),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.all(18),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: const Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Información relevante',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold)),
|
|
SizedBox(height: 12),
|
|
Text(
|
|
'• Separa orgánicos y reciclables. No mezcles líquidos con plásticos.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'• Saca tu basura a la acera solo cuando recibas la alerta de proximidad.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'• Si el GPS del camión se desconecta, recibirás una alerta de seguimiento.',
|
|
style: TextStyle(color: Colors.white70, fontSize: 14)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
// ── BOTÓN VER RUTAS ──────────────────────────────────────
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => Navigator.pushNamed(context, '/routes'),
|
|
icon: const Icon(Icons.local_shipping_rounded),
|
|
label: const Text('Ver estado de rutas y simular avance'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.green.shade900,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => Navigator.pushNamed(context, '/info'),
|
|
icon: const Icon(Icons.eco_rounded),
|
|
label: const Text('Información relevante'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: Colors.green.shade900,
|
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// ── FOOTER: REFRESH ──────────────────────────────────────
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 32),
|
|
child: Column(children: [
|
|
const Text('🔄 Se actualiza automáticamente cada minuto',
|
|
style: TextStyle(color: Colors.white54, fontSize: 12)),
|
|
const SizedBox(height: 12),
|
|
OutlinedButton.icon(
|
|
onPressed: _cargarETA,
|
|
icon: const Icon(Icons.refresh, color: Colors.white),
|
|
label: const Text('Actualizar ahora',
|
|
style: TextStyle(color: Colors.white)),
|
|
style: OutlinedButton.styleFrom(
|
|
side: const BorderSide(color: Colors.white54),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(20)),
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|