Files
hackathon-innovaflow5.0-cdf…/views/lib/screens/admin_screen.dart
2026-05-22 20:46:14 -06:00

813 lines
25 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../widgets/widgets.dart' as w;
enum TruckStatus { disponible, enRuta, mantenimiento, detenido }
extension TruckStatusX on TruckStatus {
String get label {
switch (this) {
case TruckStatus.disponible:
return 'Disponible';
case TruckStatus.enRuta:
return 'En ruta';
case TruckStatus.mantenimiento:
return 'Mantenimiento';
case TruckStatus.detenido:
return 'Detenido';
}
}
w.StatusBadge get badge {
switch (this) {
case TruckStatus.disponible:
return w.StatusBadge.green(label);
case TruckStatus.enRuta:
return w.StatusBadge.amber(label);
case TruckStatus.mantenimiento:
return w.StatusBadge.gray(label);
case TruckStatus.detenido:
return w.StatusBadge.gray(label);
}
}
}
class AdminUser {
final String id;
final String nombre;
final String apellido;
final String email;
final String telefono;
const AdminUser({
required this.id,
required this.nombre,
required this.apellido,
required this.email,
required this.telefono,
});
String get nombreCompleto => '$nombre $apellido';
String get iniciales =>
'${nombre.isNotEmpty ? nombre[0] : ''}${apellido.isNotEmpty ? apellido[0] : ''}'
.toUpperCase();
AdminUser copyWith({
String? nombre,
String? apellido,
String? email,
String? telefono,
}) {
return AdminUser(
id: id,
nombre: nombre ?? this.nombre,
apellido: apellido ?? this.apellido,
email: email ?? this.email,
telefono: telefono ?? this.telefono,
);
}
}
class AdminRoute {
final String id;
final String nombre;
final String zona;
final bool activa;
const AdminRoute({
required this.id,
required this.nombre,
required this.zona,
this.activa = true,
});
AdminRoute copyWith({
String? nombre,
String? zona,
bool? activa,
}) {
return AdminRoute(
id: id,
nombre: nombre ?? this.nombre,
zona: zona ?? this.zona,
activa: activa ?? this.activa,
);
}
}
class AdminTruck {
final String id;
final String placas;
final String modelo;
final String conductor;
final TruckStatus status;
final String rutaId;
const AdminTruck({
required this.id,
required this.placas,
required this.modelo,
required this.conductor,
required this.status,
required this.rutaId,
});
AdminTruck copyWith({
String? placas,
String? modelo,
String? conductor,
TruckStatus? status,
String? rutaId,
}) {
return AdminTruck(
id: id,
placas: placas ?? this.placas,
modelo: modelo ?? this.modelo,
conductor: conductor ?? this.conductor,
status: status ?? this.status,
rutaId: rutaId ?? this.rutaId,
);
}
}
class AdminScreen extends StatefulWidget {
const AdminScreen({super.key});
@override
State<AdminScreen> createState() => _AdminScreenState();
}
class _AdminScreenState extends State<AdminScreen>
with SingleTickerProviderStateMixin {
late final TabController _tabController;
int _activeTab = 0;
final List<AdminUser> _usuarios = [
const AdminUser(
id: 'user-01',
nombre: 'Laura',
apellido: 'Gómez',
email: 'laura.gomez@rutaverde.com',
telefono: '+52 461 987 1234',
),
const AdminUser(
id: 'user-02',
nombre: 'Miguel',
apellido: 'Sánchez',
email: 'miguel.sanchez@rutaverde.com',
telefono: '+52 461 123 7890',
),
];
final List<AdminRoute> _rutas = [
const AdminRoute(
id: 'ruta-01',
nombre: 'Ruta Norte',
zona: 'Zona Norte',
),
const AdminRoute(
id: 'ruta-02',
nombre: 'Ruta Sur',
zona: 'Zona Sur',
activa: false,
),
];
final List<AdminTruck> _camiones = [
const AdminTruck(
id: 'truck-01',
placas: 'ABC-1234',
modelo: 'Volvo FH',
conductor: 'Javier Pérez',
status: TruckStatus.enRuta,
rutaId: 'ruta-01',
),
const AdminTruck(
id: 'truck-02',
placas: 'DEF-5678',
modelo: 'Mercedes 1830',
conductor: 'Ana Díaz',
status: TruckStatus.disponible,
rutaId: 'ruta-02',
),
];
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this)
..addListener(() {
if (_tabController.indexIsChanging) return;
setState(() => _activeTab = _tabController.index);
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppTheme.background,
appBar: AppBar(
title: const Text('Panel de administración'),
bottom: TabBar(
controller: _tabController,
indicatorColor: AppTheme.primary,
tabs: const [
Tab(text: 'Usuarios'),
Tab(text: 'Rutas'),
Tab(text: 'Camiones'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildUsersTab(),
_buildRoutesTab(),
_buildTrucksTab(),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
if (_activeTab == 0) {
_showUserForm();
} else if (_activeTab == 1) {
_showRouteForm();
} else {
_showTruckForm();
}
},
label: Text(_activeTab == 0
? 'Nuevo usuario'
: _activeTab == 1
? 'Nueva ruta'
: 'Nuevo camión'),
icon: const Icon(Icons.add),
),
);
}
Widget _buildUsersTab() {
if (_usuarios.isEmpty) {
return _buildEmptyState('No hay usuarios registrados aún.');
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _usuarios.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final user = _usuarios[index];
return w.AppCard(
child: Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryLight,
foregroundColor: AppTheme.primary,
child: Text(user.iniciales),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(user.nombreCompleto,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(user.email,
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 2),
Text(user.telefono, style: const TextStyle(fontSize: 13)),
],
),
),
IconButton(
icon: const Icon(Icons.edit_outlined, color: AppTheme.primary),
onPressed: () => _showUserForm(user: user),
),
IconButton(
icon: const Icon(Icons.delete_outline, color: AppTheme.danger),
onPressed: () => _confirmDeleteUser(user),
),
],
),
);
},
);
}
Widget _buildRoutesTab() {
if (_rutas.isEmpty) {
return _buildEmptyState('No hay rutas registradas aún.');
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _rutas.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final ruta = _rutas[index];
return w.AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(ruta.nombre,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
),
Text(ruta.activa ? 'Activa' : 'Inactiva',
style: TextStyle(
fontSize: 13,
color: ruta.activa
? AppTheme.primary
: AppTheme.textSecondary)),
],
),
const SizedBox(height: 8),
Text('Zona ${ruta.zona}',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => _showRouteForm(route: ruta),
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Editar'),
),
const SizedBox(width: 8),
TextButton.icon(
onPressed: () => _confirmDeleteRoute(ruta),
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Eliminar'),
),
],
),
],
),
);
},
);
}
Widget _buildTrucksTab() {
if (_camiones.isEmpty) {
return _buildEmptyState('No hay camiones registrados aún.');
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _camiones.length,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (context, index) {
final truck = _camiones[index];
final route = _rutas.firstWhere(
(route) => route.id == truck.rutaId,
orElse: () =>
const AdminRoute(id: 'none', nombre: 'Sin ruta', zona: ''),
);
return w.AppCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(truck.placas,
style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w600)),
),
truck.status.badge,
],
),
const SizedBox(height: 8),
Text('${truck.modelo} · ${truck.conductor}',
style: const TextStyle(fontSize: 13)),
const SizedBox(height: 4),
Text('Ruta: ${route.nombre}',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary)),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: () => _showTruckForm(truck: truck),
icon: const Icon(Icons.edit_outlined, size: 18),
label: const Text('Editar'),
),
const SizedBox(width: 8),
TextButton.icon(
onPressed: () => _confirmDeleteTruck(truck),
icon: const Icon(Icons.delete_outline, size: 18),
label: const Text('Eliminar'),
),
],
),
],
),
);
},
);
}
Widget _buildEmptyState(String message) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(message,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 15,
color: AppTheme.textSecondary,
)),
),
);
}
void _confirmDeleteUser(AdminUser user) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
title: const Text('Eliminar usuario'),
content: const Text('¿Deseas eliminar este usuario?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
setState(
() => _usuarios.removeWhere((item) => item.id == user.id));
Navigator.pop(ctx);
},
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
child: const Text('Eliminar'),
),
],
),
);
}
void _confirmDeleteRoute(AdminRoute route) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
title: const Text('Eliminar ruta'),
content: const Text('¿Deseas eliminar esta ruta?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
setState(() => _rutas.removeWhere((item) => item.id == route.id));
Navigator.pop(ctx);
},
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
child: const Text('Eliminar'),
),
],
),
);
}
void _confirmDeleteTruck(AdminTruck truck) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
title: const Text('Eliminar camión'),
content: const Text('¿Deseas eliminar este camión?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
setState(
() => _camiones.removeWhere((item) => item.id == truck.id));
Navigator.pop(ctx);
},
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
child: const Text('Eliminar'),
),
],
),
);
}
void _showUserForm({AdminUser? user}) {
final formKey = GlobalKey<FormState>();
final nombreCtrl = TextEditingController(text: user?.nombre);
final apellidoCtrl = TextEditingController(text: user?.apellido);
final emailCtrl = TextEditingController(text: user?.email);
final telefonoCtrl = TextEditingController(text: user?.telefono);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
title: Text(user == null ? 'Nuevo usuario' : 'Editar usuario'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: nombreCtrl,
decoration: const InputDecoration(labelText: 'Nombre'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: apellidoCtrl,
decoration: const InputDecoration(labelText: 'Apellido'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: emailCtrl,
decoration: const InputDecoration(labelText: 'Correo'),
keyboardType: TextInputType.emailAddress,
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: telefonoCtrl,
decoration: const InputDecoration(labelText: 'Teléfono'),
keyboardType: TextInputType.phone,
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
if (!formKey.currentState!.validate()) return;
final nuevo = AdminUser(
id: user?.id ?? 'user-${DateTime.now().millisecondsSinceEpoch}',
nombre: nombreCtrl.text.trim(),
apellido: apellidoCtrl.text.trim(),
email: emailCtrl.text.trim(),
telefono: telefonoCtrl.text.trim(),
);
setState(() {
if (user == null) {
_usuarios.add(nuevo);
} else {
final index =
_usuarios.indexWhere((item) => item.id == user.id);
if (index >= 0) _usuarios[index] = nuevo;
}
});
Navigator.pop(ctx);
},
child: Text(user == null ? 'Crear' : 'Guardar'),
),
],
),
);
}
void _showRouteForm({AdminRoute? route}) {
final formKey = GlobalKey<FormState>();
final nombreCtrl = TextEditingController(text: route?.nombre);
final zonaCtrl = TextEditingController(text: route?.zona);
bool activa = route?.activa ?? true;
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
title: Text(route == null ? 'Nueva ruta' : 'Editar ruta'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: nombreCtrl,
decoration:
const InputDecoration(labelText: 'Nombre de ruta'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: zonaCtrl,
decoration: const InputDecoration(labelText: 'Zona'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
const SizedBox(height: 12),
Row(
children: [
const Expanded(child: Text('Ruta activa')),
Switch.adaptive(
value: activa,
onChanged: (value) => setState(() {
activa = value;
}),
),
],
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
if (!formKey.currentState!.validate()) return;
final nueva = AdminRoute(
id: route?.id ??
'ruta-${DateTime.now().millisecondsSinceEpoch}',
nombre: nombreCtrl.text.trim(),
zona: zonaCtrl.text.trim(),
activa: activa,
);
setState(() {
if (route == null) {
_rutas.add(nueva);
} else {
final index =
_rutas.indexWhere((item) => item.id == route.id);
if (index >= 0) _rutas[index] = nueva;
}
});
Navigator.pop(ctx);
},
child: Text(route == null ? 'Crear' : 'Guardar'),
),
],
),
);
}
void _showTruckForm({AdminTruck? truck}) {
final formKey = GlobalKey<FormState>();
final placasCtrl = TextEditingController(text: truck?.placas);
final modeloCtrl = TextEditingController(text: truck?.modelo);
final conductorCtrl = TextEditingController(text: truck?.conductor);
TruckStatus status = truck?.status ?? TruckStatus.disponible;
String selectedRuta =
truck?.rutaId ?? (_rutas.isNotEmpty ? _rutas.first.id : '');
showDialog(
context: context,
builder: (ctx) => AlertDialog(
backgroundColor: AppTheme.surface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppTheme.radiusLg),
),
title: Text(truck == null ? 'Nuevo camión' : 'Editar camión'),
content: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: placasCtrl,
decoration: const InputDecoration(labelText: 'Placas'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: modeloCtrl,
decoration: const InputDecoration(labelText: 'Modelo'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
TextFormField(
controller: conductorCtrl,
decoration: const InputDecoration(labelText: 'Conductor'),
validator: (value) =>
value?.trim().isEmpty == true ? 'Requerido' : null,
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: selectedRuta.isEmpty ? null : selectedRuta,
decoration: const InputDecoration(labelText: 'Ruta'),
items: _rutas
.map((ruta) => DropdownMenuItem(
value: ruta.id,
child: Text(ruta.nombre),
))
.toList(),
onChanged: (value) {
if (value != null) {
selectedRuta = value;
}
},
validator: (value) =>
value == null || value.isEmpty ? 'Requerido' : null,
),
const SizedBox(height: 12),
DropdownButtonFormField<TruckStatus>(
value: status,
decoration: const InputDecoration(labelText: 'Estatus'),
items: TruckStatus.values
.map((item) => DropdownMenuItem(
value: item,
child: Text(item.label),
))
.toList(),
onChanged: (value) {
if (value != null) {
status = value;
}
},
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
style:
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
child: const Text('Cancelar'),
),
TextButton(
onPressed: () {
if (!formKey.currentState!.validate()) return;
final nuevo = AdminTruck(
id: truck?.id ??
'truck-${DateTime.now().millisecondsSinceEpoch}',
placas: placasCtrl.text.trim(),
modelo: modeloCtrl.text.trim(),
conductor: conductorCtrl.text.trim(),
status: status,
rutaId: selectedRuta,
);
setState(() {
if (truck == null) {
_camiones.add(nuevo);
} else {
final index =
_camiones.indexWhere((item) => item.id == truck.id);
if (index >= 0) _camiones[index] = nuevo;
}
});
Navigator.pop(ctx);
},
child: Text(truck == null ? 'Crear' : 'Guardar'),
),
],
),
);
}
}