Avance de la aplicacion
This commit is contained in:
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