Actualizacion de mejoras

This commit is contained in:
2026-05-23 08:36:15 -06:00
parent ebce0badde
commit 8fe3665ffb
11 changed files with 738 additions and 291 deletions

View File

@@ -12,7 +12,7 @@ class DbHelper {
static Future<Database> _initDb() async { static Future<Database> _initDb() async {
final path = join(await getDatabasesPath(), 'celaya_v3.db'); final path = join(await getDatabasesPath(), 'celaya_v3.db');
return openDatabase(path, version: 2, return openDatabase(path, version: 3,
onCreate: _onCreate, onUpgrade: _onUpgrade); onCreate: _onCreate, onUpgrade: _onUpgrade);
} }
@@ -108,19 +108,27 @@ class DbHelper {
// Migración incremental — se ejecuta al actualizar la app // Migración incremental — se ejecuta al actualizar la app
static Future<void> _onUpgrade(Database db, int oldV, int newV) async { static Future<void> _onUpgrade(Database db, int oldV, int newV) async {
// Agregar columnas/tablas que pueden faltar en instalaciones anteriores // Lista de migraciones seguras (todas usan IF NOT EXISTS o ignoran errores)
final helpers = [ final sqls = [
// foto_path en reportes // Columnas que pueden faltar
"ALTER TABLE reportes ADD COLUMN foto_path TEXT", "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( '''CREATE TABLE IF NOT EXISTS notification_history(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER, route_id TEXT NOT NULL, user_id INTEGER, route_id TEXT NOT NULL,
event_type TEXT NOT NULL, title TEXT NOT NULL, event_type TEXT NOT NULL, title TEXT NOT NULL,
body TEXT NOT NULL, fecha TEXT NOT NULL, body TEXT NOT NULL, fecha TEXT NOT NULL, leida INTEGER DEFAULT 0)''',
leida INTEGER DEFAULT 0)''', // Tablas de gestión de reportes
'''CREATE TABLE IF NOT EXISTS user_meta(
user_id INTEGER PRIMARY KEY, activo INTEGER DEFAULT 1, notas TEXT)''',
'''CREATE TABLE IF NOT EXISTS reporte_notas( '''CREATE TABLE IF NOT EXISTS reporte_notas(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
reporte_id INTEGER NOT NULL, admin_id INTEGER NOT NULL, 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, reporte_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
rol TEXT NOT NULL, mensaje TEXT NOT NULL, rol TEXT NOT NULL, mensaje TEXT NOT NULL,
fecha TEXT NOT NULL, leido INTEGER DEFAULT 0)''', fecha TEXT NOT NULL, leido INTEGER DEFAULT 0)''',
// Tabla route_definitions
'''CREATE TABLE IF NOT EXISTS route_definitions( '''CREATE TABLE IF NOT EXISTS route_definitions(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id TEXT UNIQUE NOT NULL, nombre TEXT NOT NULL, 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, hora_fin TEXT NOT NULL, turno TEXT NOT NULL,
colonias TEXT NOT NULL, activa INTEGER DEFAULT 1)''', colonias TEXT NOT NULL, activa INTEGER DEFAULT 1)''',
]; ];
for (final sql in helpers) { for (final sql in sqls) {
try { await db.execute(sql); } catch (_) {} try { await db.execute(sql); } catch (_) {}
// Ignorar errores (ej. columna ya existe)
} }
} }

View File

@@ -49,22 +49,22 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
selectedIndex:_tab, selectedIndex:_tab,
onDestinationSelected:(i)=>setState(()=>_tab=i), onDestinationSelected:(i)=>setState(()=>_tab=i),
backgroundColor:Colors.white, backgroundColor:Colors.white,
indicatorColor:AppColors.verdeAdmin.withOpacity(0.15), indicatorColor:AppColors.guindaPrimary.withOpacity(0.15),
destinations:const[ destinations:const[
NavigationDestination(icon:Icon(Icons.dashboard_outlined), 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), 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), 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), 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), 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), 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), 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return CustomScrollView(slivers: [ 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), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
title: const Text('Panel Administrador', style: TextStyle(fontWeight: FontWeight.bold)), 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([ SliverPadding(padding: const EdgeInsets.all(12), sliver: SliverList(delegate: SliverChildListDelegate([
Row(children: [ Row(children: [
_Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.verdeAdmin), _Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.guindaPrimary),
const SizedBox(width: 10), const SizedBox(width: 10),
_Stat('Incidentes', '${_conductorIncidentes.where((i)=>!i.resuelta).length}', _Stat('Incidentes', '${_conductorIncidentes.where((i)=>!i.resuelta).length}',
Icons.warning, AppColors.naranjaAlerta), Icons.warning, AppColors.naranjaAlerta),
]), ]),
const SizedBox(height: 14), const SizedBox(height: 14),
const Text('Control de Rutas', style: TextStyle(fontWeight: FontWeight.bold, const Text('Control de Rutas', style: TextStyle(fontWeight: FontWeight.bold,
fontSize: 16, color: AppColors.verdeAdmin)), fontSize: 16, color: AppColors.guindaPrimary)),
const SizedBox(height: 8), const SizedBox(height: 8),
...routesData.map((r) { ...routesData.map((r) {
final status = _getStatus(r.routeId); final status = _getStatus(r.routeId);
@@ -249,18 +249,18 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
Padding( Padding(
padding: const EdgeInsets.fromLTRB(14, 6, 14, 2), padding: const EdgeInsets.fromLTRB(14, 6, 14, 2),
child: Row(children: [ 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 SizedBox(width: 4),
const Text('Incidentes del conductor:', const Text('Incidentes del conductor:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11,
color: AppColors.moradoConductor)), color: AppColors.guindaPrimary)),
]), ]),
), ),
...incidentes.map((inc) => Padding( ...incidentes.map((inc) => Padding(
padding: const EdgeInsets.fromLTRB(14, 2, 14, 2), padding: const EdgeInsets.fromLTRB(14, 2, 14, 2),
child: Row(children: [ child: Row(children: [
Container(width: 6, height: 6, Container(width: 6, height: 6,
decoration: const BoxDecoration(color: AppColors.moradoConductor, decoration: const BoxDecoration(color: AppColors.guindaPrimary,
shape: BoxShape.circle)), shape: BoxShape.circle)),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded(child: Text(inc.mensaje, Expanded(child: Text(inc.mensaje,
@@ -286,7 +286,7 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
} }
}, },
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: AppColors.verdeAdmin, foregroundColor: AppColors.guindaPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8)), padding: const EdgeInsets.symmetric(horizontal: 8)),
child: const Text('Actuar', style: TextStyle(fontSize: 10)), child: const Text('Actuar', style: TextStyle(fontSize: 10)),
), ),
@@ -396,7 +396,7 @@ class _AdminMapTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar:AppBar(automaticallyImplyLeading:false, appBar:AppBar(automaticallyImplyLeading:false,
backgroundColor:AppColors.verdeAdmin,foregroundColor:Colors.white, backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white,
title:const Text('Mapa — Todas las Rutas'), title:const Text('Mapa — Todas las Rutas'),
bottom:PreferredSize(preferredSize:const Size.fromHeight(4), bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
child:Container(height:4,color:AppColors.dorado))), child:Container(height:4,color:AppColors.dorado))),
@@ -444,7 +444,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Text('Reportes Ciudadanos (${_filtered.length})'), title: Text('Reportes Ciudadanos (${_filtered.length})'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -464,7 +464,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
style: TextStyle(fontSize: 11, style: TextStyle(fontSize: 11,
color: sel ? Colors.white : AppColors.negroTexto)), color: sel ? Colors.white : AppColors.negroTexto)),
selected: sel, selected: sel,
selectedColor: e == 'TODOS' ? AppColors.verdeAdmin : _estadoColor(e), selectedColor: e == 'TODOS' ? AppColors.guindaPrimary : _estadoColor(e),
checkmarkColor: Colors.white, checkmarkColor: Colors.white,
onSelected: (_) => setState(() => _filtroEstado = e))); onSelected: (_) => setState(() => _filtroEstado = e)));
})), })),
@@ -500,7 +500,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
crossAxisAlignment: CrossAxisAlignment.start, children: [ crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Text(folio, style: const TextStyle(fontWeight: FontWeight.bold, Text(folio, style: const TextStyle(fontWeight: FontWeight.bold,
fontSize: 13, color: AppColors.verdeAdmin)), fontSize: 13, color: AppColors.guindaPrimary)),
const SizedBox(width: 8), const SizedBox(width: 8),
Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -533,7 +533,7 @@ class _AdminConductoresTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: const Text('Gestión de Conductores'), title: const Text('Gestión de Conductores'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -565,7 +565,7 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Text('Rutas del Sistema (${_routes.length})'), title: Text('Rutas del Sistema (${_routes.length})'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), 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 Text('No hay rutas creadas', style: TextStyle(color: AppColors.grisTexto)),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
foregroundColor: Colors.white), foregroundColor: Colors.white),
onPressed: () async { onPressed: () async {
final ok = await Navigator.push(context, MaterialPageRoute( final ok = await Navigator.push(context, MaterialPageRoute(
@@ -607,13 +607,13 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
: r.turno == 'VESPERTINO' ? '🌅' : '🌙'; : r.turno == 'VESPERTINO' ? '🌅' : '🌙';
return Card(margin: const EdgeInsets.only(bottom: 10), return Card(margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(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( child: Padding(padding: const EdgeInsets.all(14), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [ crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Expanded(child: Text('${r.routeId}${r.nombre}', Expanded(child: Text('${r.routeId}${r.nombre}',
style: const TextStyle(fontWeight: FontWeight.bold, 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), IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
onPressed: () async { onPressed: () async {
final ok = await Navigator.push(context, MaterialPageRoute( final ok = await Navigator.push(context, MaterialPageRoute(
@@ -636,10 +636,10 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
const SizedBox(height: 4), const SizedBox(height: 4),
Wrap(spacing: 4, runSpacing: 4, children: r.colonias.take(8).map((c) => Wrap(spacing: 4, runSpacing: 4, children: r.colonias.take(8).map((c) =>
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), 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)), borderRadius: BorderRadius.circular(8)),
child: Text(c, style: const TextStyle(fontSize: 10, child: Text(c, style: const TextStyle(fontSize: 10,
color: AppColors.verdeAdmin)))).toList()), color: AppColors.guindaPrimary)))).toList()),
if (r.colonias.length > 8) if (r.colonias.length > 8)
Text(' ...y ${r.colonias.length - 8} más', Text(' ...y ${r.colonias.length - 8} más',
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
@@ -670,7 +670,7 @@ class _AdminReviewsTabState extends State<_AdminReviewsTab> {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, 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'), title: Text(_showSummary ? 'Calificaciones por Colonia' : 'Reseñas Ciudadanas'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -794,11 +794,19 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
static const _grupoA = ['LUNES', 'MIERCOLES', 'VIERNES']; static const _grupoA = ['LUNES', 'MIERCOLES', 'VIERNES'];
static const _grupoB = ['MARTES', 'JUEVES', 'SABADO']; static const _grupoB = ['MARTES', 'JUEVES', 'SABADO'];
List<String> _todasLasRutas = [];
@override void initState() { super.initState(); _load(); } @override void initState() { super.initState(); _load(); }
Future<void> _load() async { Future<void> _load() async {
final c = await DbHelper.getUsersByRol('CONDUCTOR'); 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<void> _loadAsigs(int id) async { Future<void> _loadAsigs(int id) async {
@@ -821,14 +829,227 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
await _loadAsigs(_sel!.id!); await _loadAsigs(_sel!.id!);
} }
Future<void> _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<void> _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 @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: const Text('Asignar Rutas a Conductores'), title: const Text('Conductores y Asignaciones'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), 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: [ 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), Container(padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200)), border: Border.all(color: Colors.blue.shade200)),
@@ -837,29 +1058,21 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
'Grupo A — Lunes, Miercoles y Viernes\n' 'Grupo A — Lunes, Miercoles y Viernes\n'
'Grupo B — Martes, Jueves y Sabado', 'Grupo B — Martes, Jueves y Sabado',
style: TextStyle(fontSize: 12, color: AppColors.azulInfo))), style: TextStyle(fontSize: 12, color: AppColors.azulInfo))),
DropdownButtonFormField<UserModel>(
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) ...[ if (_sel != null) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
_GrupoRow(label: 'Grupo A — Lunes, Miercoles y Viernes', _GrupoRow(label: 'Grupo A — Lunes, Miercoles y Viernes',
icon: Icons.wb_sunny_outlined, color: Colors.blue, 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)), onSave: (rid, turno) => _saveGrupo(_grupoA, rid, turno)),
const SizedBox(height: 12), const SizedBox(height: 12),
_GrupoRow(label: 'Grupo B — Martes, Jueves y Sabado', _GrupoRow(label: 'Grupo B — Martes, Jueves y Sabado',
icon: Icons.wb_twilight, color: Colors.deepPurple, 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)), onSave: (rid, turno) => _saveGrupo(_grupoB, rid, turno)),
if (_asigs.isNotEmpty) ...[ if (_asigs.isNotEmpty) ...[
const SizedBox(height: 20), const SizedBox(height: 20),
const Text('Resumen actual', style: TextStyle(fontWeight: FontWeight.bold, const Text('Resumen actual', style: TextStyle(fontWeight: FontWeight.bold,
color: AppColors.verdeAdmin, fontSize: 14)), color: AppColors.guindaPrimary, fontSize: 14)),
const SizedBox(height: 8), const SizedBox(height: 8),
Card(child: Padding(padding: const EdgeInsets.all(12), child: Column(children: [ Card(child: Padding(padding: const EdgeInsets.all(12), child: Column(children: [
..._asigs.map((a) => Padding(padding: const EdgeInsets.symmetric(vertical: 3), ..._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), SizedBox(width: 100, child: Text(AppDias.label(a.diaSemana),
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12))), style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12))),
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), 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)), borderRadius: BorderRadius.circular(10)),
child: Text('${a.routeId}${a.turno}', 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> { class _AdminAlertasTabState extends State<_AdminAlertasTab> {
List<AlertaModel> _alertas = []; List<AlertaModel> _alertas = [];
bool _loading = true; bool _loading = true;
String _filtro = 'TODAS';
@override void initState() { super.initState(); _load(); } @override void initState() { super.initState(); _load(); }
@@ -955,62 +1169,172 @@ class _AdminAlertasTabState extends State<_AdminAlertasTab> {
if (mounted) setState(() { _alertas = a; _loading = false; }); if (mounted) setState(() { _alertas = a; _loading = false; });
} }
Color _alertaColor(String tipo) { List<AlertaModel> 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.startsWith('INCIDENTE')) return AppColors.naranjaAlerta;
if (tipo == 'GPS_PERDIDO') return AppColors.rojoError; if (tipo == 'GPS_PERDIDO') return AppColors.rojoError;
if (tipo == 'CAMION_DETENIDO') return AppColors.naranjaAlerta; if (tipo == 'CAMION_DETENIDO') return AppColors.naranjaAlerta;
if (tipo.startsWith('RUTA_')) return AppColors.rojoError; if (tipo.startsWith('RUTA_')) return AppColors.guindaPrimary;
return AppColors.azulInfo; 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<void> _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 @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false, appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Text('Alertas del Sistema (${_alertas.length})'), title: Text('Alertas del Sistema (${_filtered.length})'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), 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.refresh), onPressed: _load)]), actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _load)]),
body: _loading body: Column(children: [
? const Center(child: CircularProgressIndicator()) // Filtros
: _alertas.isEmpty Container(color: Colors.white,
? const Center(child: Text('Sin alertas', padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
style: TextStyle(color: AppColors.grisTexto))) child: Row(children: ['TODAS','ACTIVAS','RESUELTAS'].map((f) {
: ListView.builder( final sel = _filtro == f;
padding: const EdgeInsets.all(12), return Padding(padding: const EdgeInsets.only(right: 8),
itemCount: _alertas.length, child: FilterChip(
itemBuilder: (_, i) { label: Text(f, style: TextStyle(fontSize: 11,
final a = _alertas[i]; color: sel ? Colors.white : AppColors.negroTexto)),
final c = _alertaColor(a.tipo); selected: sel,
return Card(margin: const EdgeInsets.only(bottom: 6), selectedColor: AppColors.guindaPrimary,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8), checkmarkColor: Colors.white,
side: BorderSide(color: c.withOpacity(0.3))), onSelected: (_) => setState(() => _filtro = f)));
child: ListTile(dense: true, }).toList())),
leading: CircleAvatar(radius: 18, backgroundColor: c.withOpacity(0.12), // Lista
child: Icon(a.resuelta ? Icons.check : Icons.warning, Expanded(child: _loading
color: a.resuelta ? AppColors.verdeExito : c, size: 16)), ? const Center(child: CircularProgressIndicator(color: AppColors.guindaPrimary))
title: Text(a.tipo.replaceAll('_', ' '), : _filtered.isEmpty
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold, ? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
color: a.resuelta ? AppColors.grisTexto : c)), const Icon(Icons.notifications_none, color: AppColors.grisTexto, size: 48),
subtitle: Text(a.mensaje, maxLines: 2, const SizedBox(height: 12),
overflow: TextOverflow.ellipsis, Text(_filtro == 'ACTIVAS' ? 'Sin alertas activas' : 'Sin alertas',
style: const TextStyle(fontSize: 11)), style: const TextStyle(color: AppColors.grisTexto)),
trailing: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ ]))
Text(a.routeId, style: const TextStyle( : ListView.builder(
fontSize: 10, color: AppColors.grisTexto)), padding: const EdgeInsets.all(10),
if (!a.resuelta) TextButton( itemCount: _filtered.length,
onPressed: () async { itemBuilder: (_, i) {
await DbHelper.resolverAlerta(a.id!); _load(); final a = _filtered[i];
}, final c = _color(a.tipo);
style: TextButton.styleFrom( final fecha = DateTime.tryParse(a.fecha);
padding: EdgeInsets.zero, minimumSize: Size.zero, final fechaStr = fecha != null
foregroundColor: AppColors.verdeExito), ? '${fecha.day}/${fecha.month} ${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}'
child: const Text('Resolver', style: TextStyle(fontSize: 10))), : '';
]))); 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 ──────────────────────────────────────────────────── // ── Widgets auxiliares ────────────────────────────────────────────────────
class _Stat extends StatelessWidget { class _Stat extends StatelessWidget {
final String label, value; final IconData icon; final Color color; final String label, value; final IconData icon; final Color color;
@@ -1034,7 +1358,7 @@ class _AdminBanner extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Material(color: Colors.transparent, Widget build(BuildContext context) => Material(color: Colors.transparent,
child: Container(margin: const EdgeInsets.all(10), 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)]), boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6)]),
child: Padding(padding: const EdgeInsets.all(10), child: Row(children: [ child: Padding(padding: const EdgeInsets.all(10), child: Row(children: [
const Icon(Icons.notifications, color: Colors.white, size: 20), const Icon(Icons.notifications, color: Colors.white, size: 20),

View File

@@ -110,7 +110,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
return Scaffold( return Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('Reporte $folio', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), Text('Reporte $folio', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
Text(_r['colonia'] as String? ?? '', style: const TextStyle(fontSize: 11, color: Colors.white70)), Text(_r['colonia'] as String? ?? '', style: const TextStyle(fontSize: 11, color: Colors.white70)),
@@ -179,7 +179,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
// Cambiar estado // Cambiar estado
if (!isClosed) ...[ if (!isClosed) ...[
const Text('Cambiar Estado', style: TextStyle(fontWeight: FontWeight.bold, const Text('Cambiar Estado', style: TextStyle(fontWeight: FontWeight.bold,
color: AppColors.verdeAdmin, fontSize: 14)), color: AppColors.guindaPrimary, fontSize: 14)),
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap(spacing: 8, runSpacing: 8, children: _estados.map((e) { Wrap(spacing: 8, runSpacing: 8, children: _estados.map((e) {
final isActual = e == estado; final isActual = e == estado;
@@ -199,7 +199,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
Row(children: [ Row(children: [
const Expanded(child: Text('Evidencias del Ayuntamiento', const Expanded(child: Text('Evidencias del Ayuntamiento',
style: TextStyle(fontWeight: FontWeight.bold, style: TextStyle(fontWeight: FontWeight.bold,
color: AppColors.verdeAdmin, fontSize: 14))), color: AppColors.guindaPrimary, fontSize: 14))),
Text('${_evidencias.length}', Text('${_evidencias.length}',
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)), style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
]), ]),
@@ -221,8 +221,8 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
onPressed: _pickFoto, onPressed: _pickFoto,
icon: const Icon(Icons.camera_alt, size: 16), icon: const Icon(Icons.camera_alt, size: 16),
label: const Text('Tomar foto', style: TextStyle(fontSize: 12)), label: const Text('Tomar foto', style: TextStyle(fontSize: 12)),
style: OutlinedButton.styleFrom(foregroundColor: AppColors.verdeAdmin, style: OutlinedButton.styleFrom(foregroundColor: AppColors.guindaPrimary,
side: const BorderSide(color: AppColors.verdeAdmin))) side: const BorderSide(color: AppColors.guindaPrimary)))
: Stack(children: [ : Stack(children: [
ClipRRect(borderRadius: BorderRadius.circular(6), ClipRRect(borderRadius: BorderRadius.circular(6),
child: Image.file(_evidFoto!, height: 70, width: double.infinity, fit: BoxFit.cover)), child: Image.file(_evidFoto!, height: 70, width: double.infinity, fit: BoxFit.cover)),
@@ -234,7 +234,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(
onPressed: _loadingEv ? null : _agregarEvidencia, onPressed: _loadingEv ? null : _agregarEvidencia,
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
foregroundColor: Colors.white, minimumSize: const Size(80, 42)), foregroundColor: Colors.white, minimumSize: const Size(80, 42)),
child: _loadingEv child: _loadingEv
? const SizedBox(width: 16, height: 16, ? const SizedBox(width: 16, height: 16,
@@ -249,11 +249,11 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
child: Padding(padding: const EdgeInsets.all(10), child: Column( child: Padding(padding: const EdgeInsets.all(10), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [ crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(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), const SizedBox(width: 4),
Text(ev['admin_nombre'] as String? ?? 'Admin', Text(ev['admin_nombre'] as String? ?? 'Admin',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11,
color: AppColors.verdeAdmin)), color: AppColors.guindaPrimary)),
const Spacer(), const Spacer(),
Text(_timeAgo(ev['fecha'] as String? ?? ''), Text(_timeAgo(ev['fecha'] as String? ?? ''),
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),

View File

@@ -31,7 +31,7 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: const Text('Dashboard de Estadisticas'), title: const Text('Dashboard de Estadisticas'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -57,7 +57,7 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
Icons.warning, AppColors.rojoError), Icons.warning, AppColors.rojoError),
const SizedBox(width: 8), const SizedBox(width: 8),
_KpiCard('Conductores', '${_stats['total_conductores']}', _KpiCard('Conductores', '${_stats['total_conductores']}',
Icons.person, AppColors.moradoConductor), Icons.person, AppColors.guindaPrimary),
]), ]),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -93,10 +93,10 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
FlSpot(e.key.toDouble(), FlSpot(e.key.toDouble(),
(e.value['promedio'] as num? ?? 0).toDouble().clamp(1.0, 5.0))).toList(), (e.value['promedio'] as num? ?? 0).toDouble().clamp(1.0, 5.0))).toList(),
isCurved: true, isCurved: true,
color: AppColors.verdeAdmin, color: AppColors.guindaPrimary,
barWidth: 3, barWidth: 3,
belowBarData: BarAreaData(show: true, belowBarData: BarAreaData(show: true,
color: AppColors.verdeAdmin.withOpacity(0.1)), color: AppColors.guindaPrimary.withOpacity(0.1)),
dotData: const FlDotData(show: true), dotData: const FlDotData(show: true),
)], )],
))))), ))))),
@@ -247,7 +247,7 @@ class _SectionTitle extends StatelessWidget {
const _SectionTitle(this.title); const _SectionTitle(this.title);
@override @override
Widget build(BuildContext context) => Text(title, 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 { class _Legend extends StatelessWidget {

View File

@@ -93,7 +93,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
return Scaffold( return Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Text(widget.editing != null ? 'Editar Ruta' : 'Nueva Ruta'), title: Text(widget.editing != null ? 'Editar Ruta' : 'Nueva Ruta'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -115,7 +115,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
Expanded(child: RadioListTile<String>(dense: true, value: t, Expanded(child: RadioListTile<String>(dense: true, value: t,
groupValue: _turno, groupValue: _turno,
title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)), title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)),
activeColor: AppColors.verdeAdmin, activeColor: AppColors.guindaPrimary,
onChanged: (v) => setState(() => _turno = v!))) onChanged: (v) => setState(() => _turno = v!)))
).toList()), ).toList()),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -150,16 +150,16 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
Expanded(child: OutlinedButton( Expanded(child: OutlinedButton(
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)), onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.verdeAdmin, foregroundColor: AppColors.guindaPrimary,
side: const BorderSide(color: AppColors.verdeAdmin)), side: const BorderSide(color: AppColors.guindaPrimary)),
child: const Text('Grupo A\nL/M/V', textAlign: TextAlign.center, child: const Text('Grupo A\nL/M/V', textAlign: TextAlign.center,
style: TextStyle(fontSize: 11)))), style: TextStyle(fontSize: 11)))),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: OutlinedButton( Expanded(child: OutlinedButton(
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoB)), onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoB)),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.moradoConductor, foregroundColor: AppColors.guindaPrimary,
side: const BorderSide(color: AppColors.moradoConductor)), side: const BorderSide(color: AppColors.guindaPrimary)),
child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center, child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center,
style: TextStyle(fontSize: 11)))), style: TextStyle(fontSize: 11)))),
]), ]),
@@ -170,7 +170,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11, label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11,
color: sel ? Colors.white : AppColors.negroTexto)), color: sel ? Colors.white : AppColors.negroTexto)),
selected: sel, selected: sel,
selectedColor: AppColors.verdeAdmin, selectedColor: AppColors.guindaPrimary,
checkmarkColor: Colors.white, checkmarkColor: Colors.white,
onSelected: (v) => setState(() { onSelected: (v) => setState(() {
if (v) _diasSeleccionados.add(dia); if (v) _diasSeleccionados.add(dia);
@@ -202,7 +202,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
return CheckboxListTile(dense: true, return CheckboxListTile(dense: true,
title: Text(c, style: const TextStyle(fontSize: 12)), title: Text(c, style: const TextStyle(fontSize: 12)),
value: sel, value: sel,
activeColor: AppColors.verdeAdmin, activeColor: AppColors.guindaPrimary,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
onChanged: (v) => setState(() { onChanged: (v) => setState(() {
if (v == true) _coloniasSeleccionadas.add(c); if (v == true) _coloniasSeleccionadas.add(c);
@@ -216,8 +216,8 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Wrap(spacing: 4, runSpacing: 4, children: _coloniasSeleccionadas.map((c) => Wrap(spacing: 4, runSpacing: 4, children: _coloniasSeleccionadas.map((c) =>
Chip(label: Text(c, style: const TextStyle(fontSize: 10)), Chip(label: Text(c, style: const TextStyle(fontSize: 10)),
backgroundColor: AppColors.verdeAdmin.withOpacity(0.1), backgroundColor: AppColors.guindaPrimary.withOpacity(0.1),
deleteIconColor: AppColors.verdeAdmin, deleteIconColor: AppColors.guindaPrimary,
onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()), onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -226,7 +226,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _loading ? null : _guardar, onPressed: _loading ? null : _guardar,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
icon: _loading icon: _loading
? const SizedBox(width: 18, height: 18, ? const SizedBox(width: 18, height: 18,
@@ -242,12 +242,12 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
Widget _section(String title) => Padding( Widget _section(String title) => Padding(
padding: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.only(bottom: 8),
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold, 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) => Widget _field(TextEditingController ctrl, String label, IconData icon) =>
TextField(controller: ctrl, TextField(controller: ctrl,
decoration: InputDecoration(labelText: label, decoration: InputDecoration(labelText: label,
prefixIcon: Icon(icon, color: AppColors.verdeAdmin), prefixIcon: Icon(icon, color: AppColors.guindaPrimary),
border: const OutlineInputBorder(), filled: true, fillColor: Colors.white)); border: const OutlineInputBorder(), filled: true, fillColor: Colors.white));
Widget _timeButton(String label, String value, VoidCallback onTap) => Widget _timeButton(String label, String value, VoidCallback onTap) =>
@@ -257,7 +257,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade400)), border: Border.all(color: Colors.grey.shade400)),
child: Row(children: [ 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), const SizedBox(width: 8),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)), Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),

View File

@@ -193,19 +193,19 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: const Text('Exportar Reporte PDF'), title: const Text('Exportar Reporte PDF'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado))), child: Container(height: 4, color: AppColors.dorado))),
body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column( body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column(
mainAxisAlignment: MainAxisAlignment.center, children: [ mainAxisAlignment: MainAxisAlignment.center, children: [
Container(width: 100, height: 100, Container(width: 100, height: 100,
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1), decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1),
shape: BoxShape.circle), 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 SizedBox(height: 24),
const Text('Reporte Mensual', style: TextStyle(fontSize: 22, 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 SizedBox(height: 8),
const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.', const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
@@ -215,7 +215,7 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: _generating ? null : _generatePdf, onPressed: _generating ? null : _generatePdf,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
icon: _generating icon: _generating
? const SizedBox(width: 20, height: 20, ? const SizedBox(width: 20, height: 20,
@@ -237,7 +237,7 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
fontSize: 13))), fontSize: 13))),
TextButton(onPressed: _generatePdf, TextButton(onPressed: _generatePdf,
child: const Text('Compartir de nuevo', child: const Text('Compartir de nuevo',
style: TextStyle(fontSize: 11, color: AppColors.verdeAdmin))), style: TextStyle(fontSize: 11, color: AppColors.guindaPrimary))),
])), ])),
], ],
])))); ]))));

