273 lines
12 KiB
Dart
273 lines
12 KiB
Dart
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.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)),
|
|
),
|
|
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.guindaPrimary,
|
|
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.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.guindaPrimary,
|
|
side: const BorderSide(color: AppColors.guindaPrimary)),
|
|
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.guindaPrimary,
|
|
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.guindaPrimary,
|
|
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.guindaPrimary.withOpacity(0.1),
|
|
deleteIconColor: AppColors.guindaPrimary,
|
|
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.guindaPrimary, 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.guindaPrimary, fontSize: 15)));
|
|
|
|
Widget _field(TextEditingController ctrl, String label, IconData icon) =>
|
|
TextField(controller: ctrl,
|
|
decoration: InputDecoration(labelText: label,
|
|
prefixIcon: Icon(icon, color: AppColors.guindaPrimary),
|
|
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.guindaPrimary, 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(); }
|
|
}
|