import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/services/auth_controller.dart'; import '../../core/theme/app_theme.dart'; import '../../core/widgets/app_widgets.dart'; import 'data/admin_service.dart'; import 'models/admin_driver.dart'; import 'models/admin_route.dart'; import 'models/admin_unit.dart'; import 'models/admin_user.dart'; import 'providers/admin_providers.dart'; class AdminScreen extends ConsumerStatefulWidget { const AdminScreen({super.key}); @override ConsumerState createState() => _AdminScreenState(); } class _AdminScreenState extends ConsumerState with SingleTickerProviderStateMixin { late final TabController _tabController; int _activeTab = 0; @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(); } AdminService get _service => ref.read(adminServiceProvider); void _snack(String msg, {bool error = false}) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: error ? AppTheme.danger : AppTheme.primary, ), ); } Future _handleAdd() async { switch (_activeTab) { case 0: await _showUserForm(); break; case 1: await _showRouteForm(); break; case 2: await _showUnitForm(); break; } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppTheme.background, appBar: AppBar( title: const Text('Panel de administración'), actions: [ IconButton( tooltip: 'Refrescar', icon: const Icon(Icons.refresh), onPressed: () { ref.invalidate(adminUsersProvider); ref.invalidate(adminRoutesProvider); ref.invalidate(adminUnitsProvider); ref.invalidate(adminDriversProvider); }, ), IconButton( tooltip: 'Cerrar sesión', icon: const Icon(Icons.logout), onPressed: () async { await ref.read(authControllerProvider.notifier).logout(); if (mounted) context.go('/login'); }, ), ], 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: 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' : 'Nuevo camión', ), ), ); } // ── Formulario usuario ────────────────────────────────────────────────────── Future _showUserForm({AdminUserModel? user}) async { final isEdit = user != null; final nombre = TextEditingController(text: user?.name ?? ''); final email = TextEditingController(text: user?.email ?? ''); final telefono = TextEditingController(text: user?.phone ?? ''); final password = TextEditingController(); String role = user?.role ?? 'citizen'; final formKey = GlobalKey(); final saved = await showDialog( context: context, builder: (ctx) { return StatefulBuilder( builder: (ctx, setStateDialog) { return AlertDialog( backgroundColor: AppTheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.radiusLg), ), title: Text(isEdit ? 'Editar usuario' : 'Nuevo usuario'), content: Form( key: formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ _textField(nombre, 'Nombre', required: true), const SizedBox(height: 10), _textField( email, 'Email', keyboardType: TextInputType.emailAddress, ), const SizedBox(height: 10), if (!isEdit) ...[ _textField( telefono, 'Teléfono', keyboardType: TextInputType.phone, ), const SizedBox(height: 10), _textField( password, 'Contraseña (mín. 6)', obscure: true, required: true, validator: (v) => (v == null || v.length < 6) ? 'Mínimo 6 caracteres' : null, ), const SizedBox(height: 10), ], DropdownButtonFormField( initialValue: role, decoration: InputDecoration( labelText: 'Rol', border: OutlineInputBorder( borderRadius: BorderRadius.circular( AppTheme.radiusMd, ), ), ), items: const [ DropdownMenuItem( value: 'citizen', child: Text('Ciudadano'), ), DropdownMenuItem( value: 'driver', child: Text('Conductor'), ), DropdownMenuItem( value: 'admin', child: Text('Administrador'), ), ], onChanged: (v) { if (v != null) setStateDialog(() => role = v); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar'), ), ElevatedButton( onPressed: () async { if (!formKey.currentState!.validate()) return; try { if (isEdit) { await _service.updateUser( user.id, name: nombre.text.trim(), email: email.text.trim().isEmpty ? null : email.text.trim(), role: role, ); } else { if (email.text.trim().isEmpty && telefono.text.trim().isEmpty) { _snack('Email o teléfono es requerido', error: true); return; } await _service.createUser( name: nombre.text.trim(), password: password.text, email: email.text.trim().isEmpty ? null : email.text.trim(), phone: telefono.text.trim().isEmpty ? null : telefono.text.trim(), role: role, ); } if (ctx.mounted) Navigator.pop(ctx, true); } catch (e) { _snack('Error: ${_errMsg(e)}', error: true); } }, child: const Text('Guardar'), ), ], ); }, ); }, ); if (saved == true) { ref.invalidate(adminUsersProvider); ref.invalidate(adminDriversProvider); _snack(isEdit ? 'Usuario actualizado' : 'Usuario creado'); } } // ── Formulario ruta ───────────────────────────────────────────────────────── Future _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(); final units = ref .read(adminUnitsProvider) .maybeWhen(data: (u) => u, orElse: () => []); final saved = await showDialog( context: context, builder: (ctx) { return StatefulBuilder( builder: (ctx, setStateDialog) { return AlertDialog( backgroundColor: AppTheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.radiusLg), ), title: Text(isEdit ? 'Editar ruta' : 'Nueva ruta'), content: Form( key: formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ _textField( id, 'ID (ej. RUTA-01)', required: true, enabled: !isEdit, ), const SizedBox(height: 10), _textField(nombre, 'Nombre'), const SizedBox(height: 10), DropdownButtonFormField( initialValue: turno, decoration: InputDecoration( labelText: 'Turno', border: OutlineInputBorder( borderRadius: BorderRadius.circular( AppTheme.radiusMd, ), ), ), items: const [ DropdownMenuItem( value: null, child: Text('—'), ), DropdownMenuItem( value: 'matutino', child: Text('Matutino'), ), DropdownMenuItem( value: 'vespertino', child: Text('Vespertino'), ), ], onChanged: (v) => setStateDialog(() => turno = v), ), const SizedBox(height: 10), DropdownButtonFormField( 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( initialValue: truckId, decoration: InputDecoration( labelText: 'Camión asignado', border: OutlineInputBorder( borderRadius: BorderRadius.circular( AppTheme.radiusMd, ), ), ), items: [ const DropdownMenuItem( value: null, child: Text('Sin asignar'), ), ...units.map( (u) => DropdownMenuItem( value: u.id, child: Text('${u.displayPlate} (#${u.id})'), ), ), ], onChanged: (v) => setStateDialog(() => truckId = v), ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar'), ), ElevatedButton( 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, ); } if (ctx.mounted) Navigator.pop(ctx, true); } catch (e) { _snack('Error: ${_errMsg(e)}', error: true); } }, child: const Text('Guardar'), ), ], ); }, ); }, ); if (saved == true) { ref.invalidate(adminRoutesProvider); _snack(isEdit ? 'Ruta actualizada' : 'Ruta creada'); } } // ── Formulario camión (unit) ──────────────────────────────────────────────── Future _showUnitForm({AdminUnitModel? unit}) async { final isEdit = unit != null; final idCtrl = TextEditingController(text: unit?.id.toString() ?? ''); final plate = TextEditingController(text: unit?.plate ?? ''); String status = unit?.status ?? 'active'; final formKey = GlobalKey(); final saved = await showDialog( context: context, builder: (ctx) { return StatefulBuilder( builder: (ctx, setStateDialog) { return AlertDialog( backgroundColor: AppTheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.radiusLg), ), title: Text(isEdit ? 'Editar camión' : 'Nuevo camión'), content: Form( key: formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ _textField( idCtrl, 'ID numérico (ej. 101)', keyboardType: TextInputType.number, required: true, enabled: !isEdit, validator: (v) { if (v == null || v.trim().isEmpty) return 'Requerido'; if (int.tryParse(v) == null) return 'Debe ser numérico'; return null; }, ), const SizedBox(height: 10), _textField(plate, 'Placa'), const SizedBox(height: 10), DropdownButtonFormField( initialValue: status, decoration: InputDecoration( labelText: 'Estado', border: OutlineInputBorder( borderRadius: BorderRadius.circular( AppTheme.radiusMd, ), ), ), items: const [ DropdownMenuItem( value: 'active', child: Text('Activo'), ), DropdownMenuItem( value: 'inactive', child: Text('Inactivo'), ), DropdownMenuItem( value: 'maintenance', child: Text('Mantenimiento'), ), ], onChanged: (v) { if (v != null) setStateDialog(() => status = v); }, ), ], ), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Cancelar'), ), ElevatedButton( onPressed: () async { if (!formKey.currentState!.validate()) return; try { if (isEdit) { await _service.updateUnit( unit.id, plate: plate.text.trim().isEmpty ? null : plate.text.trim(), status: status, ); } else { await _service.createUnit( id: int.parse(idCtrl.text.trim()), plate: plate.text.trim().isEmpty ? null : plate.text.trim(), status: status, ); } if (ctx.mounted) Navigator.pop(ctx, true); } catch (e) { _snack('Error: ${_errMsg(e)}', error: true); } }, child: const Text('Guardar'), ), ], ); }, ); }, ); if (saved == true) { ref.invalidate(adminUnitsProvider); ref.invalidate(adminRoutesProvider); _snack(isEdit ? 'Camión actualizado' : 'Camión creado'); } } // ── Helpers ───────────────────────────────────────────────────────────────── Widget _textField( TextEditingController controller, String label, { TextInputType keyboardType = TextInputType.text, bool obscure = false, bool required = false, bool enabled = true, String? Function(String?)? validator, }) { return TextFormField( controller: controller, keyboardType: keyboardType, obscureText: obscure, enabled: enabled, validator: validator ?? (required ? (v) => (v == null || v.trim().isEmpty) ? 'Campo requerido' : null : null), decoration: InputDecoration( labelText: label, border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppTheme.radiusMd), ), ), ); } String _errMsg(Object e) { final s = e.toString(); return s.length > 220 ? '${s.substring(0, 220)}…' : s; } } // ── Tabs ────────────────────────────────────────────────────────────────────── class _UsersTab extends ConsumerWidget { const _UsersTab(); @override Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(adminUsersProvider); return async.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => _ErrorView( message: e.toString(), onRetry: () => ref.invalidate(adminUsersProvider), ), data: (users) { if (users.isEmpty) { return const _EmptyView('No hay usuarios registrados.'); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 16, 16, 96), itemCount: users.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, i) { final u = users[i]; return AppCard( child: Row( children: [ CircleAvatar( backgroundColor: AppTheme.primaryLight, foregroundColor: AppTheme.primary, child: Text(u.initials), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( u.displayName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), if (u.email != null && u.email!.isNotEmpty) Text( u.email!, style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), if (u.phone != null && u.phone!.isNotEmpty) Text(u.phone!, style: const TextStyle(fontSize: 13)), const SizedBox(height: 4), _roleBadge(u.role), ], ), ), IconButton( icon: const Icon( Icons.edit_outlined, color: AppTheme.primary, ), onPressed: () { final state = context .findAncestorStateOfType<_AdminScreenState>(); state?._showUserForm(user: u); }, ), IconButton( icon: const Icon( Icons.delete_outline, color: AppTheme.danger, ), onPressed: () => _confirmAndDelete( context, tipo: 'usuario', onConfirm: () async { await ref.read(adminServiceProvider).deleteUser(u.id); ref.invalidate(adminUsersProvider); ref.invalidate(adminDriversProvider); }, ), ), ], ), ); }, ); }, ); } Widget _roleBadge(String role) { switch (role) { case 'admin': return AppStatusBadge.amber('Administrador'); case 'driver': return AppStatusBadge.green('Conductor'); default: return AppStatusBadge.gray('Ciudadano'); } } } class _RoutesTab extends ConsumerWidget { const _RoutesTab(); @override Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(adminRoutesProvider); final units = ref .watch(adminUnitsProvider) .maybeWhen(data: (u) => u, orElse: () => []); return async.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => _ErrorView( message: e.toString(), onRetry: () => ref.invalidate(adminRoutesProvider), ), data: (routes) { if (routes.isEmpty) { return const _EmptyView('No hay rutas registradas.'); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 16, 16, 96), itemCount: routes.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, i) { final r = routes[i]; AdminUnitModel? unit; if (r.truckId != null) { for (final u in units) { if (u.id == r.truckId) { unit = u; break; } } } return AppCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( r.displayName, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), ), _routeStatusBadge(r.status), ], ), const SizedBox(height: 6), Text( 'ID: ${r.id}', style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), if (r.turno != null) Text( 'Turno: ${r.turno}', style: const TextStyle(fontSize: 13), ), Text( 'Camión: ${unit?.displayPlate ?? (r.truckId == null ? 'Sin asignar' : '#${r.truckId}')}', style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), Text( 'Posición actual: ${r.currentPositionId}/8', style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () { final state = context .findAncestorStateOfType<_AdminScreenState>(); state?._showRouteForm(route: 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, ), ), ], ), ], ), ); }, ); }, ); } Widget _routeStatusBadge(String status) { switch (status) { case 'en_ruta': return AppStatusBadge.amber('En ruta'); case 'completada': return AppStatusBadge.green('Completada'); case 'diferida': return AppStatusBadge.danger('Diferida'); case 'reasignada': return AppStatusBadge.amber('Reasignada'); default: return AppStatusBadge.gray('Pendiente'); } } } class _TrucksTab extends ConsumerWidget { const _TrucksTab(); @override Widget build(BuildContext context, WidgetRef ref) { final async = ref.watch(adminUnitsProvider); final routes = ref .watch(adminRoutesProvider) .maybeWhen(data: (r) => r, orElse: () => []); final drivers = ref .watch(adminDriversProvider) .maybeWhen(data: (d) => d, orElse: () => []); return async.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => _ErrorView( message: e.toString(), onRetry: () => ref.invalidate(adminUnitsProvider), ), data: (units) { if (units.isEmpty) { return const _EmptyView('No hay camiones registrados.'); } return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 16, 16, 96), itemCount: units.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, i) { final t = units[i]; AdminRouteModel? assignedRoute; for (final r in routes) { if (r.truckId == t.id) { assignedRoute = r; break; } } AdminDriverModel? assignedDriver; for (final d in drivers) { if (d.unitId == t.id) { assignedDriver = d; break; } } return AppCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( t.displayPlate, style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w600, ), ), ), _unitStatusBadge(t.status), ], ), const SizedBox(height: 6), Text( 'ID: #${t.id}', style: const TextStyle( fontSize: 12, color: AppTheme.textSecondary, ), ), Text( 'Conductor: ${assignedDriver?.displayName ?? 'Sin asignar'}', style: const TextStyle(fontSize: 13), ), Text( 'Ruta: ${assignedRoute?.displayName ?? 'Sin asignar'}', style: const TextStyle( fontSize: 13, color: AppTheme.textSecondary, ), ), const SizedBox(height: 12), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton.icon( onPressed: () { final state = context .findAncestorStateOfType<_AdminScreenState>(); state?._showUnitForm(unit: t); }, icon: const Icon(Icons.edit_outlined, size: 18), label: const Text('Editar'), ), const SizedBox(width: 8), TextButton.icon( onPressed: () => _confirmAndDelete( context, tipo: 'camión', onConfirm: () async { await ref .read(adminServiceProvider) .deleteUnit(t.id); ref.invalidate(adminUnitsProvider); ref.invalidate(adminRoutesProvider); }, ), icon: const Icon(Icons.delete_outline, size: 18), label: const Text('Eliminar'), style: TextButton.styleFrom( foregroundColor: AppTheme.danger, ), ), ], ), ], ), ); }, ); }, ); } Widget _unitStatusBadge(String status) { switch (status) { case 'inactive': return AppStatusBadge.gray('Inactivo'); case 'maintenance': return AppStatusBadge.amber('Mantenimiento'); default: return AppStatusBadge.green('Activo'); } } } // ── Shared widgets ──────────────────────────────────────────────────────────── class _EmptyView extends StatelessWidget { const _EmptyView(this.message); final String message; @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Text( message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 15, color: AppTheme.textSecondary), ), ), ); } class _ErrorView extends StatelessWidget { const _ErrorView({required this.message, required this.onRetry}); final String message; final VoidCallback onRetry; @override Widget build(BuildContext context) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: AppTheme.danger, size: 48), const SizedBox(height: 12), Text( message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: AppTheme.textSecondary), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: onRetry, icon: const Icon(Icons.refresh), label: const Text('Reintentar'), ), ], ), ), ); } void _confirmAndDelete( BuildContext context, { required String tipo, required Future Function() 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: () async { Navigator.pop(ctx); try { await onConfirm(); if (context.mounted) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text('$tipo eliminado'))); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: $e'), backgroundColor: AppTheme.danger, ), ); } } }, style: TextButton.styleFrom(foregroundColor: AppTheme.danger), child: const Text('Eliminar'), ), ], ), ); } // ── Legacy stubs (no longer used; kept enum to avoid breaking imports) ──────── enum _LegacyTruckStatus { 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 createState() => _AdminScreenState(); } class _AdminScreenState extends State 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( 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( 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'), ), ], ), ), ); } }