Co-authored-by: MENDOZA BALLARDO GAEL RICARDO <gael-meb123@users.noreply.github.com>
Co-authored-by: Azareth-Tr <Azareth-Tr@users.noreply.github.com> Co-authored-by: eddgranados12 <eddgranados12@users.noreply.github.com> vistas de mockup
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../core/network/api_client.dart';
|
||||
import '../core/models/auth_state.dart';
|
||||
import '../core/services/auth_controller.dart';
|
||||
import '../core/storage/secure_storage.dart';
|
||||
import 'bootstrap.dart' as bootstrap;
|
||||
@@ -12,27 +13,28 @@ import '../features/auth/register_page.dart';
|
||||
import '../features/addresses/new_address_page.dart';
|
||||
|
||||
final routerProvider = Provider<GoRouter>((ref) {
|
||||
final authSnapshot = ref.watch(authControllerProvider);
|
||||
final isAuthenticated = authSnapshot.asData?.value.isAuthenticated ?? false;
|
||||
// ValueNotifier used as refreshListenable so GoRouter re-evaluates redirect
|
||||
// without recreating the router (which would unmount widgets mid-request).
|
||||
final notifier = ValueNotifier<int>(0);
|
||||
ref.listen<AsyncValue<AuthState>>(authControllerProvider, (prev, next) {
|
||||
notifier.value++;
|
||||
});
|
||||
ref.onDispose(notifier.dispose);
|
||||
|
||||
return GoRouter(
|
||||
initialLocation: '/home',
|
||||
initialLocation: '/login',
|
||||
refreshListenable: notifier,
|
||||
redirect: (context, state) {
|
||||
final authSnapshot = ref.read(authControllerProvider);
|
||||
final isAuthenticated =
|
||||
authSnapshot.asData?.value.isAuthenticated ?? false;
|
||||
final location = state.matchedLocation;
|
||||
final isAuthRoute = location == '/login' || location == '/register';
|
||||
|
||||
if (authSnapshot.isLoading) {
|
||||
return location == '/login' ? null : '/login';
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return isAuthRoute ? null : '/login';
|
||||
}
|
||||
|
||||
if (isAuthenticated && isAuthRoute) {
|
||||
return '/home';
|
||||
}
|
||||
final isAuthRoute =
|
||||
location == '/login' || location == '/register';
|
||||
|
||||
if (authSnapshot.isLoading) return null;
|
||||
if (!isAuthenticated && !isAuthRoute) return '/login';
|
||||
if (isAuthenticated && isAuthRoute) return '/home';
|
||||
return null;
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -6,7 +7,10 @@ import '../constants/auth_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
final apiClientProvider = Provider<Dio>((ref) {
|
||||
final baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000';
|
||||
final defaultBaseUrl = kIsWeb
|
||||
? 'http://localhost:8000'
|
||||
: 'http://10.0.2.2:8000';
|
||||
final baseUrl = dotenv.env['API_BASE_URL'] ?? defaultBaseUrl;
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
|
||||
final dio = Dio(
|
||||
|
||||
@@ -24,6 +24,8 @@ class AuthController extends AsyncNotifier<AuthState> {
|
||||
|
||||
Future<void> login({required String email, required String password}) async {
|
||||
state = const AsyncLoading<AuthState>();
|
||||
|
||||
try {
|
||||
final session = await ref
|
||||
.read(authServiceProvider)
|
||||
.login(email: email, password: password);
|
||||
@@ -34,6 +36,10 @@ class AuthController extends AsyncNotifier<AuthState> {
|
||||
routeId: session.routeId,
|
||||
),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
state = AsyncError<AuthState>(error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> register({
|
||||
@@ -42,6 +48,8 @@ class AuthController extends AsyncNotifier<AuthState> {
|
||||
required String password,
|
||||
}) async {
|
||||
state = const AsyncLoading<AuthState>();
|
||||
|
||||
try {
|
||||
final session = await ref
|
||||
.read(authServiceProvider)
|
||||
.register(email: email, phone: phone, password: password);
|
||||
@@ -52,6 +60,10 @@ class AuthController extends AsyncNotifier<AuthState> {
|
||||
routeId: session.routeId,
|
||||
),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
state = AsyncError<AuthState>(error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
|
||||
812
views/lib/screens/admin_screen.dart
Normal file
812
views/lib/screens/admin_screen.dart
Normal file
@@ -0,0 +1,812 @@
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../models/models.dart';
|
||||
import '../widgets/widgets.dart' as w;
|
||||
import 'admin_screen.dart';
|
||||
import 'splash_screen.dart';
|
||||
|
||||
class ProfileScreen extends StatelessWidget {
|
||||
@@ -70,6 +71,17 @@ class ProfileScreen extends StatelessWidget {
|
||||
subtitle: 'Claro',
|
||||
onTap: () {},
|
||||
),
|
||||
w.MenuTile(
|
||||
icon: Icons.admin_panel_settings_outlined,
|
||||
title: 'Panel de administración',
|
||||
subtitle: 'Gestiona usuarios, rutas y camiones',
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const AdminScreen()),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -111,9 +123,7 @@ class ProfileScreen extends StatelessWidget {
|
||||
'RutaVerde v1.0.0\nServicio de Limpia · Celaya, Gto.',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textHint,
|
||||
height: 1.6),
|
||||
fontSize: 12, color: AppTheme.textHint, height: 1.6),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -142,7 +152,8 @@ class ProfileScreen extends StatelessWidget {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
style: TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
|
||||
style:
|
||||
TextButton.styleFrom(foregroundColor: AppTheme.textSecondary),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
|
||||
Reference in New Issue
Block a user