import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import '../theme/app_theme.dart'; import '../models/models.dart'; import '../widgets/widgets.dart' as w; class MapScreen extends StatefulWidget { const MapScreen({super.key}); @override State createState() => _MapScreenState(); } class _MapScreenState extends State { final Completer _mapController = Completer(); // Coordenadas de ejemplo — Celaya, Gto. static const LatLng _casaPos = LatLng(20.5226, -100.8191); static const LatLng _camionPos = LatLng(20.5255, -100.8220); static const CameraPosition _camaraInicial = CameraPosition( target: LatLng(20.5240, -100.8205), zoom: 15.5, ); // Datos de ejemplo del camión final TruckLocation _camion = TruckLocation( id: 'truck-01', ruta: 'Ruta Norte', latitud: _camionPos.latitude, longitud: _camionPos.longitude, ultimaActualizacion: DateTime.now().subtract(const Duration(seconds: 28)), enServicio: true, ); final HouseModel _casa = HouseModel( id: 'casa-01', calle: 'Av. Insurgentes 245', colonia: 'Centro', codigoPostal: '38000', latitud: _casaPos.latitude, longitud: _casaPos.longitude, radioAlertaMetros: 200, ); Set _markers = {}; Set _circles = {}; Timer? _refreshTimer; // Distancia simulada (metros) double get _distanciaMetros => 380; int get _minutosEstimados => 8; @override void initState() { super.initState(); _buildMapElements(); // Simular actualización de posición cada 30s _refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) { if (mounted) setState(() {}); }); } @override void dispose() { _refreshTimer?.cancel(); super.dispose(); } void _buildMapElements() { _markers = { Marker( markerId: const MarkerId('camion'), position: LatLng(_camion.latitud, _camion.longitud), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen), infoWindow: InfoWindow( title: 'Camión · ${_camion.ruta}', snippet: _camion.tiempoActualizacion, ), ), Marker( markerId: const MarkerId('casa'), position: LatLng(_casa.latitud, _casa.longitud), icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue), infoWindow: InfoWindow(title: _casa.alias, snippet: _casa.calle), ), }; _circles = { Circle( circleId: const CircleId('radio-alerta'), center: LatLng(_casa.latitud, _casa.longitud), radius: _casa.radioAlertaMetros.toDouble(), fillColor: AppTheme.blue.withValues(alpha: 0.08), strokeColor: AppTheme.blue.withValues(alpha: 0.4), strokeWidth: 1, ), }; } Future _centrarMapa() async { final controller = await _mapController.future; await controller.animateCamera( CameraUpdate.newCameraPosition(_camaraInicial), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppTheme.background, appBar: AppBar( title: const Text('Rastreo en vivo'), actions: [ IconButton( icon: const Icon(Icons.my_location), onPressed: _centrarMapa, tooltip: 'Centrar mapa', ), ], ), body: Column( children: [ // ── Mapa ───────────────────────────────────────────────────── Expanded( flex: 5, child: Stack( children: [ GoogleMap( initialCameraPosition: _camaraInicial, markers: _markers, circles: _circles, myLocationButtonEnabled: false, zoomControlsEnabled: false, mapType: MapType.normal, onMapCreated: (c) { _mapController.complete(c); }, ), // Indicador "En vivo" Positioned( top: 14, right: 14, child: _LiveBadge(activo: _camion.enServicio), ), // Actualización Positioned( top: 14, left: 14, child: Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: AppTheme.softShadow, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.refresh, size: 14, color: AppTheme.textSecondary), const SizedBox(width: 4), Text( _camion.tiempoActualizacion, style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary), ), ], ), ), ), ], ), ), // ── Panel inferior ──────────────────────────────────────────── Expanded( flex: 3, child: Container( decoration: BoxDecoration( color: AppTheme.background, borderRadius: const BorderRadius.vertical( top: Radius.circular(AppTheme.radiusXl)), boxShadow: AppTheme.cardShadow, ), child: Column( children: [ // Handle Container( margin: const EdgeInsets.symmetric(vertical: 10), width: 36, height: 4, decoration: BoxDecoration( color: AppTheme.border, borderRadius: BorderRadius.circular(4), ), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ // Camión w.InfoRow( icon: Icons.delete_outline_rounded, label: '${_camion.ruta} · ${_camion.tiempoActualizacion}', value: 'Camión a ${_distanciaMetros.toStringAsFixed(0)} m', trailing: w.StatusBadge.amber('~$_minutosEstimados min'), ), const SizedBox(height: 10), // Casa w.InfoRow( icon: Icons.home_outlined, label: _casa.direccionCompleta, value: _casa.alias, trailing: w.StatusBadge.green('Activa'), ), const SizedBox(height: 12), // Barra de progreso de llegada _ArrivalBar( distanciaActual: _distanciaMetros, distanciaTotal: 1000, minutos: _minutosEstimados, ), ], ), ), ), ], ), ), ), ], ), ); } } // ── Badge "En vivo" ─────────────────────────────────────────────────────────── class _LiveBadge extends StatefulWidget { final bool activo; const _LiveBadge({required this.activo}); @override State<_LiveBadge> createState() => _LiveBadgeState(); } class _LiveBadgeState extends State<_LiveBadge> with SingleTickerProviderStateMixin { late AnimationController _anim; @override void initState() { super.initState(); _anim = AnimationController( vsync: this, duration: const Duration(milliseconds: 900), )..repeat(reverse: true); } @override void dispose() { _anim.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: AppTheme.softShadow, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedBuilder( animation: _anim, builder: (_, __) => Container( width: 8, height: 8, decoration: BoxDecoration( shape: BoxShape.circle, color: widget.activo ? AppTheme.primary.withValues(alpha: 0.5 + _anim.value * 0.5) : AppTheme.textSecondary, ), ), ), const SizedBox(width: 5), Text( widget.activo ? 'En vivo' : 'Sin servicio', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: widget.activo ? AppTheme.primary : AppTheme.textSecondary, ), ), ], ), ); } } // ── Barra de llegada estimada ───────────────────────────────────────────────── class _ArrivalBar extends StatelessWidget { final double distanciaActual; final double distanciaTotal; final int minutos; const _ArrivalBar({ required this.distanciaActual, required this.distanciaTotal, required this.minutos, }); @override Widget build(BuildContext context) { final progreso = ((distanciaTotal - distanciaActual) / distanciaTotal).clamp(0.0, 1.0); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppTheme.primaryLight, borderRadius: BorderRadius.circular(AppTheme.radiusMd), border: Border.all(color: AppTheme.primaryMid, width: 0.5), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Text('Llegada estimada', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: AppTheme.primaryDark)), const Spacer(), Text('~$minutos min', style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: AppTheme.primary)), ], ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: progreso, backgroundColor: AppTheme.primaryMid.withValues(alpha: 0.4), valueColor: const AlwaysStoppedAnimation(AppTheme.primary), minHeight: 6, ), ), const SizedBox(height: 4), Row( children: const [ Text('Ahora', style: TextStyle( fontSize: 10, color: AppTheme.primaryDark)), Spacer(), Text('Tu casa', style: TextStyle( fontSize: 10, color: AppTheme.primaryDark)), ], ), ], ), ); } }