Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> implementacion de login, vistas, correcion de errores en vista registro, domicilios
422 lines
20 KiB
Dart
422 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../core/widgets/app_widgets.dart';
|
|
|
|
// ── Modelos locales ───────────────────────────────────────────────────────────
|
|
enum TruckStatus { disponible, enRuta, mantenimiento, detenido }
|
|
|
|
extension TruckStatusX on TruckStatus {
|
|
String get label => switch (this) {
|
|
TruckStatus.disponible => 'Disponible',
|
|
TruckStatus.enRuta => 'En ruta',
|
|
TruckStatus.mantenimiento => 'Mantenimiento',
|
|
TruckStatus.detenido => 'Detenido',
|
|
};
|
|
|
|
AppStatusBadge get badge => switch (this) {
|
|
TruckStatus.disponible => AppStatusBadge.green(label),
|
|
TruckStatus.enRuta => AppStatusBadge.amber(label),
|
|
TruckStatus.mantenimiento => AppStatusBadge.gray(label),
|
|
TruckStatus.detenido => AppStatusBadge.gray(label),
|
|
};
|
|
}
|
|
|
|
class _AdminUser {
|
|
final String id, nombre, apellido, email, 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}) =>
|
|
_AdminUser(id: id, nombre: nombre ?? this.nombre, apellido: apellido ?? this.apellido, email: email ?? this.email, telefono: telefono ?? this.telefono);
|
|
}
|
|
|
|
class _AdminRoute {
|
|
final String id, nombre, 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}) =>
|
|
_AdminRoute(id: id, nombre: nombre ?? this.nombre, zona: zona ?? this.zona, activa: activa ?? this.activa);
|
|
}
|
|
|
|
class _AdminTruck {
|
|
final String id, placas, modelo, conductor, rutaId;
|
|
final TruckStatus status;
|
|
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}) =>
|
|
_AdminTruck(id: id, placas: placas ?? this.placas, modelo: modelo ?? this.modelo, conductor: conductor ?? this.conductor, status: status ?? this.status, rutaId: rutaId ?? this.rutaId);
|
|
}
|
|
|
|
// ── Pantalla ──────────────────────────────────────────────────────────────────
|
|
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: 'u-01', nombre: 'Laura', apellido: 'Gómez', email: 'laura@recolecta.com', telefono: '+52 461 987 1234'),
|
|
const _AdminUser(id: 'u-02', nombre: 'Miguel', apellido: 'Sánchez', email: 'miguel@recolecta.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: 't-01', placas: 'GTO-101', modelo: 'Volvo FH', conductor: 'Javier Pérez', status: TruckStatus.enRuta, rutaId: 'RUTA-01'),
|
|
const _AdminTruck(id: 't-02', placas: 'GTO-103', 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) {
|
|
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: Colors.white,
|
|
labelColor: Colors.white,
|
|
unselectedLabelColor: Colors.white70,
|
|
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();
|
|
},
|
|
backgroundColor: AppTheme.primary,
|
|
label: Text(_activeTab == 0 ? 'Nuevo usuario' : _activeTab == 1 ? 'Nueva ruta' : 'Nuevo camión'),
|
|
icon: const Icon(Icons.add),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Tab usuarios ────────────────────────────────────────────────────────────
|
|
Widget _buildUsersTab() {
|
|
if (_usuarios.isEmpty) return _emptyState('No hay usuarios registrados.');
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _usuarios.length,
|
|
separatorBuilder: (_, i) => const SizedBox(height: 12),
|
|
itemBuilder: (context, i) {
|
|
final u = _usuarios[i];
|
|
return AppCard(
|
|
child: Row(
|
|
children: [
|
|
CircleAvatar(
|
|
backgroundColor: AppTheme.primaryLight,
|
|
foregroundColor: AppTheme.primary,
|
|
child: Text(u.iniciales),
|
|
),
|
|
const SizedBox(width: 14),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(u.nombreCompleto, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
|
|
const SizedBox(height: 4),
|
|
Text(u.email, style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
|
Text(u.telefono, style: const TextStyle(fontSize: 13)),
|
|
],
|
|
),
|
|
),
|
|
IconButton(icon: const Icon(Icons.edit_outlined, color: AppTheme.primary), onPressed: () => _showUserForm(user: u)),
|
|
IconButton(icon: const Icon(Icons.delete_outline, color: AppTheme.danger), onPressed: () => _confirmDelete('usuario', () => setState(() => _usuarios.removeWhere((x) => x.id == u.id)))),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ── Tab rutas ───────────────────────────────────────────────────────────────
|
|
Widget _buildRoutesTab() {
|
|
if (_rutas.isEmpty) return _emptyState('No hay rutas registradas.');
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _rutas.length,
|
|
separatorBuilder: (_, i) => const SizedBox(height: 12),
|
|
itemBuilder: (context, i) {
|
|
final r = _rutas[i];
|
|
return AppCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(child: Text(r.nombre, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600))),
|
|
r.activa ? AppStatusBadge.green('Activa') : AppStatusBadge.gray('Inactiva'),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(r.zona, style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton.icon(onPressed: () => _showRouteForm(route: r), icon: const Icon(Icons.edit_outlined, size: 18), label: const Text('Editar')),
|
|
const SizedBox(width: 8),
|
|
TextButton.icon(
|
|
onPressed: () => _confirmDelete('ruta', () => setState(() => _rutas.removeWhere((x) => x.id == r.id))),
|
|
icon: const Icon(Icons.delete_outline, size: 18),
|
|
label: const Text('Eliminar'),
|
|
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
// ── Tab camiones ────────────────────────────────────────────────────────────
|
|
Widget _buildTrucksTab() {
|
|
if (_camiones.isEmpty) return _emptyState('No hay camiones registrados.');
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _camiones.length,
|
|
separatorBuilder: (_, i) => const SizedBox(height: 12),
|
|
itemBuilder: (context, i) {
|
|
final t = _camiones[i];
|
|
final ruta = _rutas.firstWhere((r) => r.id == t.rutaId, orElse: () => const _AdminRoute(id: '', nombre: 'Sin ruta', zona: ''));
|
|
return AppCard(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(child: Text(t.placas, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600))),
|
|
t.status.badge,
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text('${t.modelo} · ${t.conductor}', style: const TextStyle(fontSize: 13)),
|
|
Text('Ruta: ${ruta.nombre}', style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary)),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton.icon(onPressed: () => _showTruckForm(truck: t), icon: const Icon(Icons.edit_outlined, size: 18), label: const Text('Editar')),
|
|
const SizedBox(width: 8),
|
|
TextButton.icon(
|
|
onPressed: () => _confirmDelete('camión', () => setState(() => _camiones.removeWhere((x) => x.id == t.id))),
|
|
icon: const Icon(Icons.delete_outline, size: 18),
|
|
label: const Text('Eliminar'),
|
|
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _emptyState(String msg) => Center(child: Padding(padding: const EdgeInsets.all(24), child: Text(msg, textAlign: TextAlign.center, style: const TextStyle(fontSize: 15, color: AppTheme.textSecondary))));
|
|
|
|
// ── Confirmación de borrado ─────────────────────────────────────────────────
|
|
void _confirmDelete(String tipo, VoidCallback onConfirm) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
backgroundColor: AppTheme.surface,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
|
|
title: Text('Eliminar $tipo'),
|
|
content: Text('¿Deseas eliminar este $tipo?'),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: AppTheme.textSecondary), child: const Text('Cancelar')),
|
|
TextButton(
|
|
onPressed: () { onConfirm(); Navigator.pop(ctx); },
|
|
style: TextButton.styleFrom(foregroundColor: AppTheme.danger),
|
|
child: const Text('Eliminar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Formulario usuario ──────────────────────────────────────────────────────
|
|
void _showUserForm({_AdminUser? user}) {
|
|
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: SingleChildScrollView(
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
TextField(controller: nombreCtrl, decoration: const InputDecoration(labelText: 'Nombre')),
|
|
TextField(controller: apellidoCtrl, decoration: const InputDecoration(labelText: 'Apellido')),
|
|
TextField(controller: emailCtrl, decoration: const InputDecoration(labelText: 'Correo'), keyboardType: TextInputType.emailAddress),
|
|
TextField(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: () {
|
|
final nuevo = _AdminUser(id: user?.id ?? 'u-${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 idx = _usuarios.indexWhere((x) => x.id == user.id); if (idx >= 0) _usuarios[idx] = nuevo; }
|
|
});
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: Text(user == null ? 'Crear' : 'Guardar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Formulario ruta ─────────────────────────────────────────────────────────
|
|
void _showRouteForm({_AdminRoute? route}) {
|
|
final nombreCtrl = TextEditingController(text: route?.nombre);
|
|
final zonaCtrl = TextEditingController(text: route?.zona);
|
|
bool activa = route?.activa ?? true;
|
|
showDialog(
|
|
context: context,
|
|
builder: (ctx) => StatefulBuilder(
|
|
builder: (ctx, setInner) => AlertDialog(
|
|
backgroundColor: AppTheme.surface,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
|
|
title: Text(route == null ? 'Nueva ruta' : 'Editar ruta'),
|
|
content: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
TextField(controller: nombreCtrl, decoration: const InputDecoration(labelText: 'Nombre de ruta')),
|
|
TextField(controller: zonaCtrl, decoration: const InputDecoration(labelText: 'Zona')),
|
|
Row(children: [
|
|
const Expanded(child: Text('Ruta activa')),
|
|
Switch.adaptive(value: activa, onChanged: (v) => setInner(() => activa = v)),
|
|
]),
|
|
]),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: AppTheme.textSecondary), child: const Text('Cancelar')),
|
|
TextButton(
|
|
onPressed: () {
|
|
final nueva = _AdminRoute(id: route?.id ?? 'r-${DateTime.now().millisecondsSinceEpoch}', nombre: nombreCtrl.text.trim(), zona: zonaCtrl.text.trim(), activa: activa);
|
|
setState(() {
|
|
if (route == null) { _rutas.add(nueva); }
|
|
else { final idx = _rutas.indexWhere((x) => x.id == route.id); if (idx >= 0) _rutas[idx] = nueva; }
|
|
});
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: Text(route == null ? 'Crear' : 'Guardar'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ── Formulario camión ───────────────────────────────────────────────────────
|
|
void _showTruckForm({_AdminTruck? truck}) {
|
|
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) => StatefulBuilder(
|
|
builder: (ctx, setInner) => AlertDialog(
|
|
backgroundColor: AppTheme.surface,
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppTheme.radiusLg)),
|
|
title: Text(truck == null ? 'Nuevo camión' : 'Editar camión'),
|
|
content: SingleChildScrollView(
|
|
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
TextField(controller: placasCtrl, decoration: const InputDecoration(labelText: 'Placas')),
|
|
TextField(controller: modeloCtrl, decoration: const InputDecoration(labelText: 'Modelo')),
|
|
TextField(controller: conductorCtrl, decoration: const InputDecoration(labelText: 'Conductor')),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<String>(
|
|
value: selectedRuta.isEmpty ? null : selectedRuta,
|
|
decoration: const InputDecoration(labelText: 'Ruta'),
|
|
items: _rutas.map((r) => DropdownMenuItem(value: r.id, child: Text(r.nombre))).toList(),
|
|
onChanged: (v) { if (v != null) setInner(() => selectedRuta = v); },
|
|
),
|
|
const SizedBox(height: 12),
|
|
DropdownButtonFormField<TruckStatus>(
|
|
value: status,
|
|
decoration: const InputDecoration(labelText: 'Estatus'),
|
|
items: TruckStatus.values.map((s) => DropdownMenuItem(value: s, child: Text(s.label))).toList(),
|
|
onChanged: (v) { if (v != null) setInner(() => status = v); },
|
|
),
|
|
]),
|
|
),
|
|
actions: [
|
|
TextButton(onPressed: () => Navigator.pop(ctx), style: TextButton.styleFrom(foregroundColor: AppTheme.textSecondary), child: const Text('Cancelar')),
|
|
TextButton(
|
|
onPressed: () {
|
|
final nuevo = _AdminTruck(id: truck?.id ?? 't-${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 idx = _camiones.indexWhere((x) => x.id == truck.id); if (idx >= 0) _camiones[idx] = nuevo; }
|
|
});
|
|
Navigator.pop(ctx);
|
|
},
|
|
child: Text(truck == null ? 'Crear' : 'Guardar'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|