Avance de la aplicacion
This commit is contained in:
@@ -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)),
|
||||
],
|
||||
])));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user