Files
hackathon-innovaflow5.0-cdf…/views_v1/driver_screen.dart
2026-05-22 23:53:00 -06:00

1632 lines
52 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../models/models.dart';
import '../widgets/widgets.dart' as w;
// ── Modelo de parada ──────────────────────────────────────────────────────────
enum EstadoParada { pendiente, enCamino, completada, saltada }
class StopModel {
final String id;
final String direccion;
final String colonia;
final String referencias;
final int orden;
EstadoParada estado;
StopModel({
required this.id,
required this.direccion,
required this.colonia,
required this.referencias,
required this.orden,
this.estado = EstadoParada.pendiente,
});
}
// ── Shell principal del Chofer ─────────────────────────────────────────────────
class DriverShell extends StatefulWidget {
const DriverShell({super.key});
@override
State<DriverShell> createState() => _DriverShellState();
}
class _DriverShellState extends State<DriverShell> {
int _currentIndex = 0;
final List<Widget> _screens = const [
DriverRouteScreen(),
DriverStopsScreen(),
DriverHistoryScreen(),
DriverProfileScreen(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _currentIndex, children: _screens),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (i) => setState(() => _currentIndex = i),
type: BottomNavigationBarType.fixed,
backgroundColor: AppTheme.surface,
selectedItemColor: AppTheme.primary,
unselectedItemColor: AppTheme.textSecondary,
selectedFontSize: 11,
unselectedFontSize: 11,
elevation: 12,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.map_outlined),
activeIcon: Icon(Icons.map),
label: 'Mi ruta',
),
BottomNavigationBarItem(
icon: Icon(Icons.list_alt_outlined),
activeIcon: Icon(Icons.list_alt),
label: 'Paradas',
),
BottomNavigationBarItem(
icon: Icon(Icons.history_outlined),
activeIcon: Icon(Icons.history),
label: 'Historial',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: 'Perfil',
),
],
),
);
}
}
// ── Pantalla principal: Mi ruta (estado del turno) ────────────────────────────
class DriverRouteScreen extends StatefulWidget {
const DriverRouteScreen({super.key});
@override
State<DriverRouteScreen> createState() => _DriverRouteScreenState();
}
class _DriverRouteScreenState extends State<DriverRouteScreen> {
bool _turnoActivo = false;
bool _enPausa = false;
Timer? _timer;
Duration _duracion = Duration.zero;
// Datos de ejemplo
final TruckLocation _camion = TruckLocation(
id: 'truck-01',
ruta: 'Ruta Norte',
latitud: 20.5255,
longitud: -100.8220,
ultimaActualizacion: DateTime.now(),
enServicio: true,
);
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _iniciarTurno() {
setState(() {
_turnoActivo = true;
_enPausa = false;
});
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (!_enPausa && mounted) {
setState(() => _duracion += const Duration(seconds: 1));
}
});
}
void _pausarReanudar() {
setState(() => _enPausa = !_enPausa);
}
void _finalizarTurno() {
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
title: const Text('Finalizar turno',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary)),
content: const Text(
'¿Confirmas que has terminado el recorrido de hoy?',
style:
TextStyle(fontSize: 14, color: AppTheme.textSecondary),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_timer?.cancel();
setState(() {
_turnoActivo = false;
_enPausa = false;
_duracion = Duration.zero;
});
},
style:
TextButton.styleFrom(foregroundColor: AppTheme.danger),
child: const Text('Finalizar',
style: TextStyle(fontWeight: FontWeight.w600)),
),
],
),
);
}
String get _tiempoFormateado {
final h = _duracion.inHours.toString().padLeft(2, '0');
final m = (_duracion.inMinutes % 60).toString().padLeft(2, '0');
final s = (_duracion.inSeconds % 60).toString().padLeft(2, '0');
return '$h:$m:$s';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(
title: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(AppTheme.radiusSm),
),
child: const Icon(Icons.directions_bus_rounded,
color: Colors.white, size: 18),
),
const SizedBox(width: 10),
const Text('Panel del chofer'),
],
),
actions: [
if (_turnoActivo)
Container(
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
),
child: Row(
children: [
Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: _enPausa
? Colors.amber
: const Color(0xFF7AFFC5),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
_enPausa ? 'Pausado' : 'En servicio',
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w600),
),
],
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// ── Datos del chofer ──────────────────────────────────────
_DriverInfoBanner(ruta: _camion.ruta),
const SizedBox(height: 16),
// ── Cronómetro / estado de turno ──────────────────────────
_TurnoCronometro(
turnoActivo: _turnoActivo,
enPausa: _enPausa,
tiempo: _tiempoFormateado,
),
const SizedBox(height: 16),
// ── Botones de control ────────────────────────────────────
if (!_turnoActivo)
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton.icon(
onPressed: _iniciarTurno,
icon: const Icon(Icons.play_circle_outline_rounded),
label: const Text('Iniciar turno'),
),
)
else
Row(
children: [
Expanded(
child: SizedBox(
height: 52,
child: OutlinedButton.icon(
onPressed: _pausarReanudar,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.amber,
side: const BorderSide(color: AppTheme.amber),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppTheme.radiusMd),
),
),
icon: Icon(_enPausa
? Icons.play_circle_outline_rounded
: Icons.pause_circle_outline_rounded),
label: Text(_enPausa ? 'Reanudar' : 'Pausar',
style: const TextStyle(
fontWeight: FontWeight.w600)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: SizedBox(
height: 52,
child: ElevatedButton.icon(
onPressed: _finalizarTurno,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.danger),
icon: const Icon(Icons.stop_circle_outlined),
label: const Text('Finalizar',
style:
TextStyle(fontWeight: FontWeight.w600)),
),
),
),
],
),
const SizedBox(height: 24),
// ── Estadísticas del día ──────────────────────────────────
w.SectionTitle(title: 'Hoy'),
Row(
children: [
Expanded(
child: _SmallStatCard(
icon: Icons.location_on_outlined,
label: 'Paradas',
value: '14 / 22',
color: AppTheme.primary,
bgColor: AppTheme.primaryLight,
),
),
const SizedBox(width: 12),
Expanded(
child: _SmallStatCard(
icon: Icons.notifications_active_outlined,
label: 'Alertas enviadas',
value: '61',
color: AppTheme.blue,
bgColor: AppTheme.blueLight,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _SmallStatCard(
icon: Icons.access_time_outlined,
label: 'Tiempo en ruta',
value: _turnoActivo ? _tiempoFormateado : '--:--:--',
color: AppTheme.amber,
bgColor: AppTheme.amberLight,
),
),
const SizedBox(width: 12),
Expanded(
child: _SmallStatCard(
icon: Icons.speed_outlined,
label: 'Velocidad prom.',
value: '12 km/h',
color: AppTheme.primaryDark,
bgColor: AppTheme.primaryLight,
),
),
],
),
const SizedBox(height: 24),
// ── Próxima parada ────────────────────────────────────────
w.SectionTitle(title: 'Próxima parada'),
_ProximaParadaCard(),
const SizedBox(height: 24),
// ── Acciones rápidas ──────────────────────────────────────
w.SectionTitle(title: 'Acciones rápidas'),
w.MenuTile(
icon: Icons.report_problem_outlined,
title: 'Reportar incidencia',
subtitle: 'Tráfico, avería, desvío…',
onTap: () => _mostrarReporteIncidencia(context),
),
w.MenuTile(
icon: Icons.local_gas_station_outlined,
title: 'Registrar carga de combustible',
onTap: () {},
),
w.MenuTile(
icon: Icons.phone_in_talk_outlined,
title: 'Contactar a Control',
subtitle: '+52 461 800 0000',
onTap: () {},
),
const SizedBox(height: 32),
],
),
);
}
void _mostrarReporteIncidencia(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: AppTheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(AppTheme.radiusXl)),
),
builder: (_) => const _IncidentReportSheet(),
);
}
}
// ── Pantalla de Paradas ───────────────────────────────────────────────────────
class DriverStopsScreen extends StatefulWidget {
const DriverStopsScreen({super.key});
@override
State<DriverStopsScreen> createState() => _DriverStopsScreenState();
}
class _DriverStopsScreenState extends State<DriverStopsScreen> {
late List<StopModel> _paradas;
@override
void initState() {
super.initState();
_paradas = [
StopModel(
id: 's-01',
direccion: 'Av. Insurgentes 245',
colonia: 'Col. Centro',
referencias: 'Casa esquina, portón azul',
orden: 1,
estado: EstadoParada.completada),
StopModel(
id: 's-02',
direccion: 'Calle Morelos 18',
colonia: 'Col. Centro',
referencias: 'Frente a la farmacia',
orden: 2,
estado: EstadoParada.completada),
StopModel(
id: 's-03',
direccion: 'Privada Las Flores 7',
colonia: 'Col. Las Palmas',
referencias: 'Entrada sin número',
orden: 3,
estado: EstadoParada.enCamino),
StopModel(
id: 's-04',
direccion: 'Blvd. Torres Landa 310',
colonia: 'Col. Las Palmas',
referencias: 'Edificio verde',
orden: 4,
estado: EstadoParada.pendiente),
StopModel(
id: 's-05',
direccion: 'Calle Hidalgo 89',
colonia: 'Col. Primavera',
referencias: 'Casa con árbol en la entrada',
orden: 5,
estado: EstadoParada.pendiente),
StopModel(
id: 's-06',
direccion: 'Av. Revolución 440',
colonia: 'Col. Primavera',
referencias: 'Condominio Piso 1',
orden: 6,
estado: EstadoParada.pendiente),
StopModel(
id: 's-07',
direccion: 'Calle Juárez 112',
colonia: 'Col. Los Pinos',
referencias: 'Casa color salmón',
orden: 7,
estado: EstadoParada.saltada),
];
}
void _marcarCompletada(StopModel parada) {
setState(() => parada.estado = EstadoParada.completada);
}
void _marcarSaltada(StopModel parada) {
setState(() => parada.estado = EstadoParada.saltada);
}
int get _completadas =>
_paradas.where((p) => p.estado == EstadoParada.completada).length;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(title: const Text('Paradas de hoy')),
body: Column(
children: [
// Barra de progreso
_ProgressHeader(
completadas: _completadas, total: _paradas.length),
// Lista de paradas
Expanded(
child: ListView.builder(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 32),
itemCount: _paradas.length,
itemBuilder: (_, i) => _StopCard(
parada: _paradas[i],
onCompletada: () => _marcarCompletada(_paradas[i]),
onSaltada: () => _marcarSaltada(_paradas[i]),
),
),
),
],
),
);
}
}
// ── Pantalla de Historial ─────────────────────────────────────────────────────
class DriverHistoryScreen extends StatelessWidget {
const DriverHistoryScreen({super.key});
static const List<Map<String, dynamic>> _historial = [
{
'fecha': 'Hoy',
'ruta': 'Ruta Norte',
'duracion': '2h 43min',
'paradas': '14 / 22',
'alertas': 61,
'completada': false,
},
{
'fecha': 'Jue 22 may',
'ruta': 'Ruta Norte',
'duracion': '3h 12min',
'paradas': '22 / 22',
'alertas': 89,
'completada': true,
},
{
'fecha': 'Mié 21 may',
'ruta': 'Ruta Norte',
'duracion': '2h 55min',
'paradas': '22 / 22',
'alertas': 74,
'completada': true,
},
{
'fecha': 'Mar 20 may',
'ruta': 'Ruta Norte',
'duracion': '3h 05min',
'paradas': '21 / 22',
'alertas': 68,
'completada': true,
},
{
'fecha': 'Lun 19 may',
'ruta': 'Ruta Norte',
'duracion': '2h 48min',
'paradas': '22 / 22',
'alertas': 85,
'completada': true,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(title: const Text('Historial')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Resumen semanal
_WeeklySummaryBanner(),
const SizedBox(height: 20),
w.SectionTitle(title: 'Recorridos recientes'),
..._historial.map((h) => _HistoryCard(data: h)),
const SizedBox(height: 32),
],
),
);
}
}
// ── Pantalla de Perfil del Chofer ─────────────────────────────────────────────
class DriverProfileScreen extends StatelessWidget {
const DriverProfileScreen({super.key});
final DriverInfo _chofer = const DriverInfo(
nombre: 'Miguel',
apellido: 'Hernández',
telefono: '+52 461 100 0001',
ruta: 'Ruta Norte',
vehiculo: 'Camión #03 · MXX-483',
turno: '7:00 10:00 a.m.',
antiguedad: '3 años',
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(title: const Text('Mi perfil')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Header
_DriverProfileHeader(chofer: _chofer),
const SizedBox(height: 20),
// Mi turno
w.SectionTitle(title: 'Mi turno'),
w.InfoRow(
icon: Icons.route_outlined,
label: 'Ruta asignada',
value: _chofer.ruta),
const SizedBox(height: 8),
w.InfoRow(
icon: Icons.directions_bus_outlined,
label: 'Vehículo',
value: _chofer.vehiculo),
const SizedBox(height: 8),
w.InfoRow(
icon: Icons.schedule_outlined,
label: 'Horario',
value: _chofer.turno),
const SizedBox(height: 8),
w.InfoRow(
icon: Icons.work_outline_rounded,
label: 'Antigüedad',
value: _chofer.antiguedad),
const SizedBox(height: 20),
// Cuenta
w.SectionTitle(title: 'Mi cuenta'),
w.MenuTile(
icon: Icons.person_outline,
title: 'Editar datos personales',
onTap: () {},
),
w.MenuTile(
icon: Icons.lock_outline,
title: 'Cambiar contraseña',
onTap: () {},
),
w.MenuTile(
icon: Icons.phone_outlined,
title: 'Teléfono de emergencia',
subtitle: 'Agregar contacto',
onTap: () {},
),
const SizedBox(height: 16),
// Soporte
w.SectionTitle(title: 'Soporte'),
w.MenuTile(
icon: Icons.help_outline,
title: 'Manual del operador',
onTap: () {},
),
w.MenuTile(
icon: Icons.bug_report_outlined,
title: 'Reportar problema técnico',
onTap: () {},
),
const SizedBox(height: 16),
// Cerrar sesión
w.MenuTile(
icon: Icons.logout_rounded,
title: 'Cerrar sesión',
iconColor: AppTheme.danger,
titleColor: AppTheme.danger,
trailing: const SizedBox.shrink(),
onTap: () {},
),
const SizedBox(height: 32),
Center(
child: Text(
'RutaVerde v1.0.0 · Chofer\nServicio de Limpia · Celaya, Gto.',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 12, color: AppTheme.textHint, height: 1.6),
),
),
const SizedBox(height: 24),
],
),
);
}
}
// ── Modelo simple para perfil del chofer ──────────────────────────────────────
class DriverInfo {
final String nombre;
final String apellido;
final String telefono;
final String ruta;
final String vehiculo;
final String turno;
final String antiguedad;
const DriverInfo({
required this.nombre,
required this.apellido,
required this.telefono,
required this.ruta,
required this.vehiculo,
required this.turno,
required this.antiguedad,
});
String get nombreCompleto => '$nombre $apellido';
String get iniciales =>
'${nombre.isNotEmpty ? nombre[0] : ''}${apellido.isNotEmpty ? apellido[0] : ''}'
.toUpperCase();
}
// ─────────────────────────────────────────────────────────────────────────────
// WIDGETS INTERNOS
// ─────────────────────────────────────────────────────────────────────────────
// ── Widgets de Mi ruta ────────────────────────────────────────────────────────
class _DriverInfoBanner extends StatelessWidget {
final String ruta;
const _DriverInfoBanner({required this.ruta});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.primary, AppTheme.primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
child: Row(
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Center(
child: Text(
'MH',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w800,
color: Colors.white),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Miguel Hernández',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 3),
Row(
children: [
const Icon(Icons.route_outlined,
size: 13, color: Colors.white70),
const SizedBox(width: 4),
Text(
ruta,
style: const TextStyle(
fontSize: 12, color: Colors.white70),
),
],
),
const SizedBox(height: 3),
Row(
children: [
const Icon(Icons.directions_bus_outlined,
size: 13, color: Colors.white70),
const SizedBox(width: 4),
const Text(
'Camión #03 · MXX-483',
style: TextStyle(fontSize: 12, color: Colors.white70),
),
],
),
],
),
),
],
),
);
}
}
class _TurnoCronometro extends StatelessWidget {
final bool turnoActivo;
final bool enPausa;
final String tiempo;
const _TurnoCronometro({
required this.turnoActivo,
required this.enPausa,
required this.tiempo,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: AppTheme.border, width: 0.5),
boxShadow: AppTheme.softShadow,
),
child: Column(
children: [
Text(
turnoActivo ? (enPausa ? 'Turno pausado' : 'Turno activo') : 'Sin turno activo',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: turnoActivo
? (enPausa ? AppTheme.amber : AppTheme.primary)
: AppTheme.textSecondary),
),
const SizedBox(height: 10),
Text(
tiempo,
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.w800,
letterSpacing: 2,
color: turnoActivo
? (enPausa ? AppTheme.amber : AppTheme.textPrimary)
: AppTheme.textHint),
),
if (turnoActivo) ...[
const SizedBox(height: 8),
Text(
enPausa ? 'El GPS sigue activo durante la pausa' : 'GPS activo · Enviando ubicación',
style: const TextStyle(
fontSize: 11, color: AppTheme.textSecondary),
),
],
],
),
);
}
}
class _SmallStatCard extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Color color;
final Color bgColor;
const _SmallStatCard({
required this.icon,
required this.label,
required this.value,
required this.color,
required this.bgColor,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: AppTheme.border, width: 0.5),
boxShadow: AppTheme.softShadow,
),
child: Row(
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary)),
Text(label,
style: const TextStyle(
fontSize: 10,
color: AppTheme.textSecondary,
height: 1.3)),
],
),
),
],
),
);
}
}
class _ProximaParadaCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: AppTheme.primary, width: 1.5),
boxShadow: AppTheme.cardShadow,
),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppTheme.primaryLight,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.location_on_rounded,
color: AppTheme.primary, size: 24),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Privada Las Flores 7',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary),
),
const SizedBox(height: 2),
const Text(
'Col. Las Palmas · Parada #3',
style: TextStyle(
fontSize: 12, color: AppTheme.textSecondary),
),
const SizedBox(height: 5),
w.StatusBadge.green('~3 min'),
],
),
),
IconButton(
onPressed: () {},
icon: const Icon(Icons.open_in_new_rounded,
color: AppTheme.primary, size: 20),
),
],
),
);
}
}
// ── Widgets de Paradas ────────────────────────────────────────────────────────
class _ProgressHeader extends StatelessWidget {
final int completadas;
final int total;
const _ProgressHeader({required this.completadas, required this.total});
@override
Widget build(BuildContext context) {
final pct = total > 0 ? completadas / total : 0.0;
return Container(
color: AppTheme.primary,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'$completadas de $total paradas',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white),
),
Text(
'${(pct * 100).toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
color: Colors.white),
),
],
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
child: LinearProgressIndicator(
value: pct,
minHeight: 8,
backgroundColor: Colors.white24,
valueColor:
const AlwaysStoppedAnimation<Color>(Colors.white),
),
),
],
),
);
}
}
class _StopCard extends StatelessWidget {
final StopModel parada;
final VoidCallback onCompletada;
final VoidCallback onSaltada;
const _StopCard({
required this.parada,
required this.onCompletada,
required this.onSaltada,
});
Color get _borderColor {
switch (parada.estado) {
case EstadoParada.completada:
return AppTheme.primaryMid;
case EstadoParada.enCamino:
return AppTheme.primary;
case EstadoParada.saltada:
return AppTheme.danger;
case EstadoParada.pendiente:
return AppTheme.border;
}
}
Color get _iconBg {
switch (parada.estado) {
case EstadoParada.completada:
return AppTheme.primaryLight;
case EstadoParada.enCamino:
return AppTheme.primaryLight;
case EstadoParada.saltada:
return AppTheme.dangerLight;
case EstadoParada.pendiente:
return AppTheme.background;
}
}
Color get _iconColor {
switch (parada.estado) {
case EstadoParada.completada:
return AppTheme.primary;
case EstadoParada.enCamino:
return AppTheme.primary;
case EstadoParada.saltada:
return AppTheme.danger;
case EstadoParada.pendiente:
return AppTheme.textSecondary;
}
}
IconData get _icon {
switch (parada.estado) {
case EstadoParada.completada:
return Icons.check_circle_rounded;
case EstadoParada.enCamino:
return Icons.directions_bus_rounded;
case EstadoParada.saltada:
return Icons.cancel_outlined;
case EstadoParada.pendiente:
return Icons.location_on_outlined;
}
}
String get _etiqueta {
switch (parada.estado) {
case EstadoParada.completada:
return 'Completada';
case EstadoParada.enCamino:
return 'En camino';
case EstadoParada.saltada:
return 'Saltada';
case EstadoParada.pendiente:
return 'Pendiente';
}
}
bool get _esPendienteOEnCamino =>
parada.estado == EstadoParada.pendiente ||
parada.estado == EstadoParada.enCamino;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: _borderColor, width: 0.8),
boxShadow: AppTheme.softShadow,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Número de orden
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: _iconBg,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'${parada.orden}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: _iconColor),
),
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(parada.direccion,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary)),
const SizedBox(height: 2),
Text(parada.colonia,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
if (parada.referencias.isNotEmpty) ...[
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.info_outline_rounded,
size: 12, color: AppTheme.textHint),
const SizedBox(width: 4),
Expanded(
child: Text(parada.referencias,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textHint)),
),
],
),
],
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _iconBg,
borderRadius: BorderRadius.circular(AppTheme.radiusFull),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(_icon, size: 11, color: _iconColor),
const SizedBox(width: 4),
Text(_etiqueta,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: _iconColor)),
],
),
),
],
),
),
// Acciones (solo si pendiente o en camino)
if (_esPendienteOEnCamino) ...[
Divider(color: AppTheme.borderLight, height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(10, 8, 10, 10),
child: Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onCompletada,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primary,
side: const BorderSide(color: AppTheme.primary),
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppTheme.radiusSm),
),
),
icon: const Icon(Icons.check_rounded, size: 15),
label: const Text('Completar',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600)),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: onSaltada,
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.textSecondary,
side:
const BorderSide(color: AppTheme.border),
padding: const EdgeInsets.symmetric(vertical: 8),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(AppTheme.radiusSm),
),
),
icon: const Icon(Icons.skip_next_rounded, size: 15),
label: const Text('Saltar',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600)),
),
),
],
),
),
],
],
),
);
}
}
// ── Widgets de Historial ──────────────────────────────────────────────────────
class _WeeklySummaryBanner extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.primaryDark, AppTheme.primary],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Resumen semanal',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_SumItem(label: 'Turno activo', value: '4/5 días'),
_VertDiv(),
_SumItem(label: 'Alertas', value: '377'),
_VertDiv(),
_SumItem(label: 'Paradas', value: '101 / 110'),
],
),
],
),
);
}
}
class _SumItem extends StatelessWidget {
final String label;
final String value;
const _SumItem({required this.label, required this.value});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: Colors.white)),
const SizedBox(height: 2),
Text(label,
style: const TextStyle(fontSize: 11, color: Colors.white70)),
],
);
}
}
class _VertDiv extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(width: 1, height: 32, color: Colors.white24);
}
}
class _HistoryCard extends StatelessWidget {
final Map<String, dynamic> data;
const _HistoryCard({required this.data});
@override
Widget build(BuildContext context) {
final completada = data['completada'] as bool;
return Container(
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: AppTheme.surface,
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
border: Border.all(color: AppTheme.border, width: 0.5),
boxShadow: AppTheme.softShadow,
),
child: Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: completada ? AppTheme.primaryLight : AppTheme.amberLight,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
completada
? Icons.check_circle_outline_rounded
: Icons.timelapse_rounded,
color: completada ? AppTheme.primary : AppTheme.amber,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(data['fecha'] as String,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary)),
const SizedBox(height: 2),
Text(data['ruta'] as String,
style: const TextStyle(
fontSize: 12, color: AppTheme.textSecondary)),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(data['duracion'] as String,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary)),
const SizedBox(height: 3),
Text(
'${data['paradas']} · ${data['alertas']} alertas',
style: const TextStyle(
fontSize: 11, color: AppTheme.textSecondary),
),
],
),
],
),
);
}
}
// ── Widgets de Perfil del Chofer ──────────────────────────────────────────────
class _DriverProfileHeader extends StatelessWidget {
final DriverInfo chofer;
const _DriverProfileHeader({required this.chofer});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppTheme.primary, AppTheme.primaryDark],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
child: Row(
children: [
Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
shape: BoxShape.circle,
border:
Border.all(color: Colors.white38, width: 2),
),
child: Center(
child: Text(
chofer.iniciales,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.w800,
color: Colors.white),
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(chofer.nombreCompleto,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: Colors.white)),
const SizedBox(height: 3),
Text(chofer.telefono,
style: const TextStyle(
fontSize: 12, color: Colors.white70)),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius:
BorderRadius.circular(AppTheme.radiusFull),
),
child: const Text(
'Operador certificado',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Colors.white),
),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit_outlined,
color: Colors.white70, size: 20),
onPressed: () {},
),
],
),
);
}
}
// ── Bottom Sheet: Reporte de incidencia ───────────────────────────────────────
class _IncidentReportSheet extends StatefulWidget {
const _IncidentReportSheet();
@override
State<_IncidentReportSheet> createState() => _IncidentReportSheetState();
}
class _IncidentReportSheetState extends State<_IncidentReportSheet> {
int _tipoSeleccionado = 0;
final List<Map<String, dynamic>> _tipos = [
{'icon': Icons.traffic_rounded, 'label': 'Tráfico'},
{'icon': Icons.build_outlined, 'label': 'Avería'},
{'icon': Icons.alt_route_rounded, 'label': 'Desvío'},
{'icon': Icons.warning_amber_rounded, 'label': 'Otro'},
];
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
top: 16,
left: 20,
right: 20,
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 36,
height: 4,
decoration: BoxDecoration(
color: AppTheme.border,
borderRadius:
BorderRadius.circular(AppTheme.radiusFull),
),
),
),
const SizedBox(height: 16),
const Text('Reportar incidencia',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppTheme.textPrimary)),
const SizedBox(height: 6),
const Text('El control será notificado de inmediato.',
style: TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 20),
// Tipo de incidencia
w.SectionTitle(title: 'Tipo'),
Row(
children: List.generate(_tipos.length, (i) {
final sel = i == _tipoSeleccionado;
return Expanded(
child: GestureDetector(
onTap: () => setState(() => _tipoSeleccionado = i),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
margin: EdgeInsets.only(right: i < 3 ? 8 : 0),
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: sel ? AppTheme.primaryLight : AppTheme.surface,
borderRadius:
BorderRadius.circular(AppTheme.radiusMd),
border: Border.all(
color: sel ? AppTheme.primary : AppTheme.border,
width: sel ? 1.5 : 0.5,
),
),
child: Column(
children: [
Icon(
_tipos[i]['icon'] as IconData,
color: sel
? AppTheme.primary
: AppTheme.textSecondary,
size: 22,
),
const SizedBox(height: 5),
Text(
_tipos[i]['label'] as String,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: sel
? AppTheme.primary
: AppTheme.textSecondary),
),
],
),
),
),
);
}),
),
const SizedBox(height: 16),
w.FormField(
label: 'Descripción (opcional)',
hint: 'Cuéntanos qué está pasando…',
maxLines: 3,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.send_rounded),
label: const Text('Enviar reporte'),
),
),
],
),
);
}
}