Files
hackathon-innovaflow5.0-cdf…/recolecta_app/lib/features/admin/admin_screen.dart
2026-05-23 04:30:48 -06:00

1083 lines
38 KiB
Dart

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<AdminScreen> createState() => _AdminScreenState();
}
class _AdminScreenState extends ConsumerState<AdminScreen>
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<void> _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<void> _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<FormState>();
final saved = await showDialog<bool>(
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<String>(
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<void> _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<FormState>();
final units = ref
.read(adminUnitsProvider)
.maybeWhen(data: (u) => u, orElse: () => <AdminUnitModel>[]);
final saved = await showDialog<bool>(
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<String?>(
initialValue: turno,
decoration: InputDecoration(
labelText: 'Turno',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
AppTheme.radiusMd,
),
),
),
items: const [
DropdownMenuItem<String?>(
value: null,
child: Text(''),
),
DropdownMenuItem<String?>(
value: 'matutino',
child: Text('Matutino'),
),
DropdownMenuItem<String?>(
value: 'vespertino',
child: Text('Vespertino'),
),
],
onChanged: (v) => setStateDialog(() => turno = v),
),
const SizedBox(height: 10),
DropdownButtonFormField<String>(
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<int?>(
initialValue: truckId,
decoration: InputDecoration(
labelText: 'Camión asignado',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
AppTheme.radiusMd,
),
),
),
items: [
const DropdownMenuItem<int?>(
value: null,
child: Text('Sin asignar'),
),
...units.map(
(u) => DropdownMenuItem<int?>(
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<void> _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<FormState>();
final saved = await showDialog<bool>(
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<String>(
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: () => <AdminUnitModel>[]);
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: () => <AdminRouteModel>[]);
final drivers = ref
.watch(adminDriversProvider)
.maybeWhen(data: (d) => d, orElse: () => <AdminDriverModel>[]);
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<void> 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'),
),
],
),
);
}
// EOF