import 'dart:io'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/app_colors.dart'; import '../../services/auth_service.dart'; import '../../services/route_simulator_service.dart'; import '../../database/db_helper.dart'; import '../../models/models.dart'; import '../../data/routes_data.dart'; import '../../models/route_model.dart' show ColonyModel; import 'create_route_screen.dart'; import 'admin_reporte_detalle_screen.dart'; import 'admin_stats_screen.dart'; import 'manage_conductors_screen.dart'; import 'export_pdf_screen.dart'; import '../../screens/settings_screen.dart'; import '../../widgets/route_map_widget.dart'; class AdminDashboardScreen extends StatefulWidget { const AdminDashboardScreen({super.key}); @override State createState() => _AdminDashboardScreenState(); } class _AdminDashboardScreenState extends State { int _tab = 0; @override Widget build(BuildContext context) { final sim = context.watch(); final auth = context.watch(); final last = sim.lastNotification; final tabs = [ _AdminHomeTab(sim:sim, auth:auth), // 0 Panel _AdminMapTab(sim:sim), // 1 Mapa _AdminReportesTab(), // 2 Reportes _AdminAssignmentsTab(), // 3 Asignar _AdminAlertasTab(sim:sim), // 4 Alertas _AdminRoutesTab(), // 5 Rutas _AdminReviewsTab(), // 6 Reseñas ]; return Scaffold( body: Stack(children:[ tabs[_tab], if (last!=null) Positioned(top:MediaQuery.of(context).padding.top+8,left:0,right:0, child:_AdminBanner(notif:last,onDismiss:sim.dismissNotification)), ]), bottomNavigationBar: NavigationBar( selectedIndex:_tab, onDestinationSelected:(i)=>setState(()=>_tab=i), backgroundColor:Colors.white, indicatorColor:AppColors.verdeAdmin.withOpacity(0.15), destinations:const[ NavigationDestination(icon:Icon(Icons.dashboard_outlined), selectedIcon:Icon(Icons.dashboard,color:AppColors.verdeAdmin),label:'Panel'), NavigationDestination(icon:Icon(Icons.map_outlined), selectedIcon:Icon(Icons.map,color:AppColors.verdeAdmin),label:'Mapa'), NavigationDestination(icon:Icon(Icons.report_outlined), selectedIcon:Icon(Icons.report,color:AppColors.verdeAdmin),label:'Reportes'), NavigationDestination(icon:Icon(Icons.people_alt_outlined), selectedIcon:Icon(Icons.people_alt,color:AppColors.verdeAdmin),label:'Asignar'), NavigationDestination(icon:Icon(Icons.warning_outlined), selectedIcon:Icon(Icons.warning,color:AppColors.verdeAdmin),label:'Alertas'), NavigationDestination(icon:Icon(Icons.route_outlined), selectedIcon:Icon(Icons.route,color:AppColors.verdeAdmin),label:'Rutas'), NavigationDestination(icon:Icon(Icons.star_outline), selectedIcon:Icon(Icons.star,color:AppColors.verdeAdmin),label:'Reseñas'), ], ), ); } } // ── TAB 1: Control de rutas ─────────────────────────────────────────────── class _AdminHomeTab extends StatefulWidget { final RouteSimulatorService sim; final AuthService auth; const _AdminHomeTab({required this.sim, required this.auth}); @override State<_AdminHomeTab> createState() => _AdminHomeTabState(); } class _AdminHomeTabState extends State<_AdminHomeTab> { List _statuses = []; List _conductorIncidentes = []; @override void initState() { super.initState(); _load(); } Future _load() async { final s = await DbHelper.getAllRouteStatuses(); final inc = await DbHelper.getIncidentesConductor(); if (mounted) setState(() { _statuses = s; _conductorIncidentes = inc; }); } String _getStatus(String rid) { try { return _statuses.firstWhere((s) => s.routeId == rid).status; } catch (_) { return RouteStatus.enRuta; } } String? _getMensaje(String rid) { try { return _statuses.firstWhere((s) => s.routeId == rid).mensaje; } catch (_) { return null; } } // Incidentes del conductor asociados a esta ruta (por número) List _getIncidentesPorRuta(String routeId) { return _conductorIncidentes .where((i) => !i.resuelta) .where((i) => i.routeId.contains(routeId) || // Si es incidente de conductor sin routeId específico, mostrar en todas i.routeId.startsWith('CONDUCTOR-')) .take(2) .toList(); } Future _changeStatus(String routeId, String status, String? msg) async { await DbHelper.upsertRouteStatus(RouteStatusModel( routeId: routeId, status: status, mensaje: msg, updatedAt: DateTime.now().toIso8601String())); if (status == RouteStatus.cancelada || status == RouteStatus.fallaMecanica || status == RouteStatus.retrasada) { final emoji = status == RouteStatus.cancelada ? '❌' : status == RouteStatus.fallaMecanica ? '🔧' : '⏱️'; final titulo = status == RouteStatus.cancelada ? 'Ruta Cancelada' : status == RouteStatus.fallaMecanica ? 'Falla Mecánica' : 'Servicio con Retraso'; final cuerpo = (msg != null && msg.isNotEmpty) ? '$emoji $msg' : '$emoji La ruta $routeId ${status == RouteStatus.cancelada ? "ha sido cancelada hoy" : status == RouteStatus.fallaMecanica ? "reportó una falla mecánica" : "presenta un retraso"}. Pendiente reprogramación.'; widget.sim.fireCustomNotification(titulo, cuerpo, routeId, status == RouteStatus.cancelada ? NotifEvent.routeCancelled : NotifEvent.truckStopped); await DbHelper.insertAlerta(AlertaModel( tipo: 'RUTA_$status', routeId: routeId, mensaje: cuerpo, fecha: DateTime.now().toIso8601String())); } await _load(); setState(() {}); } @override Widget build(BuildContext context) { return CustomScrollView(slivers: [ SliverAppBar(pinned: true, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), title: const Text('Panel Administrador', style: TextStyle(fontWeight: FontWeight.bold)), actions: [ IconButton(icon: const Icon(Icons.picture_as_pdf), tooltip: 'Exportar PDF', onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ExportPdfScreen()))), IconButton(icon: const Icon(Icons.bar_chart), tooltip: 'Estadisticas', onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const AdminStatsScreen()))), IconButton(icon: const Icon(Icons.settings_outlined), tooltip: 'Configuracion', onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()))), IconButton(icon: const Icon(Icons.refresh), onPressed: _load), IconButton(icon: const Icon(Icons.logout), onPressed: () async { await widget.auth.logout(); if (context.mounted) Navigator.pushReplacementNamed(context, '/login'); }), ], ), SliverPadding(padding: const EdgeInsets.all(12), sliver: SliverList(delegate: SliverChildListDelegate([ Row(children: [ _Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.verdeAdmin), const SizedBox(width: 10), _Stat('Incidentes', '${_conductorIncidentes.where((i)=>!i.resuelta).length}', Icons.warning, AppColors.naranjaAlerta), ]), const SizedBox(height: 14), const Text('Control de Rutas', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: AppColors.verdeAdmin)), const SizedBox(height: 8), ...routesData.map((r) { final status = _getStatus(r.routeId); final mensaje = _getMensaje(r.routeId); final gpsOk = widget.sim.isGpsActive(r.routeId); final nightIcon = r.turno == 'NOCTURNO' ? '🌙 ' : r.turno == 'VESPERTINO' ? '🌅 ' : '🌄 '; final incidentes = _getIncidentesPorRuta(r.routeId); return Card(margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: RouteStatus.color(status).withOpacity(0.4), width: 1.2)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cabecera ruta ListTile(dense: true, leading: Container(width: 8, height: 44, decoration: BoxDecoration(color: RouteStatus.color(status), borderRadius: BorderRadius.circular(4))), title: Text('${r.routeId} — ${r.name}', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700)), subtitle: Wrap(spacing: 6, children: [ Text(RouteStatus.label(status), style: TextStyle(fontSize: 11, color: RouteStatus.color(status), fontWeight: FontWeight.w600)), if (!gpsOk) const Text('📡 Sin GPS', style: TextStyle(fontSize: 10, color: AppColors.rojoError)), Text(nightIcon + r.turno, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), ]), trailing: PopupMenuButton( icon: const Icon(Icons.more_vert, size: 18), onSelected: (v) async { if (v == 'GPS') { widget.sim.simulateGpsLost(r.routeId); return; } if (v == 'RESTORE') { widget.sim.restoreGps(r.routeId); return; } String? msg; if (v == RouteStatus.retrasada) { final res = await _retrasadaDialog(context); if (res != null) { final parts = res.split('|'); final nuevoTurno = parts[0]; final extra = parts.length > 1 ? parts[1] : ''; msg = 'Ruta reprogramada al turno $nuevoTurno. $extra'.trim(); } } else if ([RouteStatus.cancelada, RouteStatus.fallaMecanica].contains(v)) { msg = await _inputDialog(context, 'Mensaje / solución para ciudadanos'); } await _changeStatus(r.routeId, v, msg); }, itemBuilder: (_) => [ const PopupMenuItem(value: 'EN_RUTA', child: Text('✅ En Ruta — Continúa')), const PopupMenuItem(value: 'RETRASADA', child: Text('⏱️ Marcar Retrasada')), const PopupMenuItem(value: 'CANCELADA', child: Text('❌ Cancelar y Notificar')), const PopupMenuItem(value: 'FALLA_MECANICA', child: Text('🔧 Falla Mecánica')), const PopupMenuDivider(), const PopupMenuItem(value: 'GPS', child: Text('📡 Simular GPS Perdido')), const PopupMenuItem(value: 'RESTORE', child: Text('📶 Restaurar GPS')), ], ), ), // Mensaje del admin si hay if (mensaje != null && mensaje.isNotEmpty && status != RouteStatus.enRuta) Padding( padding: const EdgeInsets.fromLTRB(14, 0, 14, 8), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: RouteStatus.color(status).withOpacity(0.08), borderRadius: BorderRadius.circular(6), ), child: Row(children: [ Icon(Icons.message_outlined, size: 13, color: RouteStatus.color(status)), const SizedBox(width: 6), Expanded(child: Text('Msg ciudadanos: $mensaje', style: TextStyle(fontSize: 11, color: RouteStatus.color(status)))), ]), ), ), // Incidentes de conductor pendientes para esta ruta if (incidentes.isNotEmpty) ...[ const Divider(height: 1, indent: 14, endIndent: 14), Padding( padding: const EdgeInsets.fromLTRB(14, 6, 14, 2), child: Row(children: [ const Icon(Icons.build, size: 13, color: AppColors.moradoConductor), const SizedBox(width: 4), const Text('Incidentes del conductor:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, color: AppColors.moradoConductor)), ]), ), ...incidentes.map((inc) => Padding( padding: const EdgeInsets.fromLTRB(14, 2, 14, 2), child: Row(children: [ Container(width: 6, height: 6, decoration: const BoxDecoration(color: AppColors.moradoConductor, shape: BoxShape.circle)), const SizedBox(width: 6), Expanded(child: Text(inc.mensaje, style: const TextStyle(fontSize: 11), maxLines: 1, overflow: TextOverflow.ellipsis)), TextButton( onPressed: () async { // Mostrar diálogo: ¿qué hacer con este incidente? final accion = await _incidenteDialog(context, inc.mensaje); if (accion != null) { await DbHelper.resolverAlerta(inc.id!); // Soporta formato RETRASADA:TURNO para reprogramación String realStatus = accion; String msg = 'Incidente: ${inc.mensaje.substring(0, inc.mensaje.length.clamp(0, 40))}'; if (accion.startsWith('RETRASADA:')) { final parts = accion.split(':'); realStatus = 'RETRASADA'; final turno = parts.length > 1 ? parts[1] : 'VESPERTINO'; msg = 'Tu ruta ha sido reprogramada al turno $turno por incidente del conductor. ' 'Recibirás notificación cuando el camión esté listo.'; } await _changeStatus(r.routeId, realStatus, msg); } }, style: TextButton.styleFrom( foregroundColor: AppColors.verdeAdmin, padding: const EdgeInsets.symmetric(horizontal: 8)), child: const Text('Actuar', style: TextStyle(fontSize: 10)), ), ]), )), const SizedBox(height: 6), ], ])); }), const SizedBox(height: 80), ]))), ]); } Future _inputDialog(BuildContext ctx, String hint) async { final ctrl = TextEditingController(); return showDialog(context: ctx, builder: (_) => AlertDialog( title: const Text('Mensaje para ciudadanos'), content: TextField(controller: ctrl, maxLines: 2, decoration: InputDecoration(hintText: hint, border: const OutlineInputBorder())), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), ElevatedButton(onPressed: () => Navigator.pop(ctx, ctrl.text), child: const Text('Enviar')), ])); } Future _retrasadaDialog(BuildContext ctx) async { String turno = 'VESPERTINO'; final ctrl = TextEditingController(); return showDialog(context: ctx, builder: (dCtx) => StatefulBuilder( builder: (dCtx, setSt) => AlertDialog( title: const Text('Reprogramar Ruta'), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('¿A qué turno pasará el camión?', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), const SizedBox(height: 8), Row(children: [ Expanded(child: RadioListTile(dense: true, value: 'MATUTINO', groupValue: turno, title: const Text('🌄 Matutino'), onChanged: (v) => setSt(() => turno = v!))), Expanded(child: RadioListTile(dense: true, value: 'VESPERTINO', groupValue: turno, title: const Text('🌅 Vespertino'), onChanged: (v) => setSt(() => turno = v!))), ]), const SizedBox(height: 8), TextField(controller: ctrl, maxLines: 2, decoration: const InputDecoration( hintText: 'Mensaje adicional para ciudadanos (opcional)', border: OutlineInputBorder(), isDense: true)), ]), actions: [ TextButton(onPressed: () => Navigator.pop(dCtx), child: const Text('Cancelar')), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: AppColors.naranjaAlerta, foregroundColor: Colors.white), onPressed: () => Navigator.pop(dCtx, '$turno|${ctrl.text.trim()}'), child: const Text('Confirmar')), ]))); } Future _incidenteDialog(BuildContext ctx, String incMensaje) async { String turnoSeleccionado = 'VESPERTINO'; return showDialog(context: ctx, builder: (dialogCtx) => StatefulBuilder( builder: (dialogCtx, setDialogState) => AlertDialog( title: const Text('Acción sobre el incidente'), content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(incMensaje, style: const TextStyle(fontSize: 12, color: AppColors.grisTexto)), const Divider(), const Text('Si decides reprogramar, ¿a qué turno?', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), const SizedBox(height: 6), Row(children: [ Expanded(child: RadioListTile(dense: true, value: 'MATUTINO', groupValue: turnoSeleccionado, title: const Text('🌄 Matutino', style: TextStyle(fontSize: 12)), onChanged: (v) => setDialogState(() => turnoSeleccionado = v!))), Expanded(child: RadioListTile(dense: true, value: 'VESPERTINO', groupValue: turnoSeleccionado, title: const Text('🌅 Vespertino', style: TextStyle(fontSize: 12)), onChanged: (v) => setDialogState(() => turnoSeleccionado = v!))), ]), const SizedBox(height: 4), const Text('¿Qué decisión tomas?', style: TextStyle(fontWeight: FontWeight.bold)), ]), actions: [ TextButton(onPressed: () => Navigator.pop(dialogCtx), child: const Text('Cerrar')), ElevatedButton.icon( style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeExito, foregroundColor: Colors.white), onPressed: () => Navigator.pop(dialogCtx, 'EN_RUTA'), icon: const Icon(Icons.check, size: 14), label: const Text('Continúa', style: TextStyle(fontSize: 12))), ElevatedButton.icon( style: ElevatedButton.styleFrom(backgroundColor: AppColors.naranjaAlerta, foregroundColor: Colors.white), onPressed: () => Navigator.pop(dialogCtx, 'RETRASADA:$turnoSeleccionado'), icon: const Icon(Icons.access_time, size: 14), label: Text('Retraso→$turnoSeleccionado', style: const TextStyle(fontSize: 11))), ElevatedButton.icon( style: ElevatedButton.styleFrom(backgroundColor: AppColors.rojoError, foregroundColor: Colors.white), onPressed: () => Navigator.pop(dialogCtx, 'CANCELADA'), icon: const Icon(Icons.cancel, size: 14), label: const Text('Cancelar', style: TextStyle(fontSize: 12))), ]))); } } class _AdminMapTab extends StatelessWidget { final RouteSimulatorService sim; const _AdminMapTab({required this.sim}); @override Widget build(BuildContext context) => Scaffold( appBar:AppBar(automaticallyImplyLeading:false, backgroundColor:AppColors.verdeAdmin,foregroundColor:Colors.white, title:const Text('Mapa — Todas las Rutas'), bottom:PreferredSize(preferredSize:const Size.fromHeight(4), child:Container(height:4,color:AppColors.dorado))), body:AdminMapWidget(routes:routesData,simulator:sim)); } // ── TAB 3: Reportes ciudadanos ──────────────────────────────────────────── // ── TAB 2: Reportes ciudadanos ─────────────────────────────────────────── class _AdminReportesTab extends StatefulWidget { @override State<_AdminReportesTab> createState() => _AdminReportesTabState(); } class _AdminReportesTabState extends State<_AdminReportesTab> { List> _reportes = []; bool _loading = true; String _filtroEstado = 'TODOS'; static const _estados = ['TODOS','PENDIENTE','EN_REVISION','EN_PROCESO','RESUELTO','COMPLETADO']; @override void initState() { super.initState(); _load(); } Future _load() async { final r = await DbHelper.getReportesConUsuario(); if (mounted) setState(() { _reportes = r; _loading = false; }); } List> get _filtered => _filtroEstado == 'TODOS' ? _reportes : _reportes.where((r) => (r['estado'] as String?) == _filtroEstado).toList(); Color _estadoColor(String s) { switch(s) { case 'COMPLETADO': return AppColors.verdeExito; case 'RESUELTO': return Colors.teal; case 'EN_PROCESO': return AppColors.azulInfo; case 'EN_REVISION': return AppColors.naranjaAlerta; default: return AppColors.grisTexto; } } String _estadoLabel(String s) => s.replaceAll('_', ' '); String _folio(int id) => 'RPT-${id.toString().padLeft(5, "0")}'; @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: Text('Reportes Ciudadanos (${_filtered.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _load)]), body: Column(children: [ Container(color: Colors.white, height: 44, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), itemCount: _estados.length, itemBuilder: (_, i) { final e = _estados[i]; final sel = _filtroEstado == e; return Padding(padding: const EdgeInsets.only(right: 6), child: FilterChip( label: Text(e == 'TODOS' ? 'Todos' : _estadoLabel(e), style: TextStyle(fontSize: 11, color: sel ? Colors.white : AppColors.negroTexto)), selected: sel, selectedColor: e == 'TODOS' ? AppColors.verdeAdmin : _estadoColor(e), checkmarkColor: Colors.white, onSelected: (_) => setState(() => _filtroEstado = e))); })), Expanded(child: _loading ? const Center(child: CircularProgressIndicator()) : _filtered.isEmpty ? const Center(child: Text('Sin reportes', style: TextStyle(color: AppColors.grisTexto))) : RefreshIndicator(onRefresh: _load, child: ListView.builder( padding: const EdgeInsets.all(10), itemCount: _filtered.length, itemBuilder: (ctx, i) { final r = _filtered[i]; final estado = r['estado'] as String? ?? 'PENDIENTE'; final id = r['id'] as int? ?? 0; final folio = _folio(id); final fotoPath = r['foto_path'] as String?; final nombre = r['user_nombre'] as String? ?? 'Ciudadano'; final colonia = r['colonia'] as String? ?? ''; final desc = r['descripcion'] as String? ?? ''; return Card(margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: _estadoColor(estado).withOpacity(0.3))), child: InkWell( borderRadius: BorderRadius.circular(10), onTap: () async { await Navigator.push(ctx, MaterialPageRoute( builder: (_) => AdminReporteDetalleScreen(reporte: r))); _load(); }, child: Padding(padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text(folio, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13, color: AppColors.verdeAdmin)), const SizedBox(width: 8), Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( color: _estadoColor(estado).withOpacity(0.1), borderRadius: BorderRadius.circular(10), border: Border.all(color: _estadoColor(estado).withOpacity(0.4))), child: Text(_estadoLabel(estado), style: TextStyle(fontSize: 9, fontWeight: FontWeight.bold, color: _estadoColor(estado)))), const Spacer(), if (fotoPath != null && fotoPath.isNotEmpty) const Icon(Icons.photo_camera, size: 14, color: AppColors.azulInfo), const SizedBox(width: 4), const Icon(Icons.chevron_right, color: AppColors.grisTexto, size: 18), ]), const SizedBox(height: 4), Text('$nombre — $colonia', style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)), const SizedBox(height: 2), Text(desc, style: const TextStyle(fontSize: 12), maxLines: 2, overflow: TextOverflow.ellipsis), ])))); }))), ]), ); } class _AdminConductoresTab extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: const Text('Gestión de Conductores'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), actions: [ IconButton(icon: const Icon(Icons.open_in_full), tooltip: 'Ver en pantalla completa', onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => const ManageConductorsScreen()))), ]), body: const ManageConductorsScreen()); } // ── TAB 6: Gestión de Rutas ─────────────────────────────────────────────── class _AdminRoutesTab extends StatefulWidget { @override State<_AdminRoutesTab> createState() => _AdminRoutesTabState(); } class _AdminRoutesTabState extends State<_AdminRoutesTab> { List _routes = []; bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { final r = await DbHelper.getAllRouteDefinitions(); if (mounted) setState(() { _routes = r; _loading = false; }); } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: Text('Rutas del Sistema (${_routes.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), actions: [ IconButton(icon: const Icon(Icons.refresh), onPressed: _load), IconButton( icon: const Icon(Icons.add_circle_outline), tooltip: 'Nueva ruta', onPressed: () async { final ok = await Navigator.push(context, MaterialPageRoute( builder: (_) => const CreateRouteScreen())); if (ok == true) await _load(); }), ]), body: _loading ? const Center(child: CircularProgressIndicator()) : _routes.isEmpty ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.route, color: AppColors.grisTexto, size: 48), const SizedBox(height: 12), const Text('No hay rutas creadas', style: TextStyle(color: AppColors.grisTexto)), const SizedBox(height: 16), ElevatedButton.icon( style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white), onPressed: () async { final ok = await Navigator.push(context, MaterialPageRoute( builder: (_) => const CreateRouteScreen())); if (ok == true) await _load(); }, icon: const Icon(Icons.add), label: const Text('Crear primera ruta')), ])) : ListView.builder( padding: const EdgeInsets.all(12), itemCount: _routes.length, itemBuilder: (_, i) { final r = _routes[i]; final turnoEmoji = r.turno == 'MATUTINO' ? '🌄' : r.turno == 'VESPERTINO' ? '🌅' : '🌙'; return Card(margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: AppColors.verdeAdmin.withOpacity(0.3))), child: Padding(padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Expanded(child: Text('${r.routeId} — ${r.nombre}', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14, color: AppColors.verdeAdmin))), IconButton(icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () async { final ok = await Navigator.push(context, MaterialPageRoute( builder: (_) => CreateRouteScreen(editing: r))); if (ok == true) await _load(); }), ]), const SizedBox(height: 4), Row(children: [ Text('$turnoEmoji ${r.turno} • ${r.horaInicio}–${r.horaFin}', style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)), ]), const SizedBox(height: 4), Text(r.dias.map(AppDias.label).join(', '), style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)), const SizedBox(height: 6), // Colonias Text('📍 ${r.colonias.length} colonias:', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)), const SizedBox(height: 4), Wrap(spacing: 4, runSpacing: 4, children: r.colonias.take(8).map((c) => Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Text(c, style: const TextStyle(fontSize: 10, color: AppColors.verdeAdmin)))).toList()), if (r.colonias.length > 8) Text(' ...y ${r.colonias.length - 8} más', style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), ]))); }), ); } // ── TAB 7: Reseñas y calificaciones ────────────────────────────────────── class _AdminReviewsTab extends StatefulWidget { @override State<_AdminReviewsTab> createState() => _AdminReviewsTabState(); } class _AdminReviewsTabState extends State<_AdminReviewsTab> { List _reviews = []; List> _summary = []; bool _showSummary = false; bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { final r = await DbHelper.getAllReviews(); final s = await DbHelper.getReviewSummaryByColonia(); if (mounted) setState(() { _reviews = r; _summary = s; _loading = false; }); } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: Text(_showSummary ? 'Calificaciones por Colonia' : 'Reseñas Ciudadanas'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), actions: [ IconButton( icon: Icon(_showSummary ? Icons.list : Icons.bar_chart), tooltip: _showSummary ? 'Ver reseñas' : 'Ver por colonia', onPressed: () => setState(() => _showSummary = !_showSummary)), IconButton(icon: const Icon(Icons.refresh), onPressed: _load), ]), body: _loading ? const Center(child: CircularProgressIndicator()) : _showSummary ? _buildSummary() : _buildReviews(), ); Widget _buildSummary() { if (_summary.isEmpty) return const Center( child: Text('Sin calificaciones aún', style: TextStyle(color: AppColors.grisTexto))); return Column(children: [ // Header explicativo Container(margin: const EdgeInsets.all(12), padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200)), child: const Row(children: [ Icon(Icons.info_outline, color: AppColors.azulInfo, size: 16), SizedBox(width: 6), Expanded(child: Text( 'Colonias ordenadas de menor a mayor calificación. ' 'Las primeras requieren atención prioritaria.', style: TextStyle(fontSize: 11, color: AppColors.azulInfo))), ])), Expanded(child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: _summary.length, itemBuilder: (_, i) { final s = _summary[i]; final prom = (s['promedio'] as num).toDouble(); final total = s['total'] as int; final colonia = s['colonia'] as String; final routeId = s['route_id'] as String; final color = prom >= 4.5 ? AppColors.verdeExito : prom >= 3.5 ? Colors.amber.shade700 : prom >= 2.5 ? AppColors.naranjaAlerta : AppColors.rojoError; final emoji = prom >= 4.5 ? '🟢' : prom >= 3.5 ? '🟡' : prom >= 2.5 ? '🟠' : '🔴'; return Card(margin: const EdgeInsets.only(bottom: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: color.withOpacity(0.3))), child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [ Container(width: 6, height: 50, decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(3))), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(colonia, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), Text('$emoji $routeId • $total reseña${total != 1 ? "s" : ""}', style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)), ])), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [ Text(prom.toStringAsFixed(1), style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)), Row(children: List.generate(5, (j) => Icon(j < prom.round() ? Icons.star : Icons.star_border, color: Colors.amber, size: 12))), ]), ]))); })), ]); } Widget _buildReviews() { if (_reviews.isEmpty) return const Center( child: Text('Sin reseñas aún', style: TextStyle(color: AppColors.grisTexto))); return ListView.builder( padding: const EdgeInsets.all(12), itemCount: _reviews.length, itemBuilder: (_, i) { final r = _reviews[i]; final fecha = DateTime.tryParse(r.fecha); final fechaStr = fecha != null ? '${fecha.day}/${fecha.month}/${fecha.year} ${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}' : r.fecha; return Card(margin: const EdgeInsets.only(bottom: 8), child: Padding(padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ CircleAvatar(backgroundColor: AppColors.guindaPrimary.withOpacity(0.1), radius: 18, child: Text('${r.estrellas}⭐', style: const TextStyle(fontSize: 11))), const SizedBox(width: 10), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(r.nombreUsuario, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), Text('${r.colonia} — ${r.routeId}', style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)), ])), Text(fechaStr, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), ]), const SizedBox(height: 8), Row(children: List.generate(5, (j) => Icon(j < r.estrellas ? Icons.star : Icons.star_border, color: Colors.amber, size: 16))), if (r.comentario.isNotEmpty && r.comentario != 'Sin comentario') ...[ const SizedBox(height: 6), Text('"${r.comentario}"', style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic, color: AppColors.negroTexto)), ], ]))); }); } } // ── TAB 3: Asignaciones LMV / MJS ──────────────────────────────────────── class _AdminAssignmentsTab extends StatefulWidget { @override State<_AdminAssignmentsTab> createState() => _AdminAssignmentsTabState(); } class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> { List _conductores = []; UserModel? _sel; List _asigs = []; static const _grupoA = ['LUNES', 'MIERCOLES', 'VIERNES']; static const _grupoB = ['MARTES', 'JUEVES', 'SABADO']; @override void initState() { super.initState(); _load(); } Future _load() async { final c = await DbHelper.getUsersByRol('CONDUCTOR'); if (mounted) setState(() => _conductores = c); } Future _loadAsigs(int id) async { final a = await DbHelper.getAsignacionesByConductor(id); if (mounted) setState(() => _asigs = a); } AssignmentModel? _getGrupo(List dias) { for (final dia in dias) { try { return _asigs.firstWhere((a) => a.diaSemana == dia); } catch (_) {} } return null; } Future _saveGrupo(List dias, String routeId, String turno) async { for (final dia in dias) { await DbHelper.upsertAsignacion(AssignmentModel( conductorId: _sel!.id!, routeId: routeId, diaSemana: dia, turno: turno)); } await _loadAsigs(_sel!.id!); } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: const Text('Asignar Rutas a Conductores'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado))), body: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [ Container(padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200)), child: const Text( 'Cada conductor opera en un bloque:\n' 'Grupo A — Lunes, Miercoles y Viernes\n' 'Grupo B — Martes, Jueves y Sabado', style: TextStyle(fontSize: 12, color: AppColors.azulInfo))), DropdownButtonFormField( decoration: const InputDecoration(labelText: 'Selecciona conductor', border: OutlineInputBorder(), filled: true, fillColor: Colors.white), hint: const Text('Conductor...'), value: _sel, items: _conductores.map((c) => DropdownMenuItem(value: c, child: Text(c.nombre, style: const TextStyle(fontSize: 13)))).toList(), onChanged: (c) { setState(() => _sel = c); if (c != null) _loadAsigs(c.id!); }), if (_sel != null) ...[ const SizedBox(height: 20), _GrupoRow(label: 'Grupo A — Lunes, Miercoles y Viernes', icon: Icons.wb_sunny_outlined, color: Colors.blue, current: _getGrupo(_grupoA), routeIds: routesData.map((r) => r.routeId).toList(), onSave: (rid, turno) => _saveGrupo(_grupoA, rid, turno)), const SizedBox(height: 12), _GrupoRow(label: 'Grupo B — Martes, Jueves y Sabado', icon: Icons.wb_twilight, color: Colors.deepPurple, current: _getGrupo(_grupoB), routeIds: routesData.map((r) => r.routeId).toList(), onSave: (rid, turno) => _saveGrupo(_grupoB, rid, turno)), if (_asigs.isNotEmpty) ...[ const SizedBox(height: 20), const Text('Resumen actual', style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.verdeAdmin, fontSize: 14)), const SizedBox(height: 8), Card(child: Padding(padding: const EdgeInsets.all(12), child: Column(children: [ ..._asigs.map((a) => Padding(padding: const EdgeInsets.symmetric(vertical: 3), child: Row(children: [ SizedBox(width: 100, child: Text(AppDias.label(a.diaSemana), style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12))), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), child: Text('${a.routeId} • ${a.turno}', style: const TextStyle(fontSize: 11, color: AppColors.verdeAdmin))), ]))), ]))), ], ], ])), ); } class _GrupoRow extends StatefulWidget { final String label; final IconData icon; final Color color; final AssignmentModel? current; final List routeIds; final Function(String, String) onSave; const _GrupoRow({required this.label, required this.icon, required this.color, required this.current, required this.routeIds, required this.onSave}); @override State<_GrupoRow> createState() => _GrupoRowState(); } class _GrupoRowState extends State<_GrupoRow> { String? _route; String _turno = 'MATUTINO'; @override void initState() { super.initState(); _route = widget.current?.routeId; _turno = widget.current?.turno ?? 'MATUTINO'; } @override Widget build(BuildContext context) => Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: widget.color.withOpacity(0.3))), child: Padding(padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon(widget.icon, color: widget.color, size: 18), const SizedBox(width: 8), Expanded(child: Text(widget.label, style: TextStyle( fontWeight: FontWeight.bold, color: widget.color, fontSize: 13))), ]), const SizedBox(height: 12), Row(children: [ Expanded(child: DropdownButtonFormField( value: _route, decoration: const InputDecoration(labelText: 'Ruta', border: OutlineInputBorder(), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10)), hint: const Text('Sin ruta', style: TextStyle(fontSize: 12)), items: widget.routeIds.map((r) => DropdownMenuItem(value: r, child: Text(r, style: const TextStyle(fontSize: 12)))).toList(), onChanged: (v) => setState(() => _route = v))), const SizedBox(width: 8), SizedBox(width: 130, child: DropdownButtonFormField( value: _turno, decoration: const InputDecoration(labelText: 'Turno', border: OutlineInputBorder(), isDense: true, contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 10)), items: const [ DropdownMenuItem(value: 'MATUTINO', child: Text('Matutino', style: TextStyle(fontSize: 12))), DropdownMenuItem(value: 'VESPERTINO', child: Text('Vespertino', style: TextStyle(fontSize: 12))), DropdownMenuItem(value: 'NOCTURNO', child: Text('Nocturno', style: TextStyle(fontSize: 12))), ], onChanged: (v) => setState(() => _turno = v!))), const SizedBox(width: 8), ElevatedButton( onPressed: _route == null ? null : () => widget.onSave(_route!, _turno), style: ElevatedButton.styleFrom(backgroundColor: widget.color, foregroundColor: Colors.white, minimumSize: const Size(50, 42), padding: EdgeInsets.zero, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), child: const Icon(Icons.save, size: 18)), ]), ]))); } // ── TAB 4: Alertas del sistema ──────────────────────────────────────────── class _AdminAlertasTab extends StatefulWidget { final RouteSimulatorService sim; const _AdminAlertasTab({required this.sim}); @override State<_AdminAlertasTab> createState() => _AdminAlertasTabState(); } class _AdminAlertasTabState extends State<_AdminAlertasTab> { List _alertas = []; bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { final a = await DbHelper.getAlertas(); if (mounted) setState(() { _alertas = a; _loading = false; }); } Color _alertaColor(String tipo) { if (tipo.startsWith('INCIDENTE')) return AppColors.naranjaAlerta; if (tipo == 'GPS_PERDIDO') return AppColors.rojoError; if (tipo == 'CAMION_DETENIDO') return AppColors.naranjaAlerta; if (tipo.startsWith('RUTA_')) return AppColors.rojoError; return AppColors.azulInfo; } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, title: Text('Alertas del Sistema (${_alertas.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _load)]), body: _loading ? const Center(child: CircularProgressIndicator()) : _alertas.isEmpty ? const Center(child: Text('Sin alertas', style: TextStyle(color: AppColors.grisTexto))) : ListView.builder( padding: const EdgeInsets.all(12), itemCount: _alertas.length, itemBuilder: (_, i) { final a = _alertas[i]; final c = _alertaColor(a.tipo); return Card(margin: const EdgeInsets.only(bottom: 6), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8), side: BorderSide(color: c.withOpacity(0.3))), child: ListTile(dense: true, leading: CircleAvatar(radius: 18, backgroundColor: c.withOpacity(0.12), child: Icon(a.resuelta ? Icons.check : Icons.warning, color: a.resuelta ? AppColors.verdeExito : c, size: 16)), title: Text(a.tipo.replaceAll('_', ' '), style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, color: a.resuelta ? AppColors.grisTexto : c)), subtitle: Text(a.mensaje, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)), trailing: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ Text(a.routeId, style: const TextStyle( fontSize: 10, color: AppColors.grisTexto)), if (!a.resuelta) TextButton( onPressed: () async { await DbHelper.resolverAlerta(a.id!); _load(); }, style: TextButton.styleFrom( padding: EdgeInsets.zero, minimumSize: Size.zero, foregroundColor: AppColors.verdeExito), child: const Text('Resolver', style: TextStyle(fontSize: 10))), ]))); }), ); } // ── Widgets auxiliares ──────────────────────────────────────────────────── class _Stat extends StatelessWidget { final String label, value; final IconData icon; final Color color; const _Stat(this.label, this.value, this.icon, this.color); @override Widget build(BuildContext context) => Expanded(child: Card(child: Padding( padding: const EdgeInsets.all(14), child: Row(children: [ CircleAvatar(radius: 20, backgroundColor: color.withOpacity(0.12), child: Icon(icon, color: color, size: 20)), const SizedBox(width: 10), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(value, style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: color)), Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), ]), ])))); } class _AdminBanner extends StatelessWidget { final AppNotification notif; final VoidCallback onDismiss; const _AdminBanner({required this.notif, required this.onDismiss}); @override Widget build(BuildContext context) => Material(color: Colors.transparent, child: Container(margin: const EdgeInsets.all(10), decoration: BoxDecoration(color: AppColors.verdeAdmin, borderRadius: BorderRadius.circular(10), boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6)]), child: Padding(padding: const EdgeInsets.all(10), child: Row(children: [ const Icon(Icons.notifications, color: Colors.white, size: 20), const SizedBox(width: 8), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text(notif.title, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)), Text(notif.body, style: const TextStyle(color: Colors.white70, fontSize: 10), maxLines: 1, overflow: TextOverflow.ellipsis), ])), IconButton(icon: const Icon(Icons.close, color: Colors.white, size: 16), onPressed: onDismiss), ])))); }