Avance de la aplicacion

This commit is contained in:
2026-05-22 20:43:49 -06:00
parent 37e83a8226
commit 458af32fcf
13 changed files with 1918 additions and 463 deletions

View File

@@ -6,6 +6,8 @@ import '../../services/route_simulator_service.dart';
import '../../database/db_helper.dart';
import '../../models/models.dart';
import '../../data/routes_data.dart';
import '../../models/route_model.dart' show ColonyModel;
import 'create_route_screen.dart';
import '../../widgets/route_map_widget.dart';
class AdminDashboardScreen extends StatefulWidget {
@@ -27,6 +29,8 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
_AdminReportesTab(),
_AdminAssignmentsTab(),
_AdminAlertasTab(sim:sim),
_AdminRoutesTab(),
_AdminReviewsTab(),
];
return Scaffold(
@@ -51,6 +55,10 @@ class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
selectedIcon:Icon(Icons.calendar_month,color:AppColors.verdeAdmin),label:'Asignar'),
NavigationDestination(icon:Icon(Icons.warning_outlined),
selectedIcon:Icon(Icons.warning,color:AppColors.verdeAdmin),label:'Alertas'),
NavigationDestination(icon:Icon(Icons.route_outlined),
selectedIcon:Icon(Icons.route,color:AppColors.verdeAdmin),label:'Rutas'),
NavigationDestination(icon:Icon(Icons.star_outline),
selectedIcon:Icon(Icons.star,color:AppColors.verdeAdmin),label:'Reseñas'),
],
),
);
@@ -784,3 +792,238 @@ class _AdminBanner extends StatelessWidget {
IconButton(icon:const Icon(Icons.close,color:Colors.white,size:18),onPressed:onDismiss),
]))));
}
// ── TAB 6: Gestión de Rutas ───────────────────────────────────────────────
class _AdminRoutesTab extends StatefulWidget {
@override State<_AdminRoutesTab> createState() => _AdminRoutesTabState();
}
class _AdminRoutesTabState extends State<_AdminRoutesTab> {
List<RouteDefinitionModel> _routes = [];
bool _loading = true;
@override void initState() { super.initState(); _load(); }
Future<void> _load() async {
final r = await DbHelper.getAllRouteDefinitions();
if (mounted) setState(() { _routes = r; _loading = false; });
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
title: Text('Rutas del Sistema (${_routes.length})'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
IconButton(
icon: const Icon(Icons.add_circle_outline),
tooltip: 'Nueva ruta',
onPressed: () async {
final ok = await Navigator.push(context, MaterialPageRoute(
builder: (_) => const CreateRouteScreen()));
if (ok == true) await _load();
}),
]),
body: _loading
? const Center(child: CircularProgressIndicator())
: _routes.isEmpty
? Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
const Icon(Icons.route, color: AppColors.grisTexto, size: 48),
const SizedBox(height: 12),
const Text('No hay rutas creadas', style: TextStyle(color: AppColors.grisTexto)),
const SizedBox(height: 16),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(backgroundColor: AppColors.verdeAdmin,
foregroundColor: Colors.white),
onPressed: () async {
final ok = await Navigator.push(context, MaterialPageRoute(
builder: (_) => const CreateRouteScreen()));
if (ok == true) await _load();
},
icon: const Icon(Icons.add), label: const Text('Crear primera ruta')),
]))
: ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _routes.length,
itemBuilder: (_, i) {
final r = _routes[i];
final turnoEmoji = r.turno == 'MATUTINO' ? '🌄'
: r.turno == 'VESPERTINO' ? '🌅' : '🌙';
return Card(margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
side: BorderSide(color: AppColors.verdeAdmin.withOpacity(0.3))),
child: Padding(padding: const EdgeInsets.all(14), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Expanded(child: Text('${r.routeId}${r.nombre}',
style: const TextStyle(fontWeight: FontWeight.bold,
fontSize: 14, color: AppColors.verdeAdmin))),
IconButton(icon: const Icon(Icons.edit_outlined, size: 18),
onPressed: () async {
final ok = await Navigator.push(context, MaterialPageRoute(
builder: (_) => CreateRouteScreen(editing: r)));
if (ok == true) await _load();
}),
]),
const SizedBox(height: 4),
Row(children: [
Text('$turnoEmoji ${r.turno}${r.horaInicio}${r.horaFin}',
style: const TextStyle(color: AppColors.grisTexto, fontSize: 12)),
]),
const SizedBox(height: 4),
Text(r.dias.map(AppDias.label).join(', '),
style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)),
const SizedBox(height: 6),
// Colonias
Text('📍 ${r.colonias.length} colonias:',
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12)),
const SizedBox(height: 4),
Wrap(spacing: 4, runSpacing: 4, children: r.colonias.take(8).map((c) =>
Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(color: AppColors.verdeAdmin.withOpacity(0.1),
borderRadius: BorderRadius.circular(8)),
child: Text(c, style: const TextStyle(fontSize: 10,
color: AppColors.verdeAdmin)))).toList()),
if (r.colonias.length > 8)
Text(' ...y ${r.colonias.length - 8} más',
style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
])));
}),
);
}
// ── TAB 7: Reseñas y calificaciones ──────────────────────────────────────
class _AdminReviewsTab extends StatefulWidget {
@override State<_AdminReviewsTab> createState() => _AdminReviewsTabState();
}
class _AdminReviewsTabState extends State<_AdminReviewsTab> {
List<ReviewModel> _reviews = [];
List<Map<String, dynamic>> _summary = [];
bool _showSummary = false;
bool _loading = true;
@override void initState() { super.initState(); _load(); }
Future<void> _load() async {
final r = await DbHelper.getAllReviews();
final s = await DbHelper.getReviewSummaryByColonia();
if (mounted) setState(() { _reviews = r; _summary = s; _loading = false; });
}
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(automaticallyImplyLeading: false,
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
title: Text(_showSummary ? 'Calificaciones por Colonia' : 'Reseñas Ciudadanas'),
bottom: PreferredSize(preferredSize: const Size.fromHeight(4),
child: Container(height: 4, color: AppColors.dorado)),
actions: [
IconButton(
icon: Icon(_showSummary ? Icons.list : Icons.bar_chart),
tooltip: _showSummary ? 'Ver reseñas' : 'Ver por colonia',
onPressed: () => setState(() => _showSummary = !_showSummary)),
IconButton(icon: const Icon(Icons.refresh), onPressed: _load),
]),
body: _loading
? const Center(child: CircularProgressIndicator())
: _showSummary ? _buildSummary() : _buildReviews(),
);
Widget _buildSummary() {
if (_summary.isEmpty) return const Center(
child: Text('Sin calificaciones aún', style: TextStyle(color: AppColors.grisTexto)));
return Column(children: [
// Header explicativo
Container(margin: const EdgeInsets.all(12), padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200)),
child: const Row(children: [
Icon(Icons.info_outline, color: AppColors.azulInfo, size: 16),
SizedBox(width: 6),
Expanded(child: Text(
'Colonias ordenadas de menor a mayor calificación. '
'Las primeras requieren atención prioritaria.',
style: TextStyle(fontSize: 11, color: AppColors.azulInfo))),
])),
Expanded(child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 12),
itemCount: _summary.length,
itemBuilder: (_, i) {
final s = _summary[i];
final prom = (s['promedio'] as num).toDouble();
final total = s['total'] as int;
final colonia = s['colonia'] as String;
final routeId = s['route_id'] as String;
final color = prom >= 4.5 ? AppColors.verdeExito
: prom >= 3.5 ? Colors.amber.shade700
: prom >= 2.5 ? AppColors.naranjaAlerta
: AppColors.rojoError;
final emoji = prom >= 4.5 ? '🟢' : prom >= 3.5 ? '🟡' : prom >= 2.5 ? '🟠' : '🔴';
return Card(margin: const EdgeInsets.only(bottom: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10),
side: BorderSide(color: color.withOpacity(0.3))),
child: Padding(padding: const EdgeInsets.all(12), child: Row(children: [
Container(width: 6, height: 50,
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(3))),
const SizedBox(width: 12),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(colonia, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
Text('$emoji $routeId$total reseña${total != 1 ? "s" : ""}',
style: const TextStyle(fontSize: 11, color: AppColors.grisTexto)),
])),
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
Text(prom.toStringAsFixed(1),
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: color)),
Row(children: List.generate(5, (j) =>
Icon(j < prom.round() ? Icons.star : Icons.star_border,
color: Colors.amber, size: 12))),
]),
])));
})),
]);
}
Widget _buildReviews() {
if (_reviews.isEmpty) return const Center(
child: Text('Sin reseñas aún', style: TextStyle(color: AppColors.grisTexto)));
return ListView.builder(
padding: const EdgeInsets.all(12),
itemCount: _reviews.length,
itemBuilder: (_, i) {
final r = _reviews[i];
final fecha = DateTime.tryParse(r.fecha);
final fechaStr = fecha != null
? '${fecha.day}/${fecha.month}/${fecha.year} ${fecha.hour.toString().padLeft(2,'0')}:${fecha.minute.toString().padLeft(2,'0')}'
: r.fecha;
return Card(margin: const EdgeInsets.only(bottom: 8),
child: Padding(padding: const EdgeInsets.all(12), child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
CircleAvatar(backgroundColor: AppColors.guindaPrimary.withOpacity(0.1), radius: 18,
child: Text('${r.estrellas}', style: const TextStyle(fontSize: 11))),
const SizedBox(width: 10),
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(r.nombreUsuario, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
Text('${r.colonia}${r.routeId}',
style: const TextStyle(color: AppColors.grisTexto, fontSize: 11)),
])),
Text(fechaStr, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
]),
const SizedBox(height: 8),
Row(children: List.generate(5, (j) =>
Icon(j < r.estrellas ? Icons.star : Icons.star_border,
color: Colors.amber, size: 16))),
if (r.comentario.isNotEmpty && r.comentario != 'Sin comentario') ...[
const SizedBox(height: 6),
Text('"${r.comentario}"',
style: const TextStyle(fontSize: 12, fontStyle: FontStyle.italic,
color: AppColors.negroTexto)),
],
])));
});
}
}