Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
version final final ya enserio la final del proyecto :)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -59,9 +60,6 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
case 0:
|
||||
await _showUserForm();
|
||||
break;
|
||||
case 1:
|
||||
await _showRouteForm();
|
||||
break;
|
||||
case 2:
|
||||
await _showUnitForm();
|
||||
break;
|
||||
@@ -132,18 +130,14 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
controller: _tabController,
|
||||
children: const [_UsersTab(), _RoutesTab(), _TrucksTab()],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _handleAdd,
|
||||
backgroundColor: AppTheme.primary,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(
|
||||
_activeTab == 0
|
||||
? 'Nuevo usuario'
|
||||
: _activeTab == 1
|
||||
? 'Nueva ruta'
|
||||
: 'Nueva unidad',
|
||||
),
|
||||
),
|
||||
floatingActionButton: _activeTab == 1
|
||||
? null // Oculta el botón flotante en la pestaña de Rutas
|
||||
: FloatingActionButton.extended(
|
||||
onPressed: _handleAdd,
|
||||
backgroundColor: AppTheme.primary,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(_activeTab == 0 ? 'Nuevo usuario' : 'Nueva unidad'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -289,18 +283,17 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
}
|
||||
}
|
||||
|
||||
// ── Formulario ruta ─────────────────────────────────────────────────────────
|
||||
Future<void> _showRouteForm({AdminRouteModel? route}) async {
|
||||
final isEdit = route != null;
|
||||
final id = TextEditingController(text: route?.id ?? '');
|
||||
final nombre = TextEditingController(text: route?.name ?? '');
|
||||
String? turno = route?.turno;
|
||||
String status = route?.status ?? 'pendiente';
|
||||
int? truckId = route?.truckId;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
// ── Formulario para Reasignar Ruta (Solo Vespertinas) ───────────────────────
|
||||
Future<void> _showReassignRoute(AdminRouteModel route) async {
|
||||
final units = ref
|
||||
.read(adminUnitsProvider)
|
||||
.maybeWhen(data: (u) => u, orElse: () => <AdminUnitModel>[]);
|
||||
.maybeWhen(
|
||||
data: (u) => u.where((x) => x.status == 'active').toList(),
|
||||
orElse: () => <AdminUnitModel>[],
|
||||
);
|
||||
|
||||
int? selectedUnitId;
|
||||
final formKey = GlobalKey<FormState>();
|
||||
|
||||
final saved = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -312,112 +305,47 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
|
||||
),
|
||||
title: Text(isEdit ? 'Editar ruta' : 'Nueva ruta'),
|
||||
title: const Text('Reasignar Unidad'),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_textField(
|
||||
id,
|
||||
'ID (ej. RUTA-01)',
|
||||
required: true,
|
||||
enabled: !isEdit,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Ruta: ${route.displayName}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
'Selecciona una unidad activa para cubrir este turno vespertino:',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_textField(nombre, 'Nombre'),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<String?>(
|
||||
initialValue: turno,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Turno',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppTheme.radiusMd,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nueva Unidad',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppTheme.radiusMd,
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem<String?>(
|
||||
value: null,
|
||||
child: Text('—'),
|
||||
),
|
||||
DropdownMenuItem<String?>(
|
||||
value: 'matutino',
|
||||
child: Text('Matutino'),
|
||||
),
|
||||
DropdownMenuItem<String?>(
|
||||
value: 'vespertino',
|
||||
child: Text('Vespertino'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setStateDialog(() => turno = v),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: status,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Status',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppTheme.radiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: 'pendiente',
|
||||
child: Text('Pendiente'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'en_ruta',
|
||||
child: Text('En ruta'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'completada',
|
||||
child: Text('Completada'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'diferida',
|
||||
child: Text('Diferida'),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'reasignada',
|
||||
child: Text('Reasignada'),
|
||||
),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) setStateDialog(() => status = v);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
DropdownButtonFormField<int?>(
|
||||
initialValue: truckId,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Unidad asignada',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppTheme.radiusMd,
|
||||
),
|
||||
),
|
||||
),
|
||||
items: [
|
||||
const DropdownMenuItem<int?>(
|
||||
value: null,
|
||||
child: Text('Sin asignar'),
|
||||
),
|
||||
...units.map(
|
||||
(u) => DropdownMenuItem<int?>(
|
||||
value: u.id,
|
||||
child: Text('${u.displayPlate} (#${u.id})'),
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: (v) => setStateDialog(() => truckId = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
validator: (v) =>
|
||||
v == null ? 'Selecciona una unidad' : null,
|
||||
items: units.map((u) {
|
||||
return DropdownMenuItem(
|
||||
value: u.id,
|
||||
child: Text('${u.displayPlate} (#${u.id})'),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (v) =>
|
||||
setStateDialog(() => selectedUnitId = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@@ -429,31 +357,17 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
onPressed: () async {
|
||||
if (!formKey.currentState!.validate()) return;
|
||||
try {
|
||||
if (isEdit) {
|
||||
await _service.updateRoute(
|
||||
route.id,
|
||||
name: nombre.text.trim(),
|
||||
truckId: truckId,
|
||||
turno: turno,
|
||||
status: status,
|
||||
);
|
||||
} else {
|
||||
await _service.createRoute(
|
||||
id: id.text.trim(),
|
||||
name: nombre.text.trim().isEmpty
|
||||
? null
|
||||
: nombre.text.trim(),
|
||||
truckId: truckId,
|
||||
turno: turno,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
await _service.updateRoute(
|
||||
route.id,
|
||||
truckId: selectedUnitId,
|
||||
status: 'reasignada',
|
||||
);
|
||||
if (ctx.mounted) Navigator.pop(ctx, true);
|
||||
} catch (e) {
|
||||
_snack('Error: ${_errMsg(e)}', error: true);
|
||||
}
|
||||
},
|
||||
child: const Text('Guardar'),
|
||||
child: const Text('Confirmar'),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -464,7 +378,7 @@ class _AdminScreenState extends ConsumerState<AdminScreen>
|
||||
|
||||
if (saved == true) {
|
||||
ref.invalidate(adminRoutesProvider);
|
||||
_snack(isEdit ? 'Ruta actualizada' : 'Ruta creada');
|
||||
_snack('Ruta reasignada exitosamente');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -810,39 +724,28 @@ class _RoutesTab extends ConsumerWidget {
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
if (unit != null &&
|
||||
(unit.status == 'inactive' ||
|
||||
unit.status == 'maintenance') &&
|
||||
r.turno?.toLowerCase() == 'vespertino') ...[
|
||||
const SizedBox(height: 12),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FilledButton.icon(
|
||||
onPressed: () {
|
||||
final state = context
|
||||
.findAncestorStateOfType<_AdminScreenState>();
|
||||
state?._showRouteForm(route: r);
|
||||
state?._showReassignRoute(r);
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined, size: 18),
|
||||
label: const Text('Editar'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
TextButton.icon(
|
||||
onPressed: () => _confirmAndDelete(
|
||||
context,
|
||||
tipo: 'ruta',
|
||||
onConfirm: () async {
|
||||
await ref
|
||||
.read(adminServiceProvider)
|
||||
.deleteRoute(r.id);
|
||||
ref.invalidate(adminRoutesProvider);
|
||||
},
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
label: const Text('Eliminar'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppTheme.danger,
|
||||
icon: const Icon(Icons.swap_horiz, size: 18),
|
||||
label: const Text('Reasignar unidad'),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: AppTheme.primary,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -869,6 +772,39 @@ class _RoutesTab extends ConsumerWidget {
|
||||
}
|
||||
|
||||
// ── Tab Unidades ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Llenado estático de conductores por unidad (placeholder mientras no haya
|
||||
/// registros reales en la tabla `drivers`). Se usa como fallback en la
|
||||
/// UnitCard cuando `adminDriversProvider` no devuelve un driver asignado.
|
||||
const Map<int, String> _staticDriversByUnit = {
|
||||
101: 'Juan Pérez Hernández',
|
||||
103: 'Miguel Ángel Reyes',
|
||||
104: 'Carlos Eduardo Vázquez',
|
||||
105: 'Roberto Sánchez Luna',
|
||||
112: 'José Antonio Ramírez',
|
||||
113: 'Luis Fernando Torres',
|
||||
};
|
||||
|
||||
/// Extrae el mensaje útil de un error de red, priorizando el `detail`
|
||||
/// devuelto por FastAPI cuando hay un 500/400.
|
||||
String _formatIncidentError(Object e) {
|
||||
if (e is DioException) {
|
||||
final status = e.response?.statusCode;
|
||||
final data = e.response?.data;
|
||||
String? detail;
|
||||
if (data is Map && data['detail'] is String) {
|
||||
detail = data['detail'] as String;
|
||||
} else if (data is String && data.isNotEmpty) {
|
||||
detail = data;
|
||||
}
|
||||
if (detail != null) {
|
||||
return status != null ? '[$status] $detail' : detail;
|
||||
}
|
||||
return 'Error de red: ${e.message ?? e.type.name}';
|
||||
}
|
||||
return e.toString();
|
||||
}
|
||||
|
||||
class _TrucksTab extends ConsumerWidget {
|
||||
const _TrucksTab();
|
||||
|
||||
@@ -941,7 +877,7 @@ class _TrucksTab extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Conductor: ${assignedDriver?.displayName ?? 'Sin asignar'}',
|
||||
'Conductor: ${assignedDriver?.displayName ?? _staticDriversByUnit[t.id] ?? 'Sin asignar'}',
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
Text(
|
||||
@@ -1104,31 +1040,34 @@ class _IncidentsSheetState extends ConsumerState<_IncidentsSheet> {
|
||||
child: async.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: AppTheme.danger,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
e.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSecondary,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
color: AppTheme.danger,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(
|
||||
adminIncidentsByUnitProvider(widget.unit.id),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_formatIncidentError(e),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
child: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.invalidate(
|
||||
adminIncidentsByUnitProvider(widget.unit.id),
|
||||
),
|
||||
child: const Text('Reintentar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (incidents) {
|
||||
@@ -1301,7 +1240,9 @@ class _IncidentCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
// ── Conductor ─────────────────────────────────────────
|
||||
if (incident.driverName != null) ...[
|
||||
if ((incident.driverName ??
|
||||
_staticDriversByUnit[incident.unitId]) !=
|
||||
null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Row(
|
||||
children: [
|
||||
@@ -1313,7 +1254,8 @@ class _IncidentCard extends StatelessWidget {
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
incident.driverName!,
|
||||
incident.driverName ??
|
||||
_staticDriversByUnit[incident.unitId]!,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
Reference in New Issue
Block a user