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 createState() => _AdminScreenState(); } class _AdminScreenState extends State with SingleTickerProviderStateMixin { late final TabController _tabController; int _activeTab = 0; final List _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 _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 _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(); 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(); 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(); 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( 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( 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'), ), ], ), ); } }