1632 lines
52 KiB
Dart
1632 lines
52 KiB
Dart
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'),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|