Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> configuracion inicial para supoabase y endpoints
This commit is contained in:
383
recolecta_app/views/lib/screens/map_screen.dart
Normal file
383
recolecta_app/views/lib/screens/map_screen.dart
Normal file
@@ -0,0 +1,383 @@
|
||||
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<MapScreen> createState() => _MapScreenState();
|
||||
}
|
||||
|
||||
class _MapScreenState extends State<MapScreen> {
|
||||
final Completer<GoogleMapController> _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 = const HouseModel(
|
||||
id: 'casa-01',
|
||||
calle: 'Av. Insurgentes 245',
|
||||
colonia: 'Centro',
|
||||
codigoPostal: '38000',
|
||||
latitud: _casaPos.latitude,
|
||||
longitud: _casaPos.longitude,
|
||||
radioAlertaMetros: 200,
|
||||
);
|
||||
|
||||
Set<Marker> _markers = {};
|
||||
Set<Circle> _circles = {};
|
||||
bool _mapLoaded = false;
|
||||
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.withOpacity(0.08),
|
||||
strokeColor: AppTheme.blue.withOpacity(0.4),
|
||||
strokeWidth: 1,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _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);
|
||||
setState(() => _mapLoaded = true);
|
||||
},
|
||||
),
|
||||
|
||||
// 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.withOpacity(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.withOpacity(0.4),
|
||||
valueColor:
|
||||
const AlwaysStoppedAnimation<Color>(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)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user