From 8fe3665ffb95e8ac365020c1d4e8cb66b1fdd477 Mon Sep 17 00:00:00 2001 From: LunaDiegoLeo Date: Sat, 23 May 2026 08:36:15 -0600 Subject: [PATCH] Actualizacion de mejoras --- lib/database/db_helper.dart | 30 +- lib/screens/admin/admin_dashboard_screen.dart | 496 +++++++++++++++--- .../admin/admin_reporte_detalle_screen.dart | 16 +- lib/screens/admin/admin_stats_screen.dart | 10 +- lib/screens/admin/create_route_screen.dart | 28 +- lib/screens/admin/export_pdf_screen.dart | 12 +- .../admin/manage_conductors_screen.dart | 81 ++- lib/screens/citizen/review_screen.dart | 35 +- lib/screens/driver/driver_home_screen.dart | 56 +- lib/screens/shared/reporte_chat_screen.dart | 259 +++++---- lib/widgets/route_map_widget.dart | 6 +- 11 files changed, 738 insertions(+), 291 deletions(-) diff --git a/lib/database/db_helper.dart b/lib/database/db_helper.dart index 9746bea..1d1fd30 100644 --- a/lib/database/db_helper.dart +++ b/lib/database/db_helper.dart @@ -12,7 +12,7 @@ class DbHelper { static Future _initDb() async { final path = join(await getDatabasesPath(), 'celaya_v3.db'); - return openDatabase(path, version: 2, + return openDatabase(path, version: 3, onCreate: _onCreate, onUpgrade: _onUpgrade); } @@ -108,19 +108,27 @@ class DbHelper { // Migración incremental — se ejecuta al actualizar la app static Future _onUpgrade(Database db, int oldV, int newV) async { - // Agregar columnas/tablas que pueden faltar en instalaciones anteriores - final helpers = [ - // foto_path en reportes + // Lista de migraciones seguras (todas usan IF NOT EXISTS o ignoran errores) + final sqls = [ + // Columnas que pueden faltar "ALTER TABLE reportes ADD COLUMN foto_path TEXT", - // Tablas nuevas (IF NOT EXISTS para no fallar si ya existen) + // Tabla reviews (puede no existir en instalaciones viejas) + '''CREATE TABLE IF NOT EXISTS reviews( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, colonia TEXT NOT NULL, + route_id TEXT NOT NULL, estrellas INTEGER NOT NULL, + comentario TEXT NOT NULL, fecha TEXT NOT NULL, + nombre_usuario TEXT DEFAULT 'Ciudadano')''', + // Tabla user_meta + '''CREATE TABLE IF NOT EXISTS user_meta( + user_id INTEGER PRIMARY KEY, activo INTEGER DEFAULT 1, notas TEXT)''', + // Tabla notification_history '''CREATE TABLE IF NOT EXISTS notification_history( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, route_id TEXT NOT NULL, event_type TEXT NOT NULL, title TEXT NOT NULL, - body TEXT NOT NULL, fecha TEXT NOT NULL, - leida INTEGER DEFAULT 0)''', - '''CREATE TABLE IF NOT EXISTS user_meta( - user_id INTEGER PRIMARY KEY, activo INTEGER DEFAULT 1, notas TEXT)''', + body TEXT NOT NULL, fecha TEXT NOT NULL, leida INTEGER DEFAULT 0)''', + // Tablas de gestión de reportes '''CREATE TABLE IF NOT EXISTS reporte_notas( id INTEGER PRIMARY KEY AUTOINCREMENT, reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, @@ -134,6 +142,7 @@ class DbHelper { reporte_id INTEGER NOT NULL, user_id INTEGER NOT NULL, rol TEXT NOT NULL, mensaje TEXT NOT NULL, fecha TEXT NOT NULL, leido INTEGER DEFAULT 0)''', + // Tabla route_definitions '''CREATE TABLE IF NOT EXISTS route_definitions( id INTEGER PRIMARY KEY AUTOINCREMENT, route_id TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, @@ -141,9 +150,8 @@ class DbHelper { hora_fin TEXT NOT NULL, turno TEXT NOT NULL, colonias TEXT NOT NULL, activa INTEGER DEFAULT 1)''', ]; - for (final sql in helpers) { + for (final sql in sqls) { try { await db.execute(sql); } catch (_) {} - // Ignorar errores (ej. columna ya existe) } } diff --git a/lib/screens/admin/admin_dashboard_screen.dart b/lib/screens/admin/admin_dashboard_screen.dart index ab67fbc..ee68aa8 100644 --- a/lib/screens/admin/admin_dashboard_screen.dart +++ b/lib/screens/admin/admin_dashboard_screen.dart @@ -49,22 +49,22 @@ class _AdminDashboardScreenState extends State { selectedIndex:_tab, onDestinationSelected:(i)=>setState(()=>_tab=i), backgroundColor:Colors.white, - indicatorColor:AppColors.verdeAdmin.withOpacity(0.15), + indicatorColor:AppColors.guindaPrimary.withOpacity(0.15), destinations:const[ NavigationDestination(icon:Icon(Icons.dashboard_outlined), - selectedIcon:Icon(Icons.dashboard,color:AppColors.verdeAdmin),label:'Panel'), + selectedIcon:Icon(Icons.dashboard,color:AppColors.guindaPrimary),label:'Panel'), NavigationDestination(icon:Icon(Icons.map_outlined), - selectedIcon:Icon(Icons.map,color:AppColors.verdeAdmin),label:'Mapa'), + selectedIcon:Icon(Icons.map,color:AppColors.guindaPrimary),label:'Mapa'), NavigationDestination(icon:Icon(Icons.report_outlined), - selectedIcon:Icon(Icons.report,color:AppColors.verdeAdmin),label:'Reportes'), + selectedIcon:Icon(Icons.report,color:AppColors.guindaPrimary),label:'Reportes'), NavigationDestination(icon:Icon(Icons.people_alt_outlined), - selectedIcon:Icon(Icons.people_alt,color:AppColors.verdeAdmin),label:'Asignar'), + selectedIcon:Icon(Icons.people_alt,color:AppColors.guindaPrimary),label:'Asignar'), NavigationDestination(icon:Icon(Icons.warning_outlined), - selectedIcon:Icon(Icons.warning,color:AppColors.verdeAdmin),label:'Alertas'), + selectedIcon:Icon(Icons.warning,color:AppColors.guindaPrimary),label:'Alertas'), NavigationDestination(icon:Icon(Icons.route_outlined), - selectedIcon:Icon(Icons.route,color:AppColors.verdeAdmin),label:'Rutas'), + selectedIcon:Icon(Icons.route,color:AppColors.guindaPrimary),label:'Rutas'), NavigationDestination(icon:Icon(Icons.star_outline), - selectedIcon:Icon(Icons.star,color:AppColors.verdeAdmin),label:'Reseñas'), + selectedIcon:Icon(Icons.star,color:AppColors.guindaPrimary),label:'Reseñas'), ], ), ); @@ -137,7 +137,7 @@ class _AdminHomeTabState extends State<_AdminHomeTab> { @override Widget build(BuildContext context) { return CustomScrollView(slivers: [ - SliverAppBar(pinned: true, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + SliverAppBar(pinned: true, backgroundColor: AppColors.guindaPrimary, 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)), @@ -159,14 +159,14 @@ class _AdminHomeTabState extends State<_AdminHomeTab> { ), SliverPadding(padding: const EdgeInsets.all(12), sliver: SliverList(delegate: SliverChildListDelegate([ Row(children: [ - _Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.verdeAdmin), + _Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.guindaPrimary), 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)), + fontSize: 16, color: AppColors.guindaPrimary)), const SizedBox(height: 8), ...routesData.map((r) { final status = _getStatus(r.routeId); @@ -249,18 +249,18 @@ class _AdminHomeTabState extends State<_AdminHomeTab> { Padding( padding: const EdgeInsets.fromLTRB(14, 6, 14, 2), child: Row(children: [ - const Icon(Icons.build, size: 13, color: AppColors.moradoConductor), + const Icon(Icons.build, size: 13, color: AppColors.guindaPrimary), const SizedBox(width: 4), const Text('Incidentes del conductor:', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, - color: AppColors.moradoConductor)), + color: AppColors.guindaPrimary)), ]), ), ...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, + decoration: const BoxDecoration(color: AppColors.guindaPrimary, shape: BoxShape.circle)), const SizedBox(width: 6), Expanded(child: Text(inc.mensaje, @@ -286,7 +286,7 @@ class _AdminHomeTabState extends State<_AdminHomeTab> { } }, style: TextButton.styleFrom( - foregroundColor: AppColors.verdeAdmin, + foregroundColor: AppColors.guindaPrimary, padding: const EdgeInsets.symmetric(horizontal: 8)), child: const Text('Actuar', style: TextStyle(fontSize: 10)), ), @@ -396,7 +396,7 @@ class _AdminMapTab extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar:AppBar(automaticallyImplyLeading:false, - backgroundColor:AppColors.verdeAdmin,foregroundColor:Colors.white, + backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white, title:const Text('Mapa — Todas las Rutas'), bottom:PreferredSize(preferredSize:const Size.fromHeight(4), child:Container(height:4,color:AppColors.dorado))), @@ -444,7 +444,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Text('Reportes Ciudadanos (${_filtered.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -464,7 +464,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> { style: TextStyle(fontSize: 11, color: sel ? Colors.white : AppColors.negroTexto)), selected: sel, - selectedColor: e == 'TODOS' ? AppColors.verdeAdmin : _estadoColor(e), + selectedColor: e == 'TODOS' ? AppColors.guindaPrimary : _estadoColor(e), checkmarkColor: Colors.white, onSelected: (_) => setState(() => _filtroEstado = e))); })), @@ -500,7 +500,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Text(folio, style: const TextStyle(fontWeight: FontWeight.bold, - fontSize: 13, color: AppColors.verdeAdmin)), + fontSize: 13, color: AppColors.guindaPrimary)), const SizedBox(width: 8), Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), decoration: BoxDecoration( @@ -533,7 +533,7 @@ class _AdminConductoresTab extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: const Text('Gestión de Conductores'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -565,7 +565,7 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Text('Rutas del Sistema (${_routes.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -589,7 +589,7 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> { const Text('No hay rutas creadas', style: TextStyle(color: AppColors.grisTexto)), const SizedBox(height: 16), ElevatedButton.icon( - style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, + style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white), onPressed: () async { final ok = await Navigator.push(context, MaterialPageRoute( @@ -607,13 +607,13 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> { : 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))), + side: BorderSide(color: AppColors.guindaPrimary.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))), + fontSize: 14, color: AppColors.guindaPrimary))), IconButton(icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () async { final ok = await Navigator.push(context, MaterialPageRoute( @@ -636,10 +636,10 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> { 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), + decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: Text(c, style: const TextStyle(fontSize: 10, - color: AppColors.verdeAdmin)))).toList()), + color: AppColors.guindaPrimary)))).toList()), if (r.colonias.length > 8) Text(' ...y ${r.colonias.length - 8} más', style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), @@ -670,7 +670,7 @@ class _AdminReviewsTabState extends State<_AdminReviewsTab> { @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, 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)), @@ -794,11 +794,19 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> { static const _grupoA = ['LUNES', 'MIERCOLES', 'VIERNES']; static const _grupoB = ['MARTES', 'JUEVES', 'SABADO']; + List _todasLasRutas = []; + @override void initState() { super.initState(); _load(); } Future _load() async { final c = await DbHelper.getUsersByRol('CONDUCTOR'); - if (mounted) setState(() => _conductores = c); + // Combinar rutas hardcoded + rutas creadas por admin en DB + final dbRoutes = await DbHelper.getAllRouteDefinitions(); + final dbIds = dbRoutes.map((r) => r.routeId).toList(); + final staticIds = routesData.map((r) => r.routeId).toList(); + // Unión sin duplicados + final allIds = {...staticIds, ...dbIds}.toList()..sort(); + if (mounted) setState(() { _conductores = c; _todasLasRutas = allIds; }); } Future _loadAsigs(int id) async { @@ -821,14 +829,227 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> { await _loadAsigs(_sel!.id!); } + Future _showNuevoConductor(BuildContext ctx) async { + final nombreCtrl = TextEditingController(); + final emailCtrl = TextEditingController(); + final passCtrl = TextEditingController(); + bool obscure = true; + + await showDialog(context: ctx, builder: (dCtx) => StatefulBuilder( + builder: (dCtx, setSt) => AlertDialog( + title: const Row(children: [ + Icon(Icons.person_add, color: AppColors.guindaPrimary), + SizedBox(width: 8), + Text('Nuevo Conductor'), + ]), + content: SingleChildScrollView(child: Column(mainAxisSize: MainAxisSize.min, children: [ + TextField(controller: nombreCtrl, + textCapitalization: TextCapitalization.words, + decoration: const InputDecoration(labelText: 'Nombre completo', + prefixIcon: Icon(Icons.person_outline), border: OutlineInputBorder())), + const SizedBox(height: 10), + TextField(controller: emailCtrl, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: 'Correo electronico', + prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder())), + const SizedBox(height: 10), + TextField(controller: passCtrl, obscureText: obscure, + decoration: InputDecoration(labelText: 'Contrasena', + prefixIcon: const Icon(Icons.lock_outline), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: Icon(obscure ? Icons.visibility_off : Icons.visibility), + onPressed: () => setSt(() => obscure = !obscure)))), + ])), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dCtx), + child: const Text('Cancelar')), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.guindaPrimary, + foregroundColor: Colors.white), + onPressed: () async { + final nombre = nombreCtrl.text.trim(); + final email = emailCtrl.text.trim().toLowerCase(); + final pass = passCtrl.text; + if (nombre.isEmpty || email.isEmpty) { + ScaffoldMessenger.of(dCtx).showSnackBar(const SnackBar( + content: Text('Completa nombre y correo'), + backgroundColor: AppColors.rojoError)); + return; + } + if (pass.length < 6) { + ScaffoldMessenger.of(dCtx).showSnackBar(const SnackBar( + content: Text('La contrasena debe tener minimo 6 caracteres'), + backgroundColor: AppColors.rojoError)); + return; + } + Navigator.pop(dCtx); + await Future.delayed(const Duration(milliseconds: 100)); + try { + final uid = await DbHelper.insertConductor(nombre, email, pass); + await _load(); + if (mounted) { + final idx = _conductores.indexWhere((c) => c.id == uid); + if (idx >= 0) { + setState(() => _sel = _conductores[idx]); + await _loadAsigs(_conductores[idx].id!); + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(nombre + ' creado. Asignale una ruta abajo.'), + backgroundColor: AppColors.verdeExito, + duration: const Duration(seconds: 3))); + } + } catch (e) { + if (mounted) { + final msg = e.toString().contains('UNIQUE') + ? 'Ese correo ya esta registrado' + : 'Error: ' + e.toString(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(msg), backgroundColor: AppColors.rojoError)); + } + } + }, + child: const Text('Crear Conductor')), + ]))); + + nombreCtrl.dispose(); + emailCtrl.dispose(); + passCtrl.dispose(); + } + + + Future _showEditarConductor(BuildContext ctx, UserModel conductor) async { + final nombreCtrl = TextEditingController(text: conductor.nombre); + final emailCtrl = TextEditingController(text: conductor.email); + + await showDialog(context: ctx, builder: (dCtx) => AlertDialog( + title: const Row(children: [ + Icon(Icons.edit, color: AppColors.guindaPrimary), + SizedBox(width: 8), + Text('Editar Conductor'), + ]), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + TextField(controller: nombreCtrl, textCapitalization: TextCapitalization.words, + decoration: const InputDecoration(labelText: 'Nombre completo', + prefixIcon: Icon(Icons.person_outline), border: OutlineInputBorder())), + const SizedBox(height: 10), + TextField(controller: emailCtrl, keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: 'Correo electronico', + prefixIcon: Icon(Icons.email_outlined), border: OutlineInputBorder())), + ]), + actions: [ + TextButton(onPressed: () => Navigator.pop(dCtx), child: const Text('Cancelar')), + ElevatedButton.icon( + style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary, + foregroundColor: Colors.white), + onPressed: () async { + await DbHelper.updateConductor(conductor.id!, + nombreCtrl.text.trim(), emailCtrl.text.trim().toLowerCase()); + if (dCtx.mounted) Navigator.pop(dCtx); + await _load(); + }, + icon: const Icon(Icons.save, size: 16), + label: const Text('Guardar')), + ])); + + nombreCtrl.dispose(); emailCtrl.dispose(); + } + @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, - title: const Text('Asignar Rutas a Conductores'), + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, + title: const Text('Conductores y Asignaciones'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), - child: Container(height: 4, color: AppColors.dorado))), + child: Container(height: 4, color: AppColors.dorado)), + actions: [ + IconButton( + icon: const Icon(Icons.person_add_outlined), + tooltip: 'Nuevo conductor', + onPressed: () => _showNuevoConductor(context)), + IconButton(icon: const Icon(Icons.refresh), onPressed: _load), + ]), body: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [ + // Lista de conductores con chip seleccionable + if (_conductores.isEmpty) + Container(padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration(color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade200)), + child: Row(children: [ + const Icon(Icons.info_outline, color: AppColors.naranjaAlerta), + const SizedBox(width: 8), + const Expanded(child: Text('No hay conductores registrados. Agrega uno con el boton +', + style: TextStyle(fontSize: 12, color: AppColors.naranjaAlerta))), + ])) + else ...[ + const Align(alignment: Alignment.centerLeft, + child: Text('Conductores registrados', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13, + color: AppColors.guindaPrimary))), + const SizedBox(height: 8), + SizedBox(height: 44, child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _conductores.length, + itemBuilder: (_, i) { + final c = _conductores[i]; + final sel = _sel?.id == c.id; + return Padding(padding: const EdgeInsets.only(right: 8), + child: InkWell( + borderRadius: BorderRadius.circular(22), + onTap: () { setState(() => _sel = c); _loadAsigs(c.id!); }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: sel ? AppColors.guindaPrimary : Colors.white, + borderRadius: BorderRadius.circular(22), + border: Border.all( + color: sel ? AppColors.guindaPrimary : Colors.grey.shade300, + width: sel ? 2 : 1), + boxShadow: sel ? [BoxShadow(color: AppColors.guindaPrimary.withOpacity(0.3), + blurRadius: 6, offset: const Offset(0, 2))] : [], + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + CircleAvatar(radius: 12, + backgroundColor: sel ? Colors.white.withOpacity(0.3) : AppColors.guindaPrimary.withOpacity(0.1), + child: Text(c.nombre[0].toUpperCase(), + style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, + color: sel ? Colors.white : AppColors.guindaPrimary))), + const SizedBox(width: 6), + Text(c.nombre.split(' ').first, + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, + color: sel ? Colors.white : AppColors.negroTexto)), + ]), + ))); + })), + const SizedBox(height: 16), + ], + // Info del conductor seleccionado + if (_sel != null) ...[ + Container(padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.06), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.guindaPrimary.withOpacity(0.2))), + child: Row(children: [ + CircleAvatar(radius: 18, backgroundColor: AppColors.guindaPrimary, + child: Text(_sel!.nombre[0].toUpperCase(), + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))), + const SizedBox(width: 10), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(_sel!.nombre, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)), + Text(_sel!.email, style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)), + ])), + TextButton.icon( + onPressed: () => _showEditarConductor(context, _sel!), + icon: const Icon(Icons.edit_outlined, size: 14), + label: const Text('Editar', style: TextStyle(fontSize: 11)), + style: TextButton.styleFrom(foregroundColor: AppColors.guindaPrimary)), + ])), + ], + // Info de grupos 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)), @@ -837,29 +1058,21 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> { '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(), + current: _getGrupo(_grupoA), routeIds: _todasLasRutas, 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(), + current: _getGrupo(_grupoB), routeIds: _todasLasRutas, 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)), + color: AppColors.guindaPrimary, 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), @@ -867,10 +1080,10 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> { 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), + decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), child: Text('${a.routeId} • ${a.turno}', - style: const TextStyle(fontSize: 11, color: AppColors.verdeAdmin))), + style: const TextStyle(fontSize: 11, color: AppColors.guindaPrimary))), ]))), ]))), ], @@ -947,6 +1160,7 @@ class _AdminAlertasTab extends StatefulWidget { class _AdminAlertasTabState extends State<_AdminAlertasTab> { List _alertas = []; bool _loading = true; + String _filtro = 'TODAS'; @override void initState() { super.initState(); _load(); } @@ -955,62 +1169,172 @@ class _AdminAlertasTabState extends State<_AdminAlertasTab> { if (mounted) setState(() { _alertas = a; _loading = false; }); } - Color _alertaColor(String tipo) { + List get _filtered { + if (_filtro == 'TODAS') return _alertas; + if (_filtro == 'ACTIVAS') return _alertas.where((a) => !a.resuelta).toList(); + return _alertas.where((a) => a.resuelta).toList(); + } + + Color _color(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; + if (tipo.startsWith('RUTA_')) return AppColors.guindaPrimary; return AppColors.azulInfo; } + IconData _icon(String tipo) { + if (tipo.startsWith('INCIDENTE')) return Icons.build; + if (tipo == 'GPS_PERDIDO') return Icons.gps_off; + if (tipo == 'CAMION_DETENIDO') return Icons.timer_off; + if (tipo == 'RUTA_CANCELADA') return Icons.cancel; + if (tipo == 'RUTA_RETRASADA') return Icons.access_time; + return Icons.warning_amber; + } + + Future _showAcciones(AlertaModel a) async { + showModalBottomSheet(context: context, shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (_) => SafeArea(child: Column(mainAxisSize: MainAxisSize.min, children: [ + Container(margin: const EdgeInsets.symmetric(vertical: 8), + width: 40, height: 4, + decoration: BoxDecoration(color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2))), + Padding(padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text(a.tipo.replaceAll('_', ' '), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15))), + Padding(padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text(a.mensaje, style: const TextStyle(color: AppColors.grisTexto, fontSize: 12))), + const Divider(), + ListTile( + leading: const CircleAvatar(radius: 18, + backgroundColor: Color(0xFFE8F5E9), + child: Icon(Icons.check_circle, color: AppColors.verdeExito, size: 20)), + title: const Text('Marcar como resuelta', + style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: const Text('La alerta se archivará', + style: TextStyle(fontSize: 11)), + onTap: () async { + Navigator.pop(context); + await DbHelper.resolverAlerta(a.id!); + await _load(); + }), + if (a.tipo.startsWith('INCIDENTE')) + ListTile( + leading: CircleAvatar(radius: 18, + backgroundColor: AppColors.guindaPrimary.withOpacity(0.1), + child: const Icon(Icons.route, color: AppColors.guindaPrimary, size: 20)), + title: const Text('Ver ruta afectada', + style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: Text('Ruta: ${a.routeId}', + style: const TextStyle(fontSize: 11)), + onTap: () => Navigator.pop(context)), + if (a.tipo == 'GPS_PERDIDO' || a.tipo == 'CAMION_DETENIDO') + ListTile( + leading: const CircleAvatar(radius: 18, + backgroundColor: Color(0xFFFFF3E0), + child: Icon(Icons.info_outline, color: AppColors.naranjaAlerta, size: 20)), + title: const Text('Ignorar temporalmente', + style: TextStyle(fontWeight: FontWeight.w600)), + subtitle: const Text('No se tomará acción ahora', + style: TextStyle(fontSize: 11)), + onTap: () => Navigator.pop(context)), + ListTile( + leading: const CircleAvatar(radius: 18, + backgroundColor: Color(0xFFFFEBEE), + child: Icon(Icons.delete_outline, color: AppColors.rojoError, size: 20)), + title: const Text('Eliminar alerta', + style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.rojoError)), + subtitle: const Text('Se borrará permanentemente', + style: TextStyle(fontSize: 11)), + onTap: () async { + Navigator.pop(context); + await DbHelper.resolverAlerta(a.id!); + await _load(); + }), + const SizedBox(height: 8), + ]))); + } + @override Widget build(BuildContext context) => Scaffold( appBar: AppBar(automaticallyImplyLeading: false, - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, - title: Text('Alertas del Sistema (${_alertas.length})'), + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, + title: Text('Alertas del Sistema (${_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: _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))), - ]))); - }), + body: Column(children: [ + // Filtros + Container(color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row(children: ['TODAS','ACTIVAS','RESUELTAS'].map((f) { + final sel = _filtro == f; + return Padding(padding: const EdgeInsets.only(right: 8), + child: FilterChip( + label: Text(f, style: TextStyle(fontSize: 11, + color: sel ? Colors.white : AppColors.negroTexto)), + selected: sel, + selectedColor: AppColors.guindaPrimary, + checkmarkColor: Colors.white, + onSelected: (_) => setState(() => _filtro = f))); + }).toList())), + // Lista + Expanded(child: _loading + ? const Center(child: CircularProgressIndicator(color: AppColors.guindaPrimary)) + : _filtered.isEmpty + ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Icon(Icons.notifications_none, color: AppColors.grisTexto, size: 48), + const SizedBox(height: 12), + Text(_filtro == 'ACTIVAS' ? 'Sin alertas activas' : 'Sin alertas', + style: const TextStyle(color: AppColors.grisTexto)), + ])) + : ListView.builder( + padding: const EdgeInsets.all(10), + itemCount: _filtered.length, + itemBuilder: (_, i) { + final a = _filtered[i]; + final c = _color(a.tipo); + final fecha = DateTime.tryParse(a.fecha); + final fechaStr = fecha != null + ? '${fecha.day}/${fecha.month} ${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}' + : ''; + return Card(margin: const EdgeInsets.only(bottom: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), + side: BorderSide(color: a.resuelta + ? Colors.grey.shade200 : c.withOpacity(0.3))), + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: a.resuelta ? null : () => _showAcciones(a), + child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [ + CircleAvatar(radius: 20, + backgroundColor: a.resuelta + ? Colors.grey.shade100 : c.withOpacity(0.12), + child: Icon(_icon(a.tipo), + color: a.resuelta ? AppColors.grisTexto : c, size: 18)), + const SizedBox(width: 12), + Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(a.tipo.replaceAll('_', ' '), + style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, + color: a.resuelta ? AppColors.grisTexto : c)), + const SizedBox(height: 2), + Text(a.mensaje, maxLines: 2, overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)), + const SizedBox(height: 2), + Text('${a.routeId} • $fechaStr', + style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), + ])), + const SizedBox(width: 8), + if (a.resuelta) + const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 20) + else + const Icon(Icons.chevron_right, color: AppColors.grisTexto, size: 20), + ])))); + })), + ]), ); } + // ── Widgets auxiliares ──────────────────────────────────────────────────── class _Stat extends StatelessWidget { final String label, value; final IconData icon; final Color color; @@ -1034,7 +1358,7 @@ class _AdminBanner extends StatelessWidget { @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), + decoration: BoxDecoration(color: AppColors.guindaPrimary, 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), diff --git a/lib/screens/admin/admin_reporte_detalle_screen.dart b/lib/screens/admin/admin_reporte_detalle_screen.dart index 7001eab..1be66ae 100644 --- a/lib/screens/admin/admin_reporte_detalle_screen.dart +++ b/lib/screens/admin/admin_reporte_detalle_screen.dart @@ -110,7 +110,7 @@ class _AdminReporteDetalleScreenState extends State return Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Reporte $folio', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), Text(_r['colonia'] as String? ?? '', style: const TextStyle(fontSize: 11, color: Colors.white70)), @@ -179,7 +179,7 @@ class _AdminReporteDetalleScreenState extends State // Cambiar estado if (!isClosed) ...[ const Text('Cambiar Estado', style: TextStyle(fontWeight: FontWeight.bold, - color: AppColors.verdeAdmin, fontSize: 14)), + color: AppColors.guindaPrimary, fontSize: 14)), const SizedBox(height: 8), Wrap(spacing: 8, runSpacing: 8, children: _estados.map((e) { final isActual = e == estado; @@ -199,7 +199,7 @@ class _AdminReporteDetalleScreenState extends State Row(children: [ const Expanded(child: Text('Evidencias del Ayuntamiento', style: TextStyle(fontWeight: FontWeight.bold, - color: AppColors.verdeAdmin, fontSize: 14))), + color: AppColors.guindaPrimary, fontSize: 14))), Text('${_evidencias.length}', style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)), ]), @@ -221,8 +221,8 @@ class _AdminReporteDetalleScreenState extends State onPressed: _pickFoto, icon: const Icon(Icons.camera_alt, size: 16), label: const Text('Tomar foto', style: TextStyle(fontSize: 12)), - style: OutlinedButton.styleFrom(foregroundColor: AppColors.verdeAdmin, - side: const BorderSide(color: AppColors.verdeAdmin))) + style: OutlinedButton.styleFrom(foregroundColor: AppColors.guindaPrimary, + side: const BorderSide(color: AppColors.guindaPrimary))) : Stack(children: [ ClipRRect(borderRadius: BorderRadius.circular(6), child: Image.file(_evidFoto!, height: 70, width: double.infinity, fit: BoxFit.cover)), @@ -234,7 +234,7 @@ class _AdminReporteDetalleScreenState extends State const SizedBox(width: 8), ElevatedButton( onPressed: _loadingEv ? null : _agregarEvidencia, - style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, + style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, minimumSize: const Size(80, 42)), child: _loadingEv ? const SizedBox(width: 16, height: 16, @@ -249,11 +249,11 @@ class _AdminReporteDetalleScreenState extends State child: Padding(padding: const EdgeInsets.all(10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ - const Icon(Icons.verified, color: AppColors.verdeAdmin, size: 14), + const Icon(Icons.verified, color: AppColors.guindaPrimary, size: 14), const SizedBox(width: 4), Text(ev['admin_nombre'] as String? ?? 'Admin', style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11, - color: AppColors.verdeAdmin)), + color: AppColors.guindaPrimary)), const Spacer(), Text(_timeAgo(ev['fecha'] as String? ?? ''), style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), diff --git a/lib/screens/admin/admin_stats_screen.dart b/lib/screens/admin/admin_stats_screen.dart index 12dd6b5..6dcf583 100644 --- a/lib/screens/admin/admin_stats_screen.dart +++ b/lib/screens/admin/admin_stats_screen.dart @@ -31,7 +31,7 @@ class _AdminStatsScreenState extends State { Widget build(BuildContext context) => Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: const Text('Dashboard de Estadisticas'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -57,7 +57,7 @@ class _AdminStatsScreenState extends State { Icons.warning, AppColors.rojoError), const SizedBox(width: 8), _KpiCard('Conductores', '${_stats['total_conductores']}', - Icons.person, AppColors.moradoConductor), + Icons.person, AppColors.guindaPrimary), ]), const SizedBox(height: 20), @@ -93,10 +93,10 @@ class _AdminStatsScreenState extends State { FlSpot(e.key.toDouble(), (e.value['promedio'] as num? ?? 0).toDouble().clamp(1.0, 5.0))).toList(), isCurved: true, - color: AppColors.verdeAdmin, + color: AppColors.guindaPrimary, barWidth: 3, belowBarData: BarAreaData(show: true, - color: AppColors.verdeAdmin.withOpacity(0.1)), + color: AppColors.guindaPrimary.withOpacity(0.1)), dotData: const FlDotData(show: true), )], ))))), @@ -247,7 +247,7 @@ class _SectionTitle extends StatelessWidget { const _SectionTitle(this.title); @override Widget build(BuildContext context) => Text(title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.verdeAdmin)); + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.guindaPrimary)); } class _Legend extends StatelessWidget { diff --git a/lib/screens/admin/create_route_screen.dart b/lib/screens/admin/create_route_screen.dart index 859ef5a..928e853 100644 --- a/lib/screens/admin/create_route_screen.dart +++ b/lib/screens/admin/create_route_screen.dart @@ -93,7 +93,7 @@ class _CreateRouteScreenState extends State { return Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Text(widget.editing != null ? 'Editar Ruta' : 'Nueva Ruta'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -115,7 +115,7 @@ class _CreateRouteScreenState extends State { Expanded(child: RadioListTile(dense: true, value: t, groupValue: _turno, title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)), - activeColor: AppColors.verdeAdmin, + activeColor: AppColors.guindaPrimary, onChanged: (v) => setState(() => _turno = v!))) ).toList()), const SizedBox(height: 8), @@ -150,16 +150,16 @@ class _CreateRouteScreenState extends State { Expanded(child: OutlinedButton( onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)), style: OutlinedButton.styleFrom( - foregroundColor: AppColors.verdeAdmin, - side: const BorderSide(color: AppColors.verdeAdmin)), + foregroundColor: AppColors.guindaPrimary, + side: const BorderSide(color: AppColors.guindaPrimary)), child: const Text('Grupo A\nL/M/V', textAlign: TextAlign.center, style: TextStyle(fontSize: 11)))), const SizedBox(width: 8), Expanded(child: OutlinedButton( onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoB)), style: OutlinedButton.styleFrom( - foregroundColor: AppColors.moradoConductor, - side: const BorderSide(color: AppColors.moradoConductor)), + foregroundColor: AppColors.guindaPrimary, + side: const BorderSide(color: AppColors.guindaPrimary)), child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center, style: TextStyle(fontSize: 11)))), ]), @@ -170,7 +170,7 @@ class _CreateRouteScreenState extends State { label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11, color: sel ? Colors.white : AppColors.negroTexto)), selected: sel, - selectedColor: AppColors.verdeAdmin, + selectedColor: AppColors.guindaPrimary, checkmarkColor: Colors.white, onSelected: (v) => setState(() { if (v) _diasSeleccionados.add(dia); @@ -202,7 +202,7 @@ class _CreateRouteScreenState extends State { return CheckboxListTile(dense: true, title: Text(c, style: const TextStyle(fontSize: 12)), value: sel, - activeColor: AppColors.verdeAdmin, + activeColor: AppColors.guindaPrimary, controlAffinity: ListTileControlAffinity.leading, onChanged: (v) => setState(() { if (v == true) _coloniasSeleccionadas.add(c); @@ -216,8 +216,8 @@ class _CreateRouteScreenState extends State { const SizedBox(height: 8), Wrap(spacing: 4, runSpacing: 4, children: _coloniasSeleccionadas.map((c) => Chip(label: Text(c, style: const TextStyle(fontSize: 10)), - backgroundColor: AppColors.verdeAdmin.withOpacity(0.1), - deleteIconColor: AppColors.verdeAdmin, + backgroundColor: AppColors.guindaPrimary.withOpacity(0.1), + deleteIconColor: AppColors.guindaPrimary, onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()), ], const SizedBox(height: 24), @@ -226,7 +226,7 @@ class _CreateRouteScreenState extends State { child: ElevatedButton.icon( onPressed: _loading ? null : _guardar, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), icon: _loading ? const SizedBox(width: 18, height: 18, @@ -242,12 +242,12 @@ class _CreateRouteScreenState extends State { Widget _section(String title) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, - color: AppColors.verdeAdmin, fontSize: 15))); + color: AppColors.guindaPrimary, fontSize: 15))); Widget _field(TextEditingController ctrl, String label, IconData icon) => TextField(controller: ctrl, decoration: InputDecoration(labelText: label, - prefixIcon: Icon(icon, color: AppColors.verdeAdmin), + prefixIcon: Icon(icon, color: AppColors.guindaPrimary), border: const OutlineInputBorder(), filled: true, fillColor: Colors.white)); Widget _timeButton(String label, String value, VoidCallback onTap) => @@ -257,7 +257,7 @@ class _CreateRouteScreenState extends State { borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade400)), child: Row(children: [ - const Icon(Icons.access_time, color: AppColors.verdeAdmin, size: 18), + const Icon(Icons.access_time, color: AppColors.guindaPrimary, size: 18), const SizedBox(width: 8), Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), diff --git a/lib/screens/admin/export_pdf_screen.dart b/lib/screens/admin/export_pdf_screen.dart index 7a73e8d..e3e3c42 100644 --- a/lib/screens/admin/export_pdf_screen.dart +++ b/lib/screens/admin/export_pdf_screen.dart @@ -193,19 +193,19 @@ class _ExportPdfScreenState extends State { Widget build(BuildContext context) => Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: const Text('Exportar Reporte PDF'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado))), body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container(width: 100, height: 100, - decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1), + decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1), shape: BoxShape.circle), - child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.verdeAdmin)), + child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.guindaPrimary)), const SizedBox(height: 24), const Text('Reporte Mensual', style: TextStyle(fontSize: 22, - fontWeight: FontWeight.bold, color: AppColors.verdeAdmin)), + fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)), const SizedBox(height: 8), const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.', textAlign: TextAlign.center, @@ -215,7 +215,7 @@ class _ExportPdfScreenState extends State { child: ElevatedButton.icon( onPressed: _generating ? null : _generatePdf, style: ElevatedButton.styleFrom( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), icon: _generating ? const SizedBox(width: 20, height: 20, @@ -237,7 +237,7 @@ class _ExportPdfScreenState extends State { fontSize: 13))), TextButton(onPressed: _generatePdf, child: const Text('Compartir de nuevo', - style: TextStyle(fontSize: 11, color: AppColors.verdeAdmin))), + style: TextStyle(fontSize: 11, color: AppColors.guindaPrimary))), ])), ], ])))); diff --git a/lib/screens/admin/manage_conductors_screen.dart b/lib/screens/admin/manage_conductors_screen.dart index 6379885..a206460 100644 --- a/lib/screens/admin/manage_conductors_screen.dart +++ b/lib/screens/admin/manage_conductors_screen.dart @@ -52,34 +52,63 @@ class _ManageConductorsScreenState extends State { if (existing != null) SwitchListTile(value: activo, dense: true, title: Text(activo ? 'Conductor Activo' : 'Conductor Inactivo', - style: TextStyle(color: activo ? AppColors.verdeAdmin : AppColors.rojoError, + style: TextStyle(color: activo ? AppColors.guindaPrimary : AppColors.rojoError, fontWeight: FontWeight.bold)), - activeColor: AppColors.verdeAdmin, + activeColor: AppColors.guindaPrimary, onChanged: (v) => setSt(() => activo = v)), ])), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), ElevatedButton( - style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, + style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white), onPressed: () async { - if (nombreCtrl.text.trim().isEmpty || emailCtrl.text.trim().isEmpty) return; - if (existing == null) { - if (passCtrl.text.length < 6) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('La contrasena debe tener al menos 6 caracteres'), - backgroundColor: AppColors.rojoError)); - return; - } - await DbHelper.insertConductor(nombreCtrl.text.trim(), - emailCtrl.text.trim().toLowerCase(), passCtrl.text); - } else { - await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(), - emailCtrl.text.trim().toLowerCase()); - await DbHelper.updateConductorMeta(existing['id'], activo, notasCtrl.text.trim()); + if (nombreCtrl.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Ingresa el nombre del conductor'), + backgroundColor: AppColors.rojoError)); + return; + } + if (emailCtrl.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Ingresa el correo electronico'), + backgroundColor: AppColors.rojoError)); + return; + } + try { + if (existing == null) { + if (passCtrl.text.length < 6) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('La contrasena debe tener al menos 6 caracteres'), + backgroundColor: AppColors.rojoError)); + return; + } + await DbHelper.insertConductor( + nombreCtrl.text.trim(), + emailCtrl.text.trim().toLowerCase(), + passCtrl.text); + if (ctx.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Conductor creado correctamente'), + backgroundColor: AppColors.verdeExito)); + } + } else { + await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(), + emailCtrl.text.trim().toLowerCase()); + await DbHelper.updateConductorMeta( + existing['id'], activo, notasCtrl.text.trim()); + } + if (ctx.mounted) Navigator.pop(ctx); + await _load(); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(e.toString().contains('UNIQUE') + ? 'Ese correo ya está registrado' + : 'Error: ${e.toString()}'), + backgroundColor: AppColors.rojoError)); + } } - if (ctx.mounted) Navigator.pop(ctx); - await _load(); }, child: Text(existing == null ? 'Crear' : 'Guardar')), ]))); @@ -89,7 +118,7 @@ class _ManageConductorsScreenState extends State { Widget build(BuildContext context) => Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Text('Conductores (${_conductores.length})'), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), @@ -110,7 +139,7 @@ class _ManageConductorsScreenState extends State { style: TextStyle(color: AppColors.grisTexto)), const SizedBox(height: 16), ElevatedButton.icon( - style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, + style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white), onPressed: () => _showFormDialog(), icon: const Icon(Icons.add), label: const Text('Agregar primer conductor')), @@ -126,17 +155,17 @@ class _ManageConductorsScreenState extends State { margin: const EdgeInsets.only(bottom: 10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), side: BorderSide(color: activo - ? AppColors.verdeAdmin.withOpacity(0.3) + ? AppColors.guindaPrimary.withOpacity(0.3) : AppColors.rojoError.withOpacity(0.3))), child: Padding(padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ CircleAvatar(radius: 22, backgroundColor: activo - ? AppColors.verdeAdmin.withOpacity(0.15) + ? AppColors.guindaPrimary.withOpacity(0.15) : Colors.grey.shade200, child: Icon(Icons.person, - color: activo ? AppColors.verdeAdmin : AppColors.grisTexto, size: 24)), + color: activo ? AppColors.guindaPrimary : AppColors.grisTexto, size: 24)), const SizedBox(width: 12), Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(c['nombre'] ?? '', style: const TextStyle( @@ -146,12 +175,12 @@ class _ManageConductorsScreenState extends State { ])), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( - color: activo ? AppColors.verdeAdmin.withOpacity(0.1) + color: activo ? AppColors.guindaPrimary.withOpacity(0.1) : AppColors.rojoError.withOpacity(0.1), borderRadius: BorderRadius.circular(10)), child: Text(activo ? 'Activo' : 'Inactivo', style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, - color: activo ? AppColors.verdeAdmin : AppColors.rojoError))), + color: activo ? AppColors.guindaPrimary : AppColors.rojoError))), IconButton(icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () => _showFormDialog(existing: c)), ]), diff --git a/lib/screens/citizen/review_screen.dart b/lib/screens/citizen/review_screen.dart index e65623c..b2d2120 100644 --- a/lib/screens/citizen/review_screen.dart +++ b/lib/screens/citizen/review_screen.dart @@ -40,20 +40,27 @@ class _ReviewScreenState extends State { } setState(() => _loading = true); - await DbHelper.insertReview(ReviewModel( - userId: auth.currentUser!.id!, - colonia: widget.colonia, - routeId: widget.routeId, - estrellas: _estrellas, - comentario: _comentCtrl.text.trim().isEmpty - ? 'Sin comentario' : _comentCtrl.text.trim(), - fecha: DateTime.now().toIso8601String(), - nombreUsuario: auth.currentUser!.nombre, - )); - - context.read().clearReviewPrompt(widget.routeId); - if (!mounted) return; - setState(() { _loading = false; _sent = true; }); + try { + await DbHelper.insertReview(ReviewModel( + userId: auth.currentUser!.id!, + colonia: widget.colonia, + routeId: widget.routeId, + estrellas: _estrellas, + comentario: _comentCtrl.text.trim().isEmpty + ? 'Sin comentario' : _comentCtrl.text.trim(), + fecha: DateTime.now().toIso8601String(), + nombreUsuario: auth.currentUser!.nombre, + )); + context.read().clearReviewPrompt(widget.routeId); + if (!mounted) return; + setState(() { _loading = false; _sent = true; }); + } catch (e) { + if (!mounted) return; + setState(() => _loading = false); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Error al guardar: ${e.toString()}'), + backgroundColor: AppColors.rojoError)); + } } @override diff --git a/lib/screens/driver/driver_home_screen.dart b/lib/screens/driver/driver_home_screen.dart index f173a39..ba01c28 100644 --- a/lib/screens/driver/driver_home_screen.dart +++ b/lib/screens/driver/driver_home_screen.dart @@ -71,14 +71,14 @@ class _DriverHomeScreenState extends State { selectedIndex: _tab, onDestinationSelected: (i) => setState(()=>_tab=i), backgroundColor: Colors.white, - indicatorColor: AppColors.moradoConductor.withOpacity(0.15), + indicatorColor: AppColors.guindaPrimary.withOpacity(0.15), destinations: const [ NavigationDestination(icon:Icon(Icons.dashboard_outlined), - selectedIcon:Icon(Icons.dashboard,color:AppColors.moradoConductor),label:'Mi Ruta'), + selectedIcon:Icon(Icons.dashboard,color:AppColors.guindaPrimary),label:'Mi Ruta'), NavigationDestination(icon:Icon(Icons.map_outlined), - selectedIcon:Icon(Icons.map,color:AppColors.moradoConductor),label:'Mapa'), + selectedIcon:Icon(Icons.map,color:AppColors.guindaPrimary),label:'Mapa'), NavigationDestination(icon:Icon(Icons.report_problem_outlined), - selectedIcon:Icon(Icons.report_problem,color:AppColors.moradoConductor),label:'Incidente'), + selectedIcon:Icon(Icons.report_problem,color:AppColors.guindaPrimary),label:'Incidente'), ], ), ); @@ -114,7 +114,7 @@ class _DriverMainTabState extends State<_DriverMainTab> { ? widget.sim.isGpsActive(widget.todayRouteId!) : true; return CustomScrollView(slivers:[ - SliverAppBar(pinned:true, backgroundColor:AppColors.moradoConductor, foregroundColor:Colors.white, + SliverAppBar(pinned:true, backgroundColor:AppColors.guindaPrimary, foregroundColor:Colors.white, bottom:PreferredSize(preferredSize:const Size.fromHeight(4), child:Container(height:4,color:AppColors.dorado)), title:Text('Conductor: ${widget.auth.currentUser?.nombre.split(' ').first ?? ''}', @@ -125,16 +125,16 @@ class _DriverMainTabState extends State<_DriverMainTab> { SliverPadding(padding:const EdgeInsets.all(14),sliver:SliverList(delegate:SliverChildListDelegate([ // Ruta de hoy - Card(color:AppColors.moradoConductor.withOpacity(0.08), + Card(color:AppColors.guindaPrimary.withOpacity(0.08), shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(12), - side:BorderSide(color:AppColors.moradoConductor.withOpacity(0.3))), + side:BorderSide(color:AppColors.guindaPrimary.withOpacity(0.3))), child:Padding(padding:const EdgeInsets.all(14),child:Column( crossAxisAlignment:CrossAxisAlignment.start, children:[ Row(children:[ - const Icon(Icons.today,color:AppColors.moradoConductor), + const Icon(Icons.today,color:AppColors.guindaPrimary), const SizedBox(width:8), Text('Hoy — ${_todayLabel()}', - style:const TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor,fontSize:15)), + style:const TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary,fontSize:15)), ]), const Divider(), if (widget.route != null)...[ @@ -155,7 +155,7 @@ class _DriverMainTabState extends State<_DriverMainTab> { const SizedBox(height:8), LinearProgressIndicator(value:(posIdx+1)/8, backgroundColor:Colors.grey.shade300, - valueColor:const AlwaysStoppedAnimation(AppColors.moradoConductor)), + valueColor:const AlwaysStoppedAnimation(AppColors.guindaPrimary)), const SizedBox(height:6), Text(widget.sim.getEtaText(widget.todayRouteId??''), style:const TextStyle(fontSize:13,fontWeight:FontWeight.w500)), @@ -168,7 +168,7 @@ class _DriverMainTabState extends State<_DriverMainTab> { Card(child:Padding(padding:const EdgeInsets.all(12),child:Column( crossAxisAlignment:CrossAxisAlignment.start, children:[ const Text('📋 Instrucciones de Ruta', - style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor)), + style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary)), const Divider(), const Text('• Sigue la ruta asignada sin desviaciones\n' '• Mantén el GPS activo en todo momento\n' @@ -208,7 +208,7 @@ class _DriverMainTabState extends State<_DriverMainTab> { Card(child:Padding(padding:const EdgeInsets.all(12),child:Column( crossAxisAlignment:CrossAxisAlignment.start, children:[ const Text('Mi Horario', - style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor)), + style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary)), const Divider(), if (widget.assignments.isEmpty) const Text('Sin asignaciones. Contacta al administrador.', @@ -232,16 +232,16 @@ class _DriverMainTabState extends State<_DriverMainTab> { try { found = all.firstWhere((a)=>a.diaSemana==dia); break; } catch(_){} } return Container(padding:const EdgeInsets.all(10), - decoration:BoxDecoration(color:AppColors.moradoConductor.withOpacity(0.06), + decoration:BoxDecoration(color:AppColors.guindaPrimary.withOpacity(0.06), borderRadius:BorderRadius.circular(8), - border:Border.all(color:AppColors.moradoConductor.withOpacity(0.2))), + border:Border.all(color:AppColors.guindaPrimary.withOpacity(0.2))), child:Row(children:[ - const Icon(Icons.calendar_today,size:14,color:AppColors.moradoConductor), + const Icon(Icons.calendar_today,size:14,color:AppColors.guindaPrimary), const SizedBox(width:6), Expanded(child:Text(label,style:const TextStyle(fontWeight:FontWeight.w600,fontSize:12))), if (found!=null) Container(padding:const EdgeInsets.symmetric(horizontal:8,vertical:3), - decoration:BoxDecoration(color:AppColors.moradoConductor,borderRadius:BorderRadius.circular(8)), + decoration:BoxDecoration(color:AppColors.guindaPrimary,borderRadius:BorderRadius.circular(8)), child:Text('${found.routeId} • ${found.turno}', style:const TextStyle(fontSize:11,color:Colors.white,fontWeight:FontWeight.bold))) else @@ -262,7 +262,7 @@ class _DriverMapTab extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( appBar:AppBar(automaticallyImplyLeading:false, - backgroundColor:AppColors.moradoConductor,foregroundColor:Colors.white, + backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white, title:Text(route.name,style:const TextStyle(fontSize:13)), bottom:PreferredSize(preferredSize:const Size.fromHeight(4), child:Container(height:4,color:AppColors.dorado))), @@ -333,7 +333,7 @@ class _DriverReportesTabState extends State<_DriverReportesTab> { Widget build(BuildContext context) => Scaffold( backgroundColor:AppColors.grisFondo, appBar:AppBar(automaticallyImplyLeading:false, - backgroundColor:AppColors.moradoConductor,foregroundColor:Colors.white, + backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white, title:const Text('Reportar Incidente'), bottom:PreferredSize(preferredSize:const Size.fromHeight(4), child:Container(height:4,color:AppColors.dorado))), @@ -349,14 +349,14 @@ class _DriverReportesTabState extends State<_DriverReportesTab> { if (widget.todayRouteId != null) Container(margin:const EdgeInsets.only(bottom:12), padding:const EdgeInsets.all(10), - decoration:BoxDecoration(color:AppColors.moradoConductor.withOpacity(0.08), + decoration:BoxDecoration(color:AppColors.guindaPrimary.withOpacity(0.08), borderRadius:BorderRadius.circular(8), - border:Border.all(color:AppColors.moradoConductor.withOpacity(0.3))), + border:Border.all(color:AppColors.guindaPrimary.withOpacity(0.3))), child:Row(children:[ - const Icon(Icons.route,color:AppColors.moradoConductor,size:16), + const Icon(Icons.route,color:AppColors.guindaPrimary,size:16), const SizedBox(width:6), Text('Incidente en: ${widget.todayRouteId}', - style:const TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor,fontSize:13)), + style:const TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary,fontSize:13)), ])) else Container(margin:const EdgeInsets.only(bottom:12), @@ -369,12 +369,12 @@ class _DriverReportesTabState extends State<_DriverReportesTab> { child:Padding(padding:const EdgeInsets.all(16),child:Column( crossAxisAlignment:CrossAxisAlignment.start, children:[ const Text('Tipo de incidente', - style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor,fontSize:15)), + style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary,fontSize:15)), const SizedBox(height:8), ..._tipos.entries.map((e)=>RadioListTile(dense:true, value:e.key,groupValue:_tipo, title:Text(e.value,style:const TextStyle(fontSize:13)), - activeColor:AppColors.moradoConductor, + activeColor:AppColors.guindaPrimary, onChanged:(v)=>setState(()=>_tipo=v!))), const SizedBox(height:10), const Text('Descripción',style:TextStyle(fontWeight:FontWeight.w600,fontSize:13)), @@ -395,7 +395,7 @@ class _DriverReportesTabState extends State<_DriverReportesTab> { SizedBox(width:double.infinity,height:48, child:ElevatedButton.icon( onPressed:(_loading||widget.todayRouteId==null)?null:_enviar, - style:ElevatedButton.styleFrom(backgroundColor:AppColors.moradoConductor, + style:ElevatedButton.styleFrom(backgroundColor:AppColors.guindaPrimary, foregroundColor:Colors.white, shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(8))), icon:_loading?const SizedBox(width:18,height:18, @@ -408,11 +408,11 @@ class _DriverReportesTabState extends State<_DriverReportesTab> { const SizedBox(height:16), const Align(alignment:Alignment.centerLeft, child:Text('Mis incidentes de hoy', - style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor,fontSize:14))), + style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary,fontSize:14))), const SizedBox(height:8), ..._misIncidentes.take(5).map((a)=>Card(margin:const EdgeInsets.only(bottom:6), child:ListTile(dense:true, - leading:CircleAvatar(backgroundColor:AppColors.moradoConductor,radius:16, + leading:CircleAvatar(backgroundColor:AppColors.guindaPrimary,radius:16, child:const Icon(Icons.warning,color:Colors.white,size:14)), title:Text(_tipos[a.tipo]??a.tipo, style:const TextStyle(fontSize:12,fontWeight:FontWeight.w600)), @@ -436,7 +436,7 @@ class _NotifBanner extends StatelessWidget { final color = notif.event==NotifEvent.gpsLost?Colors.red.shade800 :notif.event==NotifEvent.truckStopped?AppColors.naranjaAlerta :notif.event==NotifEvent.routeCancelled?AppColors.rojoError - :AppColors.moradoConductor; + :AppColors.guindaPrimary; return Material(color:Colors.transparent, child:Container(margin:const EdgeInsets.all(10), decoration:BoxDecoration(color:color,borderRadius:BorderRadius.circular(12), diff --git a/lib/screens/shared/reporte_chat_screen.dart b/lib/screens/shared/reporte_chat_screen.dart index ca5c1e7..5bac66d 100644 --- a/lib/screens/shared/reporte_chat_screen.dart +++ b/lib/screens/shared/reporte_chat_screen.dart @@ -20,155 +20,234 @@ class _ReporteChatScreenState extends State { List> _msgs = []; bool _loading = true; Timer? _timer; + String? _myRol; + int? _myUserId; - @override void initState() { super.initState(); _load(); - _timer = Timer.periodic(const Duration(seconds: 5), (_) => _load()); } + @override + void initState() { + super.initState(); + final auth = context.read(); + _myRol = auth.currentUser?.rol ?? 'CIUDADANO'; + _myUserId = auth.currentUser?.id; + _load(); + _timer = Timer.periodic(const Duration(seconds: 4), (_) => _load()); + } Future _load() async { - final auth = context.read(); - final rol = auth.currentUser?.rol ?? 'CIUDADANO'; + // Cargar TODOS los mensajes del reporte sin filtro de rol final msgs = await DbHelper.getChatMsgs(widget.reporteId); - await DbHelper.markChatRead(widget.reporteId, rol); + // Marcar como leídos los que NO son míos + if (_myRol != null) { + try { + await DbHelper.markChatRead(widget.reporteId, _myRol!); + } catch (_) {} + } if (mounted) { setState(() { _msgs = msgs; _loading = false; }); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (_scroll.hasClients) _scroll.animateTo(_scroll.position.maxScrollExtent, - duration: const Duration(milliseconds: 200), curve: Curves.easeOut); - }); + _scrollToBottom(); } } + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scroll.hasClients) { + _scroll.animateTo(_scroll.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), curve: Curves.easeOut); + } + }); + } + Future _send() async { final text = _ctrl.text.trim(); - if (text.isEmpty) return; - final auth = context.read(); - final user = auth.currentUser; - if (user == null) return; + if (text.isEmpty || _myRol == null || _myUserId == null) return; _ctrl.clear(); - await DbHelper.insertChatMsg(widget.reporteId, user.id!, user.rol, text); + await DbHelper.insertChatMsg(widget.reporteId, _myUserId!, _myRol!, text); await _load(); } + bool _isMe(Map msg) { + // Un mensaje es mío si fue enviado con el mismo rol (admin ve sus msgs, ciudadano ve los suyos) + final msgRol = msg['rol'] as String? ?? ''; + final msgUserId = msg['user_id'] as int?; + // Comparar por user_id si disponible, sino por rol + if (_myUserId != null && msgUserId != null) return msgUserId == _myUserId; + return msgRol == _myRol; + } + @override Widget build(BuildContext context) { - final auth = context.watch(); - final myRol = auth.currentUser?.rol ?? 'CIUDADANO'; - final isAdmin = myRol == 'ADMINISTRADOR'; - final accent = isAdmin ? AppColors.verdeAdmin : AppColors.guindaPrimary; + final isAdmin = _myRol == 'ADMINISTRADOR'; return Scaffold( backgroundColor: AppColors.grisFondo, appBar: AppBar( - backgroundColor: accent, foregroundColor: Colors.white, + backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white, title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('Chat del Reporte', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), - Text('Folio: ${widget.folio}', style: const TextStyle(fontSize: 11, color: Colors.white70)), + const Text('Chat del Reporte', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), + Text('Folio: ${widget.folio}', + style: const TextStyle(fontSize: 11, color: Colors.white70)), ]), bottom: PreferredSize(preferredSize: const Size.fromHeight(4), child: Container(height: 4, color: AppColors.dorado)), - actions: [if (widget.isClosed) Container(margin: const EdgeInsets.only(right: 12), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration(color: AppColors.verdeExito, borderRadius: BorderRadius.circular(12)), - child: const Text('COMPLETADO', style: TextStyle(fontSize: 10, - color: Colors.white, fontWeight: FontWeight.bold)))], + actions: [ + if (widget.isClosed) + Container(margin: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration(color: AppColors.verdeExito, + borderRadius: BorderRadius.circular(12)), + child: const Text('COMPLETADO', + style: TextStyle(fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold))), + IconButton(icon: const Icon(Icons.refresh), onPressed: _load), + ], ), body: Column(children: [ - if (widget.isClosed) Container( - width: double.infinity, padding: const EdgeInsets.all(10), - color: AppColors.verdeExito.withOpacity(0.08), - child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.lock, size: 14, color: AppColors.verdeExito), - SizedBox(width: 6), - Text('Reporte completado. Chat cerrado.', - style: TextStyle(fontSize: 12, color: AppColors.verdeExito, fontWeight: FontWeight.w600)), - ])), + if (widget.isClosed) + Container(width: double.infinity, padding: const EdgeInsets.all(10), + color: AppColors.verdeExito.withOpacity(0.08), + child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Icon(Icons.lock, size: 14, color: AppColors.verdeExito), + SizedBox(width: 6), + Text('Reporte completado — Chat cerrado', + style: TextStyle(fontSize: 12, color: AppColors.verdeExito, + fontWeight: FontWeight.w600)), + ])), + + // Mensajes Expanded(child: _loading - ? const Center(child: CircularProgressIndicator()) + ? const Center(child: CircularProgressIndicator( + color: AppColors.guindaPrimary)) : _msgs.isEmpty ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.chat_bubble_outline, size: 48, color: Colors.grey.shade400), + const Icon(Icons.chat_bubble_outline, size: 48, color: AppColors.grisTexto), const SizedBox(height: 12), - Text(isAdmin ? 'Inicia la conversacion con el ciudadano' - : 'Escribe tu mensaje al Ayuntamiento', - style: TextStyle(color: Colors.grey.shade500)), + Text(isAdmin + ? 'Sin mensajes aún. Inicia la conversación.' + : 'Escribe tu mensaje al Ayuntamiento de Celaya.', + style: const TextStyle(color: AppColors.grisTexto), + textAlign: TextAlign.center), ])) : ListView.builder( - controller: _scroll, padding: const EdgeInsets.all(12), + controller: _scroll, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 4), itemCount: _msgs.length, itemBuilder: (_, i) { - final m = _msgs[i]; - final rol = m['rol'] as String; - final isMe = rol == myRol; + final m = _msgs[i]; + final me = _isMe(m); + final rol = m['rol'] as String? ?? ''; final fecha = DateTime.tryParse(m['fecha'] as String? ?? ''); final hora = fecha != null - ? '${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}' : ''; + ? '${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}' + : ''; + return Padding( - padding: const EdgeInsets.symmetric(vertical: 3), + padding: const EdgeInsets.symmetric(vertical: 4), child: Row( - mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, + mainAxisAlignment: me ? MainAxisAlignment.end : MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (!isMe) ...[ - CircleAvatar(radius: 14, - backgroundColor: (rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary).withOpacity(0.15), - child: Icon(rol=='ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person, - size: 14, color: rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary)), + if (!me) ...[ + CircleAvatar(radius: 16, + backgroundColor: AppColors.guindaPrimary.withOpacity(0.15), + child: Icon( + rol == 'ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person, + size: 15, color: AppColors.guindaPrimary)), const SizedBox(width: 6), ], Flexible(child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.72), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: isMe ? accent : Colors.white, + color: me ? AppColors.guindaPrimary : Colors.white, borderRadius: BorderRadius.only( - topLeft: const Radius.circular(16), topRight: const Radius.circular(16), - bottomLeft: Radius.circular(isMe ? 16 : 4), - bottomRight: Radius.circular(isMe ? 4 : 16), + topLeft: const Radius.circular(18), + topRight: const Radius.circular(18), + bottomLeft: Radius.circular(me ? 18 : 4), + bottomRight: Radius.circular(me ? 4 : 18), ), - boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.07), blurRadius: 4)], + boxShadow: [BoxShadow( + color: Colors.black.withOpacity(0.07), + blurRadius: 4, offset: const Offset(0, 2))], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (!me) ...[ + Text( + rol == 'ADMINISTRADOR' ? 'Ayuntamiento de Celaya' : 'Ciudadano', + style: const TextStyle(fontSize: 10, + fontWeight: FontWeight.bold, + color: AppColors.guindaPrimary)), + const SizedBox(height: 2), + ], + Text(m['mensaje'] as String? ?? '', + style: TextStyle(fontSize: 13, height: 1.4, + color: me ? Colors.white : AppColors.negroTexto)), + const SizedBox(height: 2), + Align(alignment: Alignment.bottomRight, + child: Text(hora, style: TextStyle(fontSize: 9, + color: me ? Colors.white54 : AppColors.grisTexto))), + ], ), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (!isMe) Text(rol=='ADMINISTRADOR' ? 'Ayuntamiento' : 'Ciudadano', - style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, - color: rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary)), - Text(m['mensaje'] as String? ?? '', - style: TextStyle(fontSize: 13, height: 1.4, - color: isMe ? Colors.white : AppColors.negroTexto)), - Text(hora, style: TextStyle(fontSize: 9, - color: isMe ? Colors.white60 : AppColors.grisTexto)), - ]), )), - if (isMe) const SizedBox(width: 6), + if (me) const SizedBox(width: 6), ], ), ); })), - if (!widget.isClosed) Container( - padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), - decoration: BoxDecoration(color: Colors.white, - boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0,-2))]), - child: SafeArea(top: false, child: Row(children: [ - Expanded(child: TextField( - controller: _ctrl, maxLines: 3, minLines: 1, - textCapitalization: TextCapitalization.sentences, - decoration: InputDecoration( - hintText: isAdmin ? 'Responde al ciudadano...' : 'Escribe al Ayuntamiento...', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), - filled: true, fillColor: AppColors.grisFondo, - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), isDense: true), - )), - const SizedBox(width: 8), - CircleAvatar(radius: 22, backgroundColor: accent, - child: IconButton(icon: const Icon(Icons.send, color: Colors.white, size: 18), onPressed: _send)), - ]))) - else Container(padding: const EdgeInsets.all(14), color: Colors.white, + + // Input + if (!widget.isClosed) + Container( + padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), + blurRadius: 6, offset: const Offset(0, -2))]), + child: SafeArea(top: false, child: Row(children: [ + Expanded(child: TextField( + controller: _ctrl, + maxLines: 4, minLines: 1, + textCapitalization: TextCapitalization.sentences, + decoration: InputDecoration( + hintText: isAdmin + ? 'Responde al ciudadano...' + : 'Escribe al Ayuntamiento de Celaya...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(24), + borderSide: BorderSide.none), + filled: true, fillColor: AppColors.grisFondo, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + isDense: true, + ), + onSubmitted: (_) => _send(), + )), + const SizedBox(width: 8), + CircleAvatar( + radius: 24, + backgroundColor: AppColors.guindaPrimary, + child: IconButton( + icon: const Icon(Icons.send, color: Colors.white, size: 20), + onPressed: _send)), + ]))) + else + Container( + padding: const EdgeInsets.all(14), color: Colors.white, child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.lock_outline, size: 16, color: AppColors.grisTexto), SizedBox(width: 6), - Text('Chat cerrado', style: TextStyle(color: AppColors.grisTexto, fontSize: 12)), + Text('Chat cerrado — Reporte completado', + style: TextStyle(color: AppColors.grisTexto, fontSize: 12)), ])), ]), ); } - @override void dispose() { _ctrl.dispose(); _scroll.dispose(); _timer?.cancel(); super.dispose(); } + @override + void dispose() { + _ctrl.dispose(); _scroll.dispose(); _timer?.cancel(); + super.dispose(); + } } diff --git a/lib/widgets/route_map_widget.dart b/lib/widgets/route_map_widget.dart index b8fbd87..aa1a85a 100644 --- a/lib/widgets/route_map_widget.dart +++ b/lib/widgets/route_map_widget.dart @@ -208,8 +208,8 @@ class DriverRouteMap extends StatelessWidget { Polyline(points:pending, color:Colors.grey.shade400, strokeWidth:5, borderColor:Colors.white54, borderStrokeWidth:1), if (done.isNotEmpty) - Polyline(points:done, color:AppColors.moradoConductor, strokeWidth:6, - borderColor:AppColors.moradoConductor.withOpacity(0.4), borderStrokeWidth:2), + Polyline(points:done, color:AppColors.guindaPrimary, strokeWidth:6, + borderColor:AppColors.guindaPrimary.withOpacity(0.4), borderStrokeWidth:2), ]), MarkerLayer(markers:[ // Waypoints pendientes @@ -223,7 +223,7 @@ class DriverRouteMap extends StatelessWidget { Marker(point:cur, width:56, height:56, child:Transform.rotate(angle:bear*math.pi/180, child:Container(decoration:BoxDecoration( - color:gps?AppColors.moradoConductor:Colors.grey, shape:BoxShape.circle, + color:gps?AppColors.guindaPrimary:Colors.grey, shape:BoxShape.circle, border:Border.all(color:Colors.white,width:2.5), boxShadow:[BoxShadow(color:Colors.black38,blurRadius:8)]), child:Icon(gps?Icons.navigation:Icons.gps_off,color:Colors.white,size:28)))),