View File

@@ -52,34 +52,63 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
if (existing != null) if (existing != null)
SwitchListTile(value: activo, dense: true, SwitchListTile(value: activo, dense: true,
title: Text(activo ? 'Conductor Activo' : 'Conductor Inactivo', 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)), fontWeight: FontWeight.bold)),
activeColor: AppColors.verdeAdmin, activeColor: AppColors.guindaPrimary,
onChanged: (v) => setSt(() => activo = v)), onChanged: (v) => setSt(() => activo = v)),
])), ])),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')), TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
foregroundColor: Colors.white), foregroundColor: Colors.white),
onPressed: () async { onPressed: () async {
if (nombreCtrl.text.trim().isEmpty || emailCtrl.text.trim().isEmpty) return; if (nombreCtrl.text.trim().isEmpty) {
if (existing == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
if (passCtrl.text.length < 6) { content: Text('Ingresa el nombre del conductor'),
ScaffoldMessenger.of(context).showSnackBar(const SnackBar( backgroundColor: AppColors.rojoError));
content: Text('La contrasena debe tener al menos 6 caracteres'), return;
backgroundColor: AppColors.rojoError)); }
return; if (emailCtrl.text.trim().isEmpty) {
} ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
await DbHelper.insertConductor(nombreCtrl.text.trim(), content: Text('Ingresa el correo electronico'),
emailCtrl.text.trim().toLowerCase(), passCtrl.text); backgroundColor: AppColors.rojoError));
} else { return;
await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(), }
emailCtrl.text.trim().toLowerCase()); try {
await DbHelper.updateConductorMeta(existing['id'], activo, notasCtrl.text.trim()); 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')), child: Text(existing == null ? 'Crear' : 'Guardar')),
]))); ])));
@@ -89,7 +118,7 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Text('Conductores (${_conductores.length})'), title: Text('Conductores (${_conductores.length})'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
@@ -110,7 +139,7 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
style: TextStyle(color: AppColors.grisTexto)), style: TextStyle(color: AppColors.grisTexto)),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin, style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
foregroundColor: Colors.white), foregroundColor: Colors.white),
onPressed: () => _showFormDialog(), onPressed: () => _showFormDialog(),
icon: const Icon(Icons.add), label: const Text('Agregar primer conductor')), icon: const Icon(Icons.add), label: const Text('Agregar primer conductor')),
@@ -126,17 +155,17 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
side: BorderSide(color: activo side: BorderSide(color: activo
? AppColors.verdeAdmin.withOpacity(0.3) ? AppColors.guindaPrimary.withOpacity(0.3)
: AppColors.rojoError.withOpacity(0.3))), : AppColors.rojoError.withOpacity(0.3))),
child: Padding(padding: const EdgeInsets.all(14), child: Column( child: Padding(padding: const EdgeInsets.all(14), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [ crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
CircleAvatar(radius: 22, CircleAvatar(radius: 22,
backgroundColor: activo backgroundColor: activo
? AppColors.verdeAdmin.withOpacity(0.15) ? AppColors.guindaPrimary.withOpacity(0.15)
: Colors.grey.shade200, : Colors.grey.shade200,
child: Icon(Icons.person, child: Icon(Icons.person,
color: activo ? AppColors.verdeAdmin : AppColors.grisTexto, size: 24)), color: activo ? AppColors.guindaPrimary : AppColors.grisTexto, size: 24)),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(c['nombre'] ?? '', style: const TextStyle( Text(c['nombre'] ?? '', style: const TextStyle(
@@ -146,12 +175,12 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
])), ])),
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
color: activo ? AppColors.verdeAdmin.withOpacity(0.1) color: activo ? AppColors.guindaPrimary.withOpacity(0.1)
: AppColors.rojoError.withOpacity(0.1), : AppColors.rojoError.withOpacity(0.1),
borderRadius: BorderRadius.circular(10)), borderRadius: BorderRadius.circular(10)),
child: Text(activo ? 'Activo' : 'Inactivo', child: Text(activo ? 'Activo' : 'Inactivo',
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold, 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), IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
onPressed: () => _showFormDialog(existing: c)), onPressed: () => _showFormDialog(existing: c)),
]), ]),

