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)),
|
||||
],
|
||||
])));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
272
lib/screens/admin/create_route_screen.dart
Normal file
272
lib/screens/admin/create_route_screen.dart
Normal file
@@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../core/app_colors.dart';
|
||||
import '../../data/celaya_colonias.dart';
|
||||
import '../../database/db_helper.dart';
|
||||
import '../../models/models.dart';
|
||||
|
||||
class CreateRouteScreen extends StatefulWidget {
|
||||
final RouteDefinitionModel? editing;
|
||||
const CreateRouteScreen({super.key, this.editing});
|
||||
@override State<CreateRouteScreen> createState() => _CreateRouteScreenState();
|
||||
}
|
||||
|
||||
class _CreateRouteScreenState extends State<CreateRouteScreen> {
|
||||
final _nombreCtrl = TextEditingController();
|
||||
final _routeIdCtrl = TextEditingController();
|
||||
String _turno = 'MATUTINO';
|
||||
String _horaInicio = '06:00';
|
||||
String _horaFin = '08:00';
|
||||
List<String> _diasSeleccionados = [];
|
||||
List<String> _coloniasSeleccionadas = [];
|
||||
String _searchColonia = '';
|
||||
bool _loading = false;
|
||||
|
||||
static const _diasGrupoA = ['LUNES', 'MIERCOLES', 'VIERNES'];
|
||||
static const _diasGrupoB = ['MARTES', 'JUEVES', 'SABADO'];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.editing != null) {
|
||||
final e = widget.editing!;
|
||||
_nombreCtrl.text = e.nombre;
|
||||
_routeIdCtrl.text = e.routeId;
|
||||
_turno = e.turno;
|
||||
_horaInicio = e.horaInicio;
|
||||
_horaFin = e.horaFin;
|
||||
_diasSeleccionados = List.from(e.dias);
|
||||
_coloniasSeleccionadas = List.from(e.colonias);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> get _filteredColonias => _searchColonia.isEmpty
|
||||
? celayaColonias
|
||||
: celayaColonias.where((c) =>
|
||||
c.toLowerCase().contains(_searchColonia.toLowerCase())).toList();
|
||||
|
||||
Future<void> _guardar() async {
|
||||
if (_nombreCtrl.text.trim().isEmpty) {
|
||||
_snack('Ingresa un nombre para la ruta', isError: true); return; }
|
||||
if (_routeIdCtrl.text.trim().isEmpty) {
|
||||
_snack('Ingresa el ID de la ruta (ej. RUTA-16)', isError: true); return; }
|
||||
if (_diasSeleccionados.isEmpty) {
|
||||
_snack('Selecciona al menos un día', isError: true); return; }
|
||||
if (_coloniasSeleccionadas.isEmpty) {
|
||||
_snack('Selecciona al menos una colonia', isError: true); return; }
|
||||
|
||||
setState(() => _loading = true);
|
||||
final route = RouteDefinitionModel(
|
||||
id: widget.editing?.id,
|
||||
routeId: _routeIdCtrl.text.trim().toUpperCase(),
|
||||
nombre: _nombreCtrl.text.trim(),
|
||||
dias: _diasSeleccionados,
|
||||
horaInicio: _horaInicio,
|
||||
horaFin: _horaFin,
|
||||
turno: _turno,
|
||||
colonias: _coloniasSeleccionadas,
|
||||
);
|
||||
await DbHelper.insertRouteDefinition(route);
|
||||
if (!mounted) return;
|
||||
setState(() => _loading = false);
|
||||
_snack('Ruta guardada correctamente');
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
|
||||
void _snack(String msg, {bool isError = false}) =>
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(msg),
|
||||
backgroundColor: isError ? AppColors.rojoError : AppColors.verdeExito));
|
||||
|
||||
Future<TimeOfDay?> _pickTime(String current) async {
|
||||
final parts = current.split(':');
|
||||
return showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay(hour: int.parse(parts[0]), minute: int.parse(parts[1])),
|
||||
);
|
||||
}
|
||||
|
||||
String _timeLabel(TimeOfDay t) =>
|
||||
'${t.hour.toString().padLeft(2,'0')}:${t.minute.toString().padLeft(2,'0')}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.grisFondo,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.verdeAdmin, 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)),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
|
||||
// Info básica
|
||||
_section('Información de la ruta'),
|
||||
_field(_routeIdCtrl, 'ID de Ruta (ej. RUTA-16)', Icons.tag),
|
||||
const SizedBox(height: 12),
|
||||
_field(_nombreCtrl, 'Nombre descriptivo', Icons.route),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Turno
|
||||
_section('Turno de operación'),
|
||||
Row(children: ['MATUTINO','VESPERTINO','NOCTURNO'].map((t) =>
|
||||
Expanded(child: RadioListTile<String>(dense: true, value: t,
|
||||
groupValue: _turno,
|
||||
title: Text(_turnoLabel(t), style: const TextStyle(fontSize: 12)),
|
||||
activeColor: AppColors.verdeAdmin,
|
||||
onChanged: (v) => setState(() => _turno = v!)))
|
||||
).toList()),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Horario
|
||||
_section('Horario de servicio'),
|
||||
Row(children: [
|
||||
Expanded(child: _timeButton('Hora inicio', _horaInicio, () async {
|
||||
final t = await _pickTime(_horaInicio);
|
||||
if (t != null) setState(() => _horaInicio = _timeLabel(t));
|
||||
})),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: _timeButton('Hora fin', _horaFin, () async {
|
||||
final t = await _pickTime(_horaFin);
|
||||
if (t != null) setState(() => _horaFin = _timeLabel(t));
|
||||
})),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Días
|
||||
_section('Días de operación'),
|
||||
Container(padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200)),
|
||||
child: const Text(
|
||||
'📅 Selecciona Grupo A (L/M/V) o Grupo B (M/J/S), o días individuales.',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.azulInfo)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [
|
||||
Expanded(child: OutlinedButton(
|
||||
onPressed: () => setState(() => _diasSeleccionados = List.from(_diasGrupoA)),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.verdeAdmin,
|
||||
side: const BorderSide(color: AppColors.verdeAdmin)),
|
||||
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)),
|
||||
child: const Text('Grupo B\nM/J/S', textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 11)))),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: AppDias.todos.map((dia) {
|
||||
final sel = _diasSeleccionados.contains(dia);
|
||||
return FilterChip(
|
||||
label: Text(AppDias.label(dia), style: TextStyle(fontSize: 11,
|
||||
color: sel ? Colors.white : AppColors.negroTexto)),
|
||||
selected: sel,
|
||||
selectedColor: AppColors.verdeAdmin,
|
||||
checkmarkColor: Colors.white,
|
||||
onSelected: (v) => setState(() {
|
||||
if (v) _diasSeleccionados.add(dia);
|
||||
else _diasSeleccionados.remove(dia);
|
||||
}),
|
||||
);
|
||||
}).toList()),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Colonias
|
||||
_section('Colonias que cubre (${_coloniasSeleccionadas.length} seleccionadas)'),
|
||||
TextField(
|
||||
onChanged: (v) => setState(() => _searchColonia = v),
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Buscar colonia de Celaya...',
|
||||
prefixIcon: Icon(Icons.search), border: OutlineInputBorder(),
|
||||
filled: true, fillColor: Colors.white, isDense: true),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(height: 220,
|
||||
decoration: BoxDecoration(color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey.shade300)),
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredColonias.length,
|
||||
itemBuilder: (_, i) {
|
||||
final c = _filteredColonias[i];
|
||||
final sel = _coloniasSeleccionadas.contains(c);
|
||||
return CheckboxListTile(dense: true,
|
||||
title: Text(c, style: const TextStyle(fontSize: 12)),
|
||||
value: sel,
|
||||
activeColor: AppColors.verdeAdmin,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
onChanged: (v) => setState(() {
|
||||
if (v == true) _coloniasSeleccionadas.add(c);
|
||||
else _coloniasSeleccionadas.remove(c);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_coloniasSeleccionadas.isNotEmpty) ...[
|
||||
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,
|
||||
onDeleted: () => setState(() => _coloniasSeleccionadas.remove(c)))).toList()),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
SizedBox(width: double.infinity, height: 50,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _loading ? null : _guardar,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.verdeAdmin, foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
|
||||
icon: _loading
|
||||
? const SizedBox(width: 18, height: 18,
|
||||
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
|
||||
: const Icon(Icons.save),
|
||||
label: const Text('GUARDAR RUTA', style: TextStyle(fontWeight: FontWeight.bold)))),
|
||||
const SizedBox(height: 30),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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)));
|
||||
|
||||
Widget _field(TextEditingController ctrl, String label, IconData icon) =>
|
||||
TextField(controller: ctrl,
|
||||
decoration: InputDecoration(labelText: label,
|
||||
prefixIcon: Icon(icon, color: AppColors.verdeAdmin),
|
||||
border: const OutlineInputBorder(), filled: true, fillColor: Colors.white));
|
||||
|
||||
Widget _timeButton(String label, String value, VoidCallback onTap) =>
|
||||
InkWell(onTap: onTap,
|
||||
child: Container(padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(color: Colors.white,
|
||||
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 SizedBox(width: 8),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: AppColors.grisTexto)),
|
||||
Text(value, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
|
||||
]),
|
||||
])));
|
||||
|
||||
String _turnoLabel(String t) => t == 'MATUTINO' ? '🌄 Matutino'
|
||||
: t == 'VESPERTINO' ? '🌅 Vespertino' : '🌙 Nocturno';
|
||||
|
||||
@override void dispose() { _nombreCtrl.dispose(); _routeIdCtrl.dispose(); super.dispose(); }
|
||||
}
|
||||
Reference in New Issue
Block a user