Actualizacion de mejoras
This commit is contained in:
@@ -49,22 +49,22 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||
selectedIndex:_tab,
|
||||
onDestinationSelected:(i)=>setState(()=>_tab=i),
|
||||
backgroundColor:Colors.white,
|
||||
indicatorColor:AppColors.verdeAdmin.withOpacity(0.15),
|
||||
indicatorColor:AppColors.guindaPrimary.withOpacity(0.15),
|
||||
destinations:const[
|
||||
NavigationDestination(icon:Icon(Icons.dashboard_outlined),
|
||||
selectedIcon:Icon(Icons.dashboard,color:AppColors.verdeAdmin),label:'Panel'),
|
||||
selectedIcon:Icon(Icons.dashboard,color:AppColors.guindaPrimary),label:'Panel'),
|
||||
NavigationDestination(icon:Icon(Icons.map_outlined),
|
||||
selectedIcon:Icon(Icons.map,color:AppColors.verdeAdmin),label:'Mapa'),
|
||||
selectedIcon:Icon(Icons.map,color:AppColors.guindaPrimary),label:'Mapa'),
|
||||
NavigationDestination(icon:Icon(Icons.report_outlined),
|
||||
selectedIcon:Icon(Icons.report,color:AppColors.verdeAdmin),label:'Reportes'),
|
||||
selectedIcon:Icon(Icons.report,color:AppColors.guindaPrimary),label:'Reportes'),
|
||||
NavigationDestination(icon:Icon(Icons.people_alt_outlined),
|
||||
selectedIcon:Icon(Icons.people_alt,color:AppColors.verdeAdmin),label:'Asignar'),
|
||||
selectedIcon:Icon(Icons.people_alt,color:AppColors.guindaPrimary),label:'Asignar'),
|
||||
NavigationDestination(icon:Icon(Icons.warning_outlined),
|
||||
selectedIcon:Icon(Icons.warning,color:AppColors.verdeAdmin),label:'Alertas'),
|
||||
selectedIcon:Icon(Icons.warning,color:AppColors.guindaPrimary),label:'Alertas'),
|
||||
NavigationDestination(icon:Icon(Icons.route_outlined),
|
||||
selectedIcon:Icon(Icons.route,color:AppColors.verdeAdmin),label:'Rutas'),
|
||||
selectedIcon:Icon(Icons.route,color:AppColors.guindaPrimary),label:'Rutas'),
|
||||
NavigationDestination(icon:Icon(Icons.star_outline),
|
||||
selectedIcon:Icon(Icons.star,color:AppColors.verdeAdmin),label:'Reseñas'),
|
||||
selectedIcon:Icon(Icons.star,color:AppColors.guindaPrimary),label:'Reseñas'),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -137,7 +137,7 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(slivers: [
|
||||
SliverAppBar(pinned: true, backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
SliverAppBar(pinned: true, backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
title: const Text('Panel Administrador', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
@@ -159,14 +159,14 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
|
||||
),
|
||||
SliverPadding(padding: const EdgeInsets.all(12), sliver: SliverList(delegate: SliverChildListDelegate([
|
||||
Row(children: [
|
||||
_Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.verdeAdmin),
|
||||
_Stat('Rutas', '${routesData.length}', Icons.local_shipping, AppColors.guindaPrimary),
|
||||
const SizedBox(width: 10),
|
||||
_Stat('Incidentes', '${_conductorIncidentes.where((i)=>!i.resuelta).length}',
|
||||
Icons.warning, AppColors.naranjaAlerta),
|
||||
]),
|
||||
const SizedBox(height: 14),
|
||||
const Text('Control de Rutas', style: TextStyle(fontWeight: FontWeight.bold,
|
||||
fontSize: 16, color: AppColors.verdeAdmin)),
|
||||
fontSize: 16, color: AppColors.guindaPrimary)),
|
||||
const SizedBox(height: 8),
|
||||
...routesData.map((r) {
|
||||
final status = _getStatus(r.routeId);
|
||||
@@ -249,18 +249,18 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 6, 14, 2),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.build, size: 13, color: AppColors.moradoConductor),
|
||||
const Icon(Icons.build, size: 13, color: AppColors.guindaPrimary),
|
||||
const SizedBox(width: 4),
|
||||
const Text('Incidentes del conductor:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 11,
|
||||
color: AppColors.moradoConductor)),
|
||||
color: AppColors.guindaPrimary)),
|
||||
]),
|
||||
),
|
||||
...incidentes.map((inc) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 2, 14, 2),
|
||||
child: Row(children: [
|
||||
Container(width: 6, height: 6,
|
||||
decoration: const BoxDecoration(color: AppColors.moradoConductor,
|
||||
decoration: const BoxDecoration(color: AppColors.guindaPrimary,
|
||||
shape: BoxShape.circle)),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(child: Text(inc.mensaje,
|
||||
@@ -286,7 +286,7 @@ class _AdminHomeTabState extends State<_AdminHomeTab> {
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.verdeAdmin,
|
||||
foregroundColor: AppColors.guindaPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8)),
|
||||
child: const Text('Actuar', style: TextStyle(fontSize: 10)),
|
||||
),
|
||||
@@ -396,7 +396,7 @@ class _AdminMapTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar:AppBar(automaticallyImplyLeading:false,
|
||||
backgroundColor:AppColors.verdeAdmin,foregroundColor:Colors.white,
|
||||
backgroundColor:AppColors.guindaPrimary,foregroundColor:Colors.white,
|
||||
title:const Text('Mapa — Todas las Rutas'),
|
||||
bottom:PreferredSize(preferredSize:const Size.fromHeight(4),
|
||||
child:Container(height:4,color:AppColors.dorado))),
|
||||
@@ -444,7 +444,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text('Reportes Ciudadanos (${_filtered.length})'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -464,7 +464,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
|
||||
style: TextStyle(fontSize: 11,
|
||||
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||
selected: sel,
|
||||
selectedColor: e == 'TODOS' ? AppColors.verdeAdmin : _estadoColor(e),
|
||||
selectedColor: e == 'TODOS' ? AppColors.guindaPrimary : _estadoColor(e),
|
||||
checkmarkColor: Colors.white,
|
||||
onSelected: (_) => setState(() => _filtroEstado = e)));
|
||||
})),
|
||||
@@ -500,7 +500,7 @@ class _AdminReportesTabState extends State<_AdminReportesTab> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Text(folio, style: const TextStyle(fontWeight: FontWeight.bold,
|
||||
fontSize: 13, color: AppColors.verdeAdmin)),
|
||||
fontSize: 13, color: AppColors.guindaPrimary)),
|
||||
const SizedBox(width: 8),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
@@ -533,7 +533,7 @@ class _AdminConductoresTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: const Text('Gestión de Conductores'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -565,7 +565,7 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text('Rutas del Sistema (${_routes.length})'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -589,7 +589,7 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
|
||||
const Text('No hay rutas creadas', style: TextStyle(color: AppColors.grisTexto)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
|
||||
foregroundColor: Colors.white),
|
||||
onPressed: () async {
|
||||
final ok = await Navigator.push(context, MaterialPageRoute(
|
||||
@@ -607,13 +607,13 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
|
||||
: r.turno == 'VESPERTINO' ? '🌅' : '🌙';
|
||||
return Card(margin: const EdgeInsets.only(bottom: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: AppColors.verdeAdmin.withOpacity(0.3))),
|
||||
side: BorderSide(color: AppColors.guindaPrimary.withOpacity(0.3))),
|
||||
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Expanded(child: Text('${r.routeId} — ${r.nombre}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold,
|
||||
fontSize: 14, color: AppColors.verdeAdmin))),
|
||||
fontSize: 14, color: AppColors.guindaPrimary))),
|
||||
IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
onPressed: () async {
|
||||
final ok = await Navigator.push(context, MaterialPageRoute(
|
||||
@@ -636,10 +636,10 @@ class _AdminRoutesTabState extends State<_AdminRoutesTab> {
|
||||
const SizedBox(height: 4),
|
||||
Wrap(spacing: 4, runSpacing: 4, children: r.colonias.take(8).map((c) =>
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(c, style: const TextStyle(fontSize: 10,
|
||||
color: AppColors.verdeAdmin)))).toList()),
|
||||
color: AppColors.guindaPrimary)))).toList()),
|
||||
if (r.colonias.length > 8)
|
||||
Text(' ...y ${r.colonias.length - 8} más',
|
||||
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||
@@ -670,7 +670,7 @@ class _AdminReviewsTabState extends State<_AdminReviewsTab> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text(_showSummary ? 'Calificaciones por Colonia' : 'Reseñas Ciudadanas'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -794,11 +794,19 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
|
||||
static const _grupoA = ['LUNES', 'MIERCOLES', 'VIERNES'];
|
||||
static const _grupoB = ['MARTES', 'JUEVES', 'SABADO'];
|
||||
|
||||
List<String> _todasLasRutas = [];
|
||||
|
||||
@override void initState() { super.initState(); _load(); }
|
||||
|
||||
Future<void> _load() async {
|
||||
final c = await DbHelper.getUsersByRol('CONDUCTOR');
|
||||
if (mounted) setState(() => _conductores = c);
|
||||
// Combinar rutas hardcoded + rutas creadas por admin en DB
|
||||
final dbRoutes = await DbHelper.getAllRouteDefinitions();
|
||||
final dbIds = dbRoutes.map((r) => r.routeId).toList();
|
||||
final staticIds = routesData.map((r) => r.routeId).toList();
|
||||
// Unión sin duplicados
|
||||
final allIds = {...staticIds, ...dbIds}.toList()..sort();
|
||||
if (mounted) setState(() { _conductores = c; _todasLasRutas = allIds; });
|
||||
}
|
||||
|
||||
Future<void> _loadAsigs(int id) async {
|
||||
@@ -821,14 +829,227 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
|
||||
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
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
title: const Text('Asignar Rutas a Conductores'),
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: const Text('Conductores y Asignaciones'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado))),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_add_outlined),
|
||||
tooltip: 'Nuevo conductor',
|
||||
onPressed: () => _showNuevoConductor(context)),
|
||||
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
|
||||
]),
|
||||
body: SingleChildScrollView(padding: const EdgeInsets.all(16), child: Column(children: [
|
||||
// Lista de conductores con chip seleccionable
|
||||
if (_conductores.isEmpty)
|
||||
Container(padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade200)),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.info_outline, color: AppColors.naranjaAlerta),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(child: Text('No hay conductores registrados. Agrega uno con el boton +',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.naranjaAlerta))),
|
||||
]))
|
||||
else ...[
|
||||
const Align(alignment: Alignment.centerLeft,
|
||||
child: Text('Conductores registrados',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13,
|
||||
color: AppColors.guindaPrimary))),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 44, child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _conductores.length,
|
||||
itemBuilder: (_, i) {
|
||||
final c = _conductores[i];
|
||||
final sel = _sel?.id == c.id;
|
||||
return Padding(padding: const EdgeInsets.only(right: 8),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
onTap: () { setState(() => _sel = c); _loadAsigs(c.id!); },
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: sel ? AppColors.guindaPrimary : Colors.white,
|
||||
borderRadius: BorderRadius.circular(22),
|
||||
border: Border.all(
|
||||
color: sel ? AppColors.guindaPrimary : Colors.grey.shade300,
|
||||
width: sel ? 2 : 1),
|
||||
boxShadow: sel ? [BoxShadow(color: AppColors.guindaPrimary.withOpacity(0.3),
|
||||
blurRadius: 6, offset: const Offset(0, 2))] : [],
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
CircleAvatar(radius: 12,
|
||||
backgroundColor: sel ? Colors.white.withOpacity(0.3) : AppColors.guindaPrimary.withOpacity(0.1),
|
||||
child: Text(c.nombre[0].toUpperCase(),
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold,
|
||||
color: sel ? Colors.white : AppColors.guindaPrimary))),
|
||||
const SizedBox(width: 6),
|
||||
Text(c.nombre.split(' ').first,
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600,
|
||||
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||
]),
|
||||
)));
|
||||
})),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
// Info del conductor seleccionado
|
||||
if (_sel != null) ...[
|
||||
Container(padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.06),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.guindaPrimary.withOpacity(0.2))),
|
||||
child: Row(children: [
|
||||
CircleAvatar(radius: 18, backgroundColor: AppColors.guindaPrimary,
|
||||
child: Text(_sel!.nombre[0].toUpperCase(),
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold))),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(_sel!.nombre, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
|
||||
Text(_sel!.email, style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)),
|
||||
])),
|
||||
TextButton.icon(
|
||||
onPressed: () => _showEditarConductor(context, _sel!),
|
||||
icon: const Icon(Icons.edit_outlined, size: 14),
|
||||
label: const Text('Editar', style: TextStyle(fontSize: 11)),
|
||||
style: TextButton.styleFrom(foregroundColor: AppColors.guindaPrimary)),
|
||||
])),
|
||||
],
|
||||
// Info de grupos
|
||||
Container(padding: const EdgeInsets.all(10), margin: const EdgeInsets.only(bottom: 12),
|
||||
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200)),
|
||||
@@ -837,29 +1058,21 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
|
||||
'Grupo A — Lunes, Miercoles y Viernes\n'
|
||||
'Grupo B — Martes, Jueves y Sabado',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.azulInfo))),
|
||||
DropdownButtonFormField<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) ...[
|
||||
const SizedBox(height: 20),
|
||||
_GrupoRow(label: 'Grupo A — Lunes, Miercoles y Viernes',
|
||||
icon: Icons.wb_sunny_outlined, color: Colors.blue,
|
||||
current: _getGrupo(_grupoA), routeIds: routesData.map((r) => r.routeId).toList(),
|
||||
current: _getGrupo(_grupoA), routeIds: _todasLasRutas,
|
||||
onSave: (rid, turno) => _saveGrupo(_grupoA, rid, turno)),
|
||||
const SizedBox(height: 12),
|
||||
_GrupoRow(label: 'Grupo B — Martes, Jueves y Sabado',
|
||||
icon: Icons.wb_twilight, color: Colors.deepPurple,
|
||||
current: _getGrupo(_grupoB), routeIds: routesData.map((r) => r.routeId).toList(),
|
||||
current: _getGrupo(_grupoB), routeIds: _todasLasRutas,
|
||||
onSave: (rid, turno) => _saveGrupo(_grupoB, rid, turno)),
|
||||
if (_asigs.isNotEmpty) ...[
|
||||
const SizedBox(height: 20),
|
||||
const Text('Resumen actual', style: TextStyle(fontWeight: FontWeight.bold,
|
||||
color: AppColors.verdeAdmin, fontSize: 14)),
|
||||
color: AppColors.guindaPrimary, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Card(child: Padding(padding: const EdgeInsets.all(12), child: Column(children: [
|
||||
..._asigs.map((a) => Padding(padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
@@ -867,10 +1080,10 @@ class _AdminAssignmentsTabState extends State<_AdminAssignmentsTab> {
|
||||
SizedBox(width: 100, child: Text(AppDias.label(a.diaSemana),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12))),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Text('${a.routeId} • ${a.turno}',
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.verdeAdmin))),
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.guindaPrimary))),
|
||||
]))),
|
||||
]))),
|
||||
],
|
||||
@@ -947,6 +1160,7 @@ class _AdminAlertasTab extends StatefulWidget {
|
||||
class _AdminAlertasTabState extends State<_AdminAlertasTab> {
|
||||
List<AlertaModel> _alertas = [];
|
||||
bool _loading = true;
|
||||
String _filtro = 'TODAS';
|
||||
|
||||
@override void initState() { super.initState(); _load(); }
|
||||
|
||||
@@ -955,62 +1169,172 @@ class _AdminAlertasTabState extends State<_AdminAlertasTab> {
|
||||
if (mounted) setState(() { _alertas = a; _loading = false; });
|
||||
}
|
||||
|
||||
Color _alertaColor(String tipo) {
|
||||
List<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 == 'GPS_PERDIDO') return AppColors.rojoError;
|
||||
if (tipo == 'CAMION_DETENIDO') return AppColors.naranjaAlerta;
|
||||
if (tipo.startsWith('RUTA_')) return AppColors.rojoError;
|
||||
if (tipo.startsWith('RUTA_')) return AppColors.guindaPrimary;
|
||||
return AppColors.azulInfo;
|
||||
}
|
||||
|
||||
IconData _icon(String tipo) {
|
||||
if (tipo.startsWith('INCIDENTE')) return Icons.build;
|
||||
if (tipo == 'GPS_PERDIDO') return Icons.gps_off;
|
||||
if (tipo == 'CAMION_DETENIDO') return Icons.timer_off;
|
||||
if (tipo == 'RUTA_CANCELADA') return Icons.cancel;
|
||||
if (tipo == 'RUTA_RETRASADA') return Icons.access_time;
|
||||
return Icons.warning_amber;
|
||||
}
|
||||
|
||||
Future<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
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false,
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
title: Text('Alertas del Sistema (${_alertas.length})'),
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text('Alertas del Sistema (${_filtered.length})'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
actions: [IconButton(icon: const Icon(Icons.refresh), onPressed: _load)]),
|
||||
body: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _alertas.isEmpty
|
||||
? const Center(child: Text('Sin alertas',
|
||||
style: TextStyle(color: AppColors.grisTexto)))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
itemCount: _alertas.length,
|
||||
itemBuilder: (_, i) {
|
||||
final a = _alertas[i];
|
||||
final c = _alertaColor(a.tipo);
|
||||
return Card(margin: const EdgeInsets.only(bottom: 6),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: c.withOpacity(0.3))),
|
||||
child: ListTile(dense: true,
|
||||
leading: CircleAvatar(radius: 18, backgroundColor: c.withOpacity(0.12),
|
||||
child: Icon(a.resuelta ? Icons.check : Icons.warning,
|
||||
color: a.resuelta ? AppColors.verdeExito : c, size: 16)),
|
||||
title: Text(a.tipo.replaceAll('_', ' '),
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold,
|
||||
color: a.resuelta ? AppColors.grisTexto : c)),
|
||||
subtitle: Text(a.mensaje, maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11)),
|
||||
trailing: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(a.routeId, style: const TextStyle(
|
||||
fontSize: 10, color: AppColors.grisTexto)),
|
||||
if (!a.resuelta) TextButton(
|
||||
onPressed: () async {
|
||||
await DbHelper.resolverAlerta(a.id!); _load();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero, minimumSize: Size.zero,
|
||||
foregroundColor: AppColors.verdeExito),
|
||||
child: const Text('Resolver', style: TextStyle(fontSize: 10))),
|
||||
])));
|
||||
}),
|
||||
body: Column(children: [
|
||||
// Filtros
|
||||
Container(color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
child: Row(children: ['TODAS','ACTIVAS','RESUELTAS'].map((f) {
|
||||
final sel = _filtro == f;
|
||||
return Padding(padding: const EdgeInsets.only(right: 8),
|
||||
child: FilterChip(
|
||||
label: Text(f, style: TextStyle(fontSize: 11,
|
||||
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||
selected: sel,
|
||||
selectedColor: AppColors.guindaPrimary,
|
||||
checkmarkColor: Colors.white,
|
||||
onSelected: (_) => setState(() => _filtro = f)));
|
||||
}).toList())),
|
||||
// Lista
|
||||
Expanded(child: _loading
|
||||
? const Center(child: CircularProgressIndicator(color: AppColors.guindaPrimary))
|
||||
: _filtered.isEmpty
|
||||
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
const Icon(Icons.notifications_none, color: AppColors.grisTexto, size: 48),
|
||||
const SizedBox(height: 12),
|
||||
Text(_filtro == 'ACTIVAS' ? 'Sin alertas activas' : 'Sin alertas',
|
||||
style: const TextStyle(color: AppColors.grisTexto)),
|
||||
]))
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(10),
|
||||
itemCount: _filtered.length,
|
||||
itemBuilder: (_, i) {
|
||||
final a = _filtered[i];
|
||||
final c = _color(a.tipo);
|
||||
final fecha = DateTime.tryParse(a.fecha);
|
||||
final fechaStr = fecha != null
|
||||
? '${fecha.day}/${fecha.month} ${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}'
|
||||
: '';
|
||||
return Card(margin: const EdgeInsets.only(bottom: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: a.resuelta
|
||||
? Colors.grey.shade200 : c.withOpacity(0.3))),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: a.resuelta ? null : () => _showAcciones(a),
|
||||
child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [
|
||||
CircleAvatar(radius: 20,
|
||||
backgroundColor: a.resuelta
|
||||
? Colors.grey.shade100 : c.withOpacity(0.12),
|
||||
child: Icon(_icon(a.tipo),
|
||||
color: a.resuelta ? AppColors.grisTexto : c, size: 18)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(a.tipo.replaceAll('_', ' '),
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold,
|
||||
color: a.resuelta ? AppColors.grisTexto : c)),
|
||||
const SizedBox(height: 2),
|
||||
Text(a.mensaje, maxLines: 2, overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)),
|
||||
const SizedBox(height: 2),
|
||||
Text('${a.routeId} • $fechaStr',
|
||||
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||
])),
|
||||
const SizedBox(width: 8),
|
||||
if (a.resuelta)
|
||||
const Icon(Icons.check_circle, color: AppColors.verdeExito, size: 20)
|
||||
else
|
||||
const Icon(Icons.chevron_right, color: AppColors.grisTexto, size: 20),
|
||||
]))));
|
||||
})),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// ── Widgets auxiliares ────────────────────────────────────────────────────
|
||||
class _Stat extends StatelessWidget {
|
||||
final String label, value; final IconData icon; final Color color;
|
||||
@@ -1034,7 +1358,7 @@ class _AdminBanner extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Material(color: Colors.transparent,
|
||||
child: Container(margin: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: AppColors.verdeAdmin, borderRadius: BorderRadius.circular(10),
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary, borderRadius: BorderRadius.circular(10),
|
||||
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6)]),
|
||||
child: Padding(padding: const EdgeInsets.all(10), child: Row(children: [
|
||||
const Icon(Icons.notifications, color: Colors.white, size: 20),
|
||||
|
||||
@@ -110,7 +110,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('Reporte $folio', style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
|
||||
Text(_r['colonia'] as String? ?? '', style: const TextStyle(fontSize: 11, color: Colors.white70)),
|
||||
@@ -179,7 +179,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
// Cambiar estado
|
||||
if (!isClosed) ...[
|
||||
const Text('Cambiar Estado', style: TextStyle(fontWeight: FontWeight.bold,
|
||||
color: AppColors.verdeAdmin, fontSize: 14)),
|
||||
color: AppColors.guindaPrimary, fontSize: 14)),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 8, runSpacing: 8, children: _estados.map((e) {
|
||||
final isActual = e == estado;
|
||||
@@ -199,7 +199,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
Row(children: [
|
||||
const Expanded(child: Text('Evidencias del Ayuntamiento',
|
||||
style: TextStyle(fontWeight: FontWeight.bold,
|
||||
color: AppColors.verdeAdmin, fontSize: 14))),
|
||||
color: AppColors.guindaPrimary, fontSize: 14))),
|
||||
Text('${_evidencias.length}',
|
||||
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
|
||||
]),
|
||||
@@ -221,8 +221,8 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
onPressed: _pickFoto,
|
||||
icon: const Icon(Icons.camera_alt, size: 16),
|
||||
label: const Text('Tomar foto', style: TextStyle(fontSize: 12)),
|
||||
style: OutlinedButton.styleFrom(foregroundColor: AppColors.verdeAdmin,
|
||||
side: const BorderSide(color: AppColors.verdeAdmin)))
|
||||
style: OutlinedButton.styleFrom(foregroundColor: AppColors.guindaPrimary,
|
||||
side: const BorderSide(color: AppColors.guindaPrimary)))
|
||||
: Stack(children: [
|
||||
ClipRRect(borderRadius: BorderRadius.circular(6),
|
||||
child: Image.file(_evidFoto!, height: 70, width: double.infinity, fit: BoxFit.cover)),
|
||||
@@ -234,7 +234,7 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _loadingEv ? null : _agregarEvidencia,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
|
||||
foregroundColor: Colors.white, minimumSize: const Size(80, 42)),
|
||||
child: _loadingEv
|
||||
? const SizedBox(width: 16, height: 16,
|
||||
@@ -249,11 +249,11 @@ class _AdminReporteDetalleScreenState extends State<AdminReporteDetalleScreen>
|
||||
child: Padding(padding: const EdgeInsets.all(10), child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Icon(Icons.verified, color: AppColors.verdeAdmin, size: 14),
|
||||
const Icon(Icons.verified, color: AppColors.guindaPrimary, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(ev['admin_nombre'] as String? ?? 'Admin',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11,
|
||||
color: AppColors.verdeAdmin)),
|
||||
color: AppColors.guindaPrimary)),
|
||||
const Spacer(),
|
||||
Text(_timeAgo(ev['fecha'] as String? ?? ''),
|
||||
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||
|
||||
@@ -31,7 +31,7 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: const Text('Dashboard de Estadisticas'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -57,7 +57,7 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
|
||||
Icons.warning, AppColors.rojoError),
|
||||
const SizedBox(width: 8),
|
||||
_KpiCard('Conductores', '${_stats['total_conductores']}',
|
||||
Icons.person, AppColors.moradoConductor),
|
||||
Icons.person, AppColors.guindaPrimary),
|
||||
]),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@@ -93,10 +93,10 @@ class _AdminStatsScreenState extends State<AdminStatsScreen> {
|
||||
FlSpot(e.key.toDouble(),
|
||||
(e.value['promedio'] as num? ?? 0).toDouble().clamp(1.0, 5.0))).toList(),
|
||||
isCurved: true,
|
||||
color: AppColors.verdeAdmin,
|
||||
color: AppColors.guindaPrimary,
|
||||
barWidth: 3,
|
||||
belowBarData: BarAreaData(show: true,
|
||||
color: AppColors.verdeAdmin.withOpacity(0.1)),
|
||||
color: AppColors.guindaPrimary.withOpacity(0.1)),
|
||||
dotData: const FlDotData(show: true),
|
||||
)],
|
||||
))))),
|
||||
@@ -247,7 +247,7 @@ class _SectionTitle extends StatelessWidget {
|
||||
const _SectionTitle(this.title);
|
||||
@override
|
||||
Widget build(BuildContext context) => Text(title,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.verdeAdmin));
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: AppColors.guindaPrimary));
|
||||
}
|
||||
|
||||
class _Legend extends StatelessWidget {
|
||||
|
||||
@@ -93,7 +93,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text(widget.editing != null ? 'Editar Ruta' : 'Nueva Ruta'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -115,7 +115,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
Expanded(child: RadioListTile<String>(dense: true, value: t,
|
||||
groupValue: _turno,
|
||||
title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)),
|
||||
activeColor: AppColors.verdeAdmin,
|
||||
activeColor: AppColors.guindaPrimary,
|
||||
onChanged: (v) => setState(() => _turno = v!)))
|
||||
).toList()),
|
||||
const SizedBox(height: 8),
|
||||
@@ -150,16 +150,16 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
Expanded(child: OutlinedButton(
|
||||
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.verdeAdmin,
|
||||
side: const BorderSide(color: AppColors.verdeAdmin)),
|
||||
foregroundColor: AppColors.guindaPrimary,
|
||||
side: const BorderSide(color: AppColors.guindaPrimary)),
|
||||
child: const Text('Grupo A\nL/M/V', textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 11)))),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: OutlinedButton(
|
||||
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoB)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.moradoConductor,
|
||||
side: const BorderSide(color: AppColors.moradoConductor)),
|
||||
foregroundColor: AppColors.guindaPrimary,
|
||||
side: const BorderSide(color: AppColors.guindaPrimary)),
|
||||
child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 11)))),
|
||||
]),
|
||||
@@ -170,7 +170,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11,
|
||||
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||
selected: sel,
|
||||
selectedColor: AppColors.verdeAdmin,
|
||||
selectedColor: AppColors.guindaPrimary,
|
||||
checkmarkColor: Colors.white,
|
||||
onSelected: (v) => setState(() {
|
||||
if (v) _diasSeleccionados.add(dia);
|
||||
@@ -202,7 +202,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
return CheckboxListTile(dense: true,
|
||||
title: Text(c, style: const TextStyle(fontSize: 12)),
|
||||
value: sel,
|
||||
activeColor: AppColors.verdeAdmin,
|
||||
activeColor: AppColors.guindaPrimary,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (v) => setState(() {
|
||||
if (v == true) _coloniasSeleccionadas.add(c);
|
||||
@@ -216,8 +216,8 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 4, runSpacing: 4, children: _coloniasSeleccionadas.map((c) =>
|
||||
Chip(label: Text(c, style: const TextStyle(fontSize: 10)),
|
||||
backgroundColor: AppColors.verdeAdmin.withOpacity(0.1),
|
||||
deleteIconColor: AppColors.verdeAdmin,
|
||||
backgroundColor: AppColors.guindaPrimary.withOpacity(0.1),
|
||||
deleteIconColor: AppColors.guindaPrimary,
|
||||
onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
@@ -226,7 +226,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _loading ? null : _guardar,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
icon: _loading
|
||||
? const SizedBox(width: 18, height: 18,
|
||||
@@ -242,12 +242,12 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
Widget _section(String title) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Text(title, style: const TextStyle(fontWeight: FontWeight.bold,
|
||||
color: AppColors.verdeAdmin, fontSize: 15)));
|
||||
color: AppColors.guindaPrimary, fontSize: 15)));
|
||||
|
||||
Widget _field(TextEditingController ctrl, String label, IconData icon) =>
|
||||
TextField(controller: ctrl,
|
||||
decoration: InputDecoration(labelText: label,
|
||||
prefixIcon: Icon(icon, color: AppColors.verdeAdmin),
|
||||
prefixIcon: Icon(icon, color: AppColors.guindaPrimary),
|
||||
border: const OutlineInputBorder(), filled: true, fillColor: Colors.white));
|
||||
|
||||
Widget _timeButton(String label, String value, VoidCallback onTap) =>
|
||||
@@ -257,7 +257,7 @@ class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade400)),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.access_time, color: AppColors.verdeAdmin, size: 18),
|
||||
const Icon(Icons.access_time, color: AppColors.guindaPrimary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||
|
||||
@@ -193,19 +193,19 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: const Text('Exportar Reporte PDF'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado))),
|
||||
body: Center(child: Padding(padding: const EdgeInsets.all(32), child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Container(width: 100, height: 100,
|
||||
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
|
||||
decoration: BoxDecoration(color: AppColors.guindaPrimary.withOpacity(0.1),
|
||||
shape: BoxShape.circle),
|
||||
child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.verdeAdmin)),
|
||||
child: const Icon(Icons.picture_as_pdf, size: 52, color: AppColors.guindaPrimary)),
|
||||
const SizedBox(height: 24),
|
||||
const Text('Reporte Mensual', style: TextStyle(fontSize: 22,
|
||||
fontWeight: FontWeight.bold, color: AppColors.verdeAdmin)),
|
||||
fontWeight: FontWeight.bold, color: AppColors.guindaPrimary)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Genera un PDF con el resumen completo:\nreportes, incidentes y calificaciones.',
|
||||
textAlign: TextAlign.center,
|
||||
@@ -215,7 +215,7 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _generating ? null : _generatePdf,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
|
||||
icon: _generating
|
||||
? const SizedBox(width: 20, height: 20,
|
||||
@@ -237,7 +237,7 @@ class _ExportPdfScreenState extends State<ExportPdfScreen> {
|
||||
fontSize: 13))),
|
||||
TextButton(onPressed: _generatePdf,
|
||||
child: const Text('Compartir de nuevo',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.verdeAdmin))),
|
||||
style: TextStyle(fontSize: 11, color: AppColors.guindaPrimary))),
|
||||
])),
|
||||
],
|
||||
]))));
|
||||
|
||||
@@ -52,34 +52,63 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||
if (existing != null)
|
||||
SwitchListTile(value: activo, dense: true,
|
||||
title: Text(activo ? 'Conductor Activo' : 'Conductor Inactivo',
|
||||
style: TextStyle(color: activo ? AppColors.verdeAdmin : AppColors.rojoError,
|
||||
style: TextStyle(color: activo ? AppColors.guindaPrimary : AppColors.rojoError,
|
||||
fontWeight: FontWeight.bold)),
|
||||
activeColor: AppColors.verdeAdmin,
|
||||
activeColor: AppColors.guindaPrimary,
|
||||
onChanged: (v) => setSt(() => activo = v)),
|
||||
])),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Cancelar')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
|
||||
foregroundColor: Colors.white),
|
||||
onPressed: () async {
|
||||
if (nombreCtrl.text.trim().isEmpty || emailCtrl.text.trim().isEmpty) return;
|
||||
if (existing == null) {
|
||||
if (passCtrl.text.length < 6) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('La contrasena debe tener al menos 6 caracteres'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
return;
|
||||
}
|
||||
await DbHelper.insertConductor(nombreCtrl.text.trim(),
|
||||
emailCtrl.text.trim().toLowerCase(), passCtrl.text);
|
||||
} else {
|
||||
await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(),
|
||||
emailCtrl.text.trim().toLowerCase());
|
||||
await DbHelper.updateConductorMeta(existing['id'], activo, notasCtrl.text.trim());
|
||||
if (nombreCtrl.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Ingresa el nombre del conductor'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
return;
|
||||
}
|
||||
if (emailCtrl.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Ingresa el correo electronico'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (existing == null) {
|
||||
if (passCtrl.text.length < 6) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('La contrasena debe tener al menos 6 caracteres'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
return;
|
||||
}
|
||||
await DbHelper.insertConductor(
|
||||
nombreCtrl.text.trim(),
|
||||
emailCtrl.text.trim().toLowerCase(),
|
||||
passCtrl.text);
|
||||
if (ctx.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Conductor creado correctamente'),
|
||||
backgroundColor: AppColors.verdeExito));
|
||||
}
|
||||
} else {
|
||||
await DbHelper.updateConductor(existing['id'], nombreCtrl.text.trim(),
|
||||
emailCtrl.text.trim().toLowerCase());
|
||||
await DbHelper.updateConductorMeta(
|
||||
existing['id'], activo, notasCtrl.text.trim());
|
||||
}
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
await _load();
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(e.toString().contains('UNIQUE')
|
||||
? 'Ese correo ya está registrado'
|
||||
: 'Error: ${e.toString()}'),
|
||||
backgroundColor: AppColors.rojoError));
|
||||
}
|
||||
}
|
||||
if (ctx.mounted) Navigator.pop(ctx);
|
||||
await _load();
|
||||
},
|
||||
child: Text(existing == null ? 'Crear' : 'Guardar')),
|
||||
])));
|
||||
@@ -89,7 +118,7 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
backgroundColor: AppColors.guindaPrimary, foregroundColor: Colors.white,
|
||||
title: Text('Conductores (${_conductores.length})'),
|
||||
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
|
||||
child: Container(height: 4, color: AppColors.dorado)),
|
||||
@@ -110,7 +139,7 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||
style: TextStyle(color: AppColors.grisTexto)),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.guindaPrimary,
|
||||
foregroundColor: Colors.white),
|
||||
onPressed: () => _showFormDialog(),
|
||||
icon: const Icon(Icons.add), label: const Text('Agregar primer conductor')),
|
||||
@@ -126,17 +155,17 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
|
||||
side: BorderSide(color: activo
|
||||
? AppColors.verdeAdmin.withOpacity(0.3)
|
||||
? AppColors.guindaPrimary.withOpacity(0.3)
|
||||
: AppColors.rojoError.withOpacity(0.3))),
|
||||
child: Padding(padding: const EdgeInsets.all(14), child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
CircleAvatar(radius: 22,
|
||||
backgroundColor: activo
|
||||
? AppColors.verdeAdmin.withOpacity(0.15)
|
||||
? AppColors.guindaPrimary.withOpacity(0.15)
|
||||
: Colors.grey.shade200,
|
||||
child: Icon(Icons.person,
|
||||
color: activo ? AppColors.verdeAdmin : AppColors.grisTexto, size: 24)),
|
||||
color: activo ? AppColors.guindaPrimary : AppColors.grisTexto, size: 24)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(c['nombre'] ?? '', style: const TextStyle(
|
||||
@@ -146,12 +175,12 @@ class _ManageConductorsScreenState extends State<ManageConductorsScreen> {
|
||||
])),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: activo ? AppColors.verdeAdmin.withOpacity(0.1)
|
||||
color: activo ? AppColors.guindaPrimary.withOpacity(0.1)
|
||||
: AppColors.rojoError.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10)),
|
||||
child: Text(activo ? 'Activo' : 'Inactivo',
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.bold,
|
||||
color: activo ? AppColors.verdeAdmin : AppColors.rojoError))),
|
||||
color: activo ? AppColors.guindaPrimary : AppColors.rojoError))),
|
||||
IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
onPressed: () => _showFormDialog(existing: c)),
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user