View File

@@ -40,20 +40,27 @@ class _ReviewScreenState extends State<ReviewScreen> {
} }
setState(() => _loading = true); setState(() => _loading = true);
await DbHelper.insertReview(ReviewModel( try {
userId: auth.currentUser!.id!, await DbHelper.insertReview(ReviewModel(
colonia: widget.colonia, userId: auth.currentUser!.id!,
routeId: widget.routeId, colonia: widget.colonia,
estrellas: _estrellas, routeId: widget.routeId,
comentario: _comentCtrl.text.trim().isEmpty estrellas: _estrellas,
? 'Sin comentario' : _comentCtrl.text.trim(), comentario: _comentCtrl.text.trim().isEmpty
fecha: DateTime.now().toIso8601String(), ? 'Sin comentario' : _comentCtrl.text.trim(),
nombreUsuario: auth.currentUser!.nombre, fecha: DateTime.now().toIso8601String(),
)); nombreUsuario: auth.currentUser!.nombre,
));
context.read<RouteSimulatorService>().clearReviewPrompt(widget.routeId); context.read<RouteSimulatorService>().clearReviewPrompt(widget.routeId);
if (!mounted) return; if (!mounted) return;
setState(() { _loading = false; _sent = true; }); 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 @override

View File

@@ -71,14 +71,14 @@ class _DriverHomeScreenState extends State<DriverHomeScreen> {
selectedIndex: _tab, selectedIndex: _tab,
onDestinationSelected: (i) => setState(()=>_tab=i), onDestinationSelected: (i) => setState(()=>_tab=i),
backgroundColor: Colors.white, backgroundColor: Colors.white,
indicatorColor: AppColors.moradoConductor.withOpacity(0.15), indicatorColor: AppColors.guindaPrimary.withOpacity(0.15),
destinations: const [ destinations: const [
NavigationDestination(icon:Icon(Icons.dashboard_outlined), 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), 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), 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; ? widget.sim.isGpsActive(widget.todayRouteId!) : true;
return CustomScrollView(slivers:[ 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), bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
child:Container(height:4,color:AppColors.dorado)), child:Container(height:4,color:AppColors.dorado)),
title:Text('Conductor: ${widget.auth.currentUser?.nombre.split(' ').first ?? ''}', 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([ SliverPadding(padding:const EdgeInsets.all(14),sliver:SliverList(delegate:SliverChildListDelegate([
// Ruta de hoy // Ruta de hoy
Card(color:AppColors.moradoConductor.withOpacity(0.08), Card(color:AppColors.guindaPrimary.withOpacity(0.08),
shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(12), 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( child:Padding(padding:const EdgeInsets.all(14),child:Column(
crossAxisAlignment:CrossAxisAlignment.start, children:[ crossAxisAlignment:CrossAxisAlignment.start, children:[
Row(children:[ Row(children:[
const Icon(Icons.today,color:AppColors.moradoConductor), const Icon(Icons.today,color:AppColors.guindaPrimary),
const SizedBox(width:8), const SizedBox(width:8),
Text('Hoy — ${_todayLabel()}', 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(), const Divider(),
if (widget.route != null)...[ if (widget.route != null)...[
@@ -155,7 +155,7 @@ class _DriverMainTabState extends State<_DriverMainTab> {
const SizedBox(height:8), const SizedBox(height:8),
LinearProgressIndicator(value:(posIdx+1)/8, LinearProgressIndicator(value:(posIdx+1)/8,
backgroundColor:Colors.grey.shade300, backgroundColor:Colors.grey.shade300,
valueColor:const AlwaysStoppedAnimation<Color>(AppColors.moradoConductor)), valueColor:const AlwaysStoppedAnimation<Color>(AppColors.guindaPrimary)),
const SizedBox(height:6), const SizedBox(height:6),
Text(widget.sim.getEtaText(widget.todayRouteId??''), Text(widget.sim.getEtaText(widget.todayRouteId??''),
style:const TextStyle(fontSize:13,fontWeight:FontWeight.w500)), 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( Card(child:Padding(padding:const EdgeInsets.all(12),child:Column(
crossAxisAlignment:CrossAxisAlignment.start, children:[ crossAxisAlignment:CrossAxisAlignment.start, children:[
const Text('📋 Instrucciones de Ruta', const Text('📋 Instrucciones de Ruta',
style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor)), style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary)),
const Divider(), const Divider(),
const Text('• Sigue la ruta asignada sin desviaciones\n' const Text('• Sigue la ruta asignada sin desviaciones\n'
'• Mantén el GPS activo en todo momento\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( Card(child:Padding(padding:const EdgeInsets.all(12),child:Column(
crossAxisAlignment:CrossAxisAlignment.start, children:[ crossAxisAlignment:CrossAxisAlignment.start, children:[
const Text('Mi Horario', const Text('Mi Horario',
style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.moradoConductor)), style:TextStyle(fontWeight:FontWeight.bold,color:AppColors.guindaPrimary)),
const Divider(), const Divider(),
if (widget.assignments.isEmpty) if (widget.assignments.isEmpty)
const Text('Sin asignaciones. Contacta al administrador.', 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(_){} try { found = all.firstWhere((a)=>a.diaSemana==dia); break; } catch(_){}
} }
return Container(padding:const EdgeInsets.all(10), 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), 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:[ 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), const SizedBox(width:6),
Expanded(child:Text(label,style:const TextStyle(fontWeight:FontWeight.w600,fontSize:12))), Expanded(child:Text(label,style:const TextStyle(fontWeight:FontWeight.w600,fontSize:12))),
if (found!=null) if (found!=null)
Container(padding:const EdgeInsets.symmetric(horizontal:8,vertical:3), 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}', child:Text('${found.routeId}${found.turno}',
style:const TextStyle(fontSize:11,color:Colors.white,fontWeight:FontWeight.bold))) style:const TextStyle(fontSize:11,color:Colors.white,fontWeight:FontWeight.bold)))
else else
@@ -262,7 +262,7 @@ class _DriverMapTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar:AppBar(automaticallyImplyLeading:false, 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)), title:Text(route.name,style:const TextStyle(fontSize:13)),
bottom:PreferredSize(preferredSize:const Size.fromHeight(4), bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
child:Container(height:4,color:AppColors.dorado))), child:Container(height:4,color:AppColors.dorado))),
@@ -333,7 +333,7 @@ class _DriverReportesTabState extends State<_DriverReportesTab> {
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
backgroundColor:AppColors.grisFondo, backgroundColor:AppColors.grisFondo,
appBar:AppBar(automaticallyImplyLeading:false, appBar:AppBar(automaticallyImplyLeading:false,
backgroundColor:AppColors.moradoConductor,foregroundColor:Colors.white, backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white,
title:const Text('Reportar Incidente'), title:const Text('Reportar Incidente'),
bottom:PreferredSize(preferredSize:const Size.fromHeight(4), bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
child:Container(height:4,color:AppColors.dorado))), child:Container(height:4,color:AppColors.dorado))),
@@ -349,14 +349,14 @@ class _DriverReportesTabState extends State<_DriverReportesTab> {
if (widget.todayRouteId != null) if (widget.todayRouteId != null)
Container(margin:const EdgeInsets.only(bottom:12), Container(margin:const EdgeInsets.only(bottom:12),
padding:const EdgeInsets.all(10), 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), 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:[ 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), const SizedBox(width:6),
Text('Incidente en: ${widget.todayRouteId}', 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 else
Container(margin:const EdgeInsets.only(bottom:12), 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( child:Padding(padding:const EdgeInsets.all(16),child:Column(
crossAxisAlignment:CrossAxisAlignment.start, children:[ crossAxisAlignment:CrossAxisAlignment.start, children:[
const Text('Tipo de incidente', 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), const SizedBox(height:8),
..._tipos.entries.map((e)=>RadioListTile<String>(dense:true, ..._tipos.entries.map((e)=>RadioListTile<String>(dense:true,
value:e.key,groupValue:_tipo, value:e.key,groupValue:_tipo,
title:Text(e.value,style:const TextStyle(fontSize:13)), title:Text(e.value,style:const TextStyle(fontSize:13)),
activeColor:AppColors.moradoConductor, activeColor:AppColors.guindaPrimary,
onChanged:(v)=>setState(()=>_tipo=v!))), onChanged:(v)=>setState(()=>_tipo=v!))),
const SizedBox(height:10), const SizedBox(height:10),
const Text('Descripción',style:TextStyle(fontWeight:FontWeight.w600,fontSize:13)), 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, SizedBox(width:double.infinity,height:48,
child:ElevatedButton.icon( child:ElevatedButton.icon(
onPressed:(_loading||widget.todayRouteId==null)?null:_enviar, onPressed:(_loading||widget.todayRouteId==null)?null:_enviar,
style:ElevatedButton.styleFrom(backgroundColor:AppColors.moradoConductor, style:ElevatedButton.styleFrom(backgroundColor:AppColors.guindaPrimary,
foregroundColor:Colors.white, foregroundColor:Colors.white,
shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(8))), shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(8))),
icon:_loading?const SizedBox(width:18,height:18, icon:_loading?const SizedBox(width:18,height:18,
@@ -408,11 +408,11 @@ class _DriverReportesTabState extends State<_DriverReportesTab> {
const SizedBox(height:16), const SizedBox(height:16),
const Align(alignment:Alignment.centerLeft, const Align(alignment:Alignment.centerLeft,
child:Text('Mis incidentes de hoy', 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), const SizedBox(height:8),
..._misIncidentes.take(5).map((a)=>Card(margin:const EdgeInsets.only(bottom:6), ..._misIncidentes.take(5).map((a)=>Card(margin:const EdgeInsets.only(bottom:6),
child:ListTile(dense:true, 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)), child:const Icon(Icons.warning,color:Colors.white,size:14)),
title:Text(_tipos[a.tipo]??a.tipo, title:Text(_tipos[a.tipo]??a.tipo,
style:const TextStyle(fontSize:12,fontWeight:FontWeight.w600)), 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 final color = notif.event==NotifEvent.gpsLost?Colors.red.shade800
:notif.event==NotifEvent.truckStopped?AppColors.naranjaAlerta :notif.event==NotifEvent.truckStopped?AppColors.naranjaAlerta
:notif.event==NotifEvent.routeCancelled?AppColors.rojoError :notif.event==NotifEvent.routeCancelled?AppColors.rojoError
:AppColors.moradoConductor; :AppColors.guindaPrimary;
return Material(color:Colors.transparent, return Material(color:Colors.transparent,
child:Container(margin:const EdgeInsets.all(10), child:Container(margin:const EdgeInsets.all(10),
decoration:BoxDecoration(color:color,borderRadius:BorderRadius.circular(12), decoration:BoxDecoration(color:color,borderRadius:BorderRadius.circular(12),

View File

@@ -20,155 +20,234 @@ class _ReporteChatScreenState extends State<ReporteChatScreen> {
List<Map<String, dynamic>> _msgs = []; List<Map<String, dynamic>> _msgs = [];
bool _loading = true; bool _loading = true;
Timer? _timer; Timer? _timer;
String? _myRol;
int? _myUserId;
@override void initState() { super.initState(); _load(); @override
_timer = Timer.periodic(const Duration(seconds: 5), (_) => _load()); } void initState() {
super.initState();
final auth = context.read<AuthService>();
_myRol = auth.currentUser?.rol ?? 'CIUDADANO';
_myUserId = auth.currentUser?.id;
_load();
_timer = Timer.periodic(const Duration(seconds: 4), (_) => _load());
}
Future<void> _load() async { Future<void> _load() async {
final auth = context.read<AuthService>(); // Cargar TODOS los mensajes del reporte sin filtro de rol
final rol = auth.currentUser?.rol ?? 'CIUDADANO';
final msgs = await DbHelper.getChatMsgs(widget.reporteId); 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) { if (mounted) {
setState(() { _msgs = msgs; _loading = false; }); setState(() { _msgs = msgs; _loading = false; });
WidgetsBinding.instance.addPostFrameCallback((_) { _scrollToBottom();
if (_scroll.hasClients) _scroll.animateTo(_scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
});
} }
} }
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scroll.hasClients) {
_scroll.animateTo(_scroll.position.maxScrollExtent,
duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
}
});
}
Future<void> _send() async { Future<void> _send() async {
final text = _ctrl.text.trim(); final text = _ctrl.text.trim();
if (text.isEmpty) return; if (text.isEmpty || _myRol == null || _myUserId == null) return;
final auth = context.read<AuthService>();
final user = auth.currentUser;
if (user == null) return;
_ctrl.clear(); _ctrl.clear();
await DbHelper.insertChatMsg(widget.reporteId, user.id!, user.rol, text); await DbHelper.insertChatMsg(widget.reporteId, _myUserId!, _myRol!, text);
await _load(); await _load();
} }
bool _isMe(Map<String, dynamic> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final auth = context.watch<AuthService>(); final isAdmin = _myRol == 'ADMINISTRADOR';
final myRol = auth.currentUser?.rol ?? 'CIUDADANO';
final isAdmin = myRol == 'ADMINISTRADOR';
final accent = isAdmin ? AppColors.verdeAdmin : AppColors.guindaPrimary;
return Scaffold( return Scaffold(
backgroundColor: AppColors.grisFondo, backgroundColor: AppColors.grisFondo,
appBar: AppBar( appBar: AppBar(
backgroundColor: accent, foregroundColor: Colors.white, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Chat del Reporte', style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)), const Text('Chat del Reporte',
Text('Folio: ${widget.folio}', style: const TextStyle(fontSize: 11, color: Colors.white70)), 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), bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)), child: Container(height: 4, color: AppColors.dorado)),
actions: [if (widget.isClosed) Container(margin: const EdgeInsets.only(right: 12), actions: [
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), if (widget.isClosed)
decoration: BoxDecoration(color: AppColors.verdeExito, borderRadius: BorderRadius.circular(12)), Container(margin: const EdgeInsets.only(right: 12),
child: const Text('COMPLETADO', style: TextStyle(fontSize: 10, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
color: Colors.white, fontWeight: FontWeight.bold)))], 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: [ body: Column(children: [
if (widget.isClosed) Container( if (widget.isClosed)
width: double.infinity, padding: const EdgeInsets.all(10), Container(width: double.infinity, padding: const EdgeInsets.all(10),
color: AppColors.verdeExito.withOpacity(0.08), color: AppColors.verdeExito.withOpacity(0.08),
child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [ child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.lock, size: 14, color: AppColors.verdeExito), Icon(Icons.lock, size: 14, color: AppColors.verdeExito),
SizedBox(width: 6), SizedBox(width: 6),
Text('Reporte completado. Chat cerrado.', Text('Reporte completado Chat cerrado',
style: TextStyle(fontSize: 12, color: AppColors.verdeExito, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: AppColors.verdeExito,
])), fontWeight: FontWeight.w600)),
])),
// Mensajes
Expanded(child: _loading Expanded(child: _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator(
color: AppColors.guindaPrimary))
: _msgs.isEmpty : _msgs.isEmpty
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ ? 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), const SizedBox(height: 12),
Text(isAdmin ? 'Inicia la conversacion con el ciudadano' Text(isAdmin
: 'Escribe tu mensaje al Ayuntamiento', ? 'Sin mensajes aún. Inicia la conversación.'
style: TextStyle(color: Colors.grey.shade500)), : 'Escribe tu mensaje al Ayuntamiento de Celaya.',
style: const TextStyle(color: AppColors.grisTexto),
textAlign: TextAlign.center),
])) ]))
: ListView.builder( : ListView.builder(
controller: _scroll, padding: const EdgeInsets.all(12), controller: _scroll,
padding: const EdgeInsets.fromLTRB(12, 12, 12, 4),
itemCount: _msgs.length, itemCount: _msgs.length,
itemBuilder: (_, i) { itemBuilder: (_, i) {
final m = _msgs[i]; final m = _msgs[i];
final rol = m['rol'] as String; final me = _isMe(m);
final isMe = rol == myRol; final rol = m['rol'] as String? ?? '';
final fecha = DateTime.tryParse(m['fecha'] as String? ?? ''); final fecha = DateTime.tryParse(m['fecha'] as String? ?? '');
final hora = fecha != null 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( return Padding(
padding: const EdgeInsets.symmetric(vertical: 3), padding: const EdgeInsets.symmetric(vertical: 4),
child: Row( child: Row(
mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, mainAxisAlignment: me ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
if (!isMe) ...[ if (!me) ...[
CircleAvatar(radius: 14, CircleAvatar(radius: 16,
backgroundColor: (rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary).withOpacity(0.15), backgroundColor: AppColors.guindaPrimary.withOpacity(0.15),
child: Icon(rol=='ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person, child: Icon(
size: 14, color: rol=='ADMINISTRADOR' ? AppColors.verdeAdmin : AppColors.guindaPrimary)), rol == 'ADMINISTRADOR' ? Icons.admin_panel_settings : Icons.person,
size: 15, color: AppColors.guindaPrimary)),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
Flexible(child: Container( 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( decoration: BoxDecoration(
color: isMe ? accent : Colors.white, color: me ? AppColors.guindaPrimary : Colors.white,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16), topRight: const Radius.circular(16), topLeft: const Radius.circular(18),
bottomLeft: Radius.circular(isMe ? 16 : 4), topRight: const Radius.circular(18),
bottomRight: Radius.circular(isMe ? 4 : 16), 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), // Input
decoration: BoxDecoration(color: Colors.white, if (!widget.isClosed)
boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 4, offset: const Offset(0,-2))]), Container(
child: SafeArea(top: false, child: Row(children: [ padding: const EdgeInsets.fromLTRB(12, 8, 12, 12),
Expanded(child: TextField( decoration: BoxDecoration(
controller: _ctrl, maxLines: 3, minLines: 1, color: Colors.white,
textCapitalization: TextCapitalization.sentences, boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08),
decoration: InputDecoration( blurRadius: 6, offset: const Offset(0, -2))]),
hintText: isAdmin ? 'Responde al ciudadano...' : 'Escribe al Ayuntamiento...', child: SafeArea(top: false, child: Row(children: [
border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), Expanded(child: TextField(
filled: true, fillColor: AppColors.grisFondo, controller: _ctrl,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), isDense: true), maxLines: 4, minLines: 1,
)), textCapitalization: TextCapitalization.sentences,
const SizedBox(width: 8), decoration: InputDecoration(
CircleAvatar(radius: 22, backgroundColor: accent, hintText: isAdmin
child: IconButton(icon: const Icon(Icons.send, color: Colors.white, size: 18), onPressed: _send)), ? 'Responde al ciudadano...'
]))) : 'Escribe al Ayuntamiento de Celaya...',
else Container(padding: const EdgeInsets.all(14), color: Colors.white, 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: [ child: const Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.lock_outline, size: 16, color: AppColors.grisTexto), Icon(Icons.lock_outline, size: 16, color: AppColors.grisTexto),
SizedBox(width: 6), 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();
}
} }

View File

@@ -208,8 +208,8 @@ class DriverRouteMap extends StatelessWidget {
Polyline(points:pending, color:Colors.grey.shade400, strokeWidth:5, Polyline(points:pending, color:Colors.grey.shade400, strokeWidth:5,
borderColor:Colors.white54, borderStrokeWidth:1), borderColor:Colors.white54, borderStrokeWidth:1),
if (done.isNotEmpty) if (done.isNotEmpty)
Polyline(points:done, color:AppColors.moradoConductor, strokeWidth:6, Polyline(points:done, color:AppColors.guindaPrimary, strokeWidth:6,
borderColor:AppColors.moradoConductor.withOpacity(0.4), borderStrokeWidth:2), borderColor:AppColors.guindaPrimary.withOpacity(0.4), borderStrokeWidth:2),
]), ]),
MarkerLayer(markers:[ MarkerLayer(markers:[
// Waypoints pendientes // Waypoints pendientes
@@ -223,7 +223,7 @@ class DriverRouteMap extends StatelessWidget {
Marker(point:cur, width:56, height:56, Marker(point:cur, width:56, height:56,
child:Transform.rotate(angle:bear*math.pi/180, child:Transform.rotate(angle:bear*math.pi/180,
child:Container(decoration:BoxDecoration( 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), border:Border.all(color:Colors.white,width:2.5),
boxShadow:[BoxShadow(color:Colors.black38,blurRadius:8)]), boxShadow:[BoxShadow(color:Colors.black38,blurRadius:8)]),
child:Icon(gps?Icons.navigation:Icons.gps_off,color:Colors.white,size:28)))), child:Icon(gps?Icons.navigation:Icons.gps_off,color:Colors.white,size:28)))),