Solucionado el error de eliminacion del componente

This commit is contained in:
Abraham
2025-07-25 14:10:28 -07:00
parent 1d513ab1a8
commit 264987e0cc
6 changed files with 2363 additions and 226 deletions

View File

@@ -619,20 +619,89 @@ class NegociosCardsView extends StatelessWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
// Cerrar el diálogo antes de la operación asíncrona
Navigator.pop(context); Navigator.pop(context);
// Mostrar indicador de carga
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Center(
child: CircularProgressIndicator(
color: AppTheme.of(context).primaryColor,
),
),
);
}
try {
final success = await provider.eliminarNegocio(negocio.id); final success = await provider.eliminarNegocio(negocio.id);
// Cerrar indicador de carga
if (context.mounted) {
Navigator.pop(context);
}
// Mostrar resultado solo si el contexto sigue válido
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Row(
children: [
Icon(
success ? Icons.check_circle : Icons.error,
color: Colors.white,
),
const SizedBox(width: 12),
Text(
success success
? 'Sucursal eliminada correctamente' ? 'Sucursal eliminada correctamente'
: 'Error al eliminar la sucursal', : 'Error al eliminar la sucursal',
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
), ),
backgroundColor: success ? Colors.green : Colors.red, backgroundColor: success ? Colors.green : Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
), ),
); );
} }
} catch (e) {
// Cerrar indicador de carga en caso de error
if (context.mounted) {
Navigator.pop(context);
}
// Mostrar error solo si el contexto sigue válido
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Text(
'Error: $e',
style:
const TextStyle(fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
}, },
child: const Text( child: const Text(
'Eliminar', 'Eliminar',

View File

@@ -514,20 +514,90 @@ class NegociosTable extends StatelessWidget {
), ),
TextButton( TextButton(
onPressed: () async { onPressed: () async {
// Cerrar el diálogo antes de la operación asíncrona
Navigator.of(context).pop(); Navigator.of(context).pop();
// Mostrar indicador de carga
if (context.mounted) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Center(
child: CircularProgressIndicator(
color: AppTheme.of(context).primaryColor,
),
),
);
}
try {
final success = await provider.eliminarNegocio(negocioId); final success = await provider.eliminarNegocio(negocioId);
// Cerrar indicador de carga
if (context.mounted) {
Navigator.of(context).pop();
}
// Mostrar resultado solo si el contexto sigue válido
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text( content: Row(
children: [
Icon(
success ? Icons.check_circle : Icons.error,
color: Colors.white,
),
const SizedBox(width: 12),
Text(
success success
? 'Sucursal eliminada correctamente' ? 'Sucursal eliminada correctamente'
: 'Error al eliminar la sucursal', : 'Error al eliminar la sucursal',
style:
const TextStyle(fontWeight: FontWeight.w600),
),
],
), ),
backgroundColor: success ? Colors.green : Colors.red, backgroundColor: success ? Colors.green : Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
), ),
); );
} }
} catch (e) {
// Cerrar indicador de carga en caso de error
if (context.mounted) {
Navigator.of(context).pop();
}
// Mostrar error solo si el contexto sigue válido
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.warning, color: Colors.white),
const SizedBox(width: 12),
Expanded(
child: Text(
'Error: $e',
style: const TextStyle(
fontWeight: FontWeight.w600),
),
),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
}, },
child: const Text( child: const Text(
'Eliminar', 'Eliminar',

View File

@@ -4,6 +4,7 @@ import 'package:pluto_grid/pluto_grid.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart'; import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/componentes_cards_view.dart'; import 'package:nethive_neo/pages/infrastructure/widgets/componentes_cards_view.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart'; import 'package:nethive_neo/pages/infrastructure/widgets/edit_componente_dialog.dart';
import 'package:nethive_neo/pages/infrastructure/widgets/add_componente_dialog.dart';
import 'package:nethive_neo/theme/theme.dart'; import 'package:nethive_neo/theme/theme.dart';
class InventarioPage extends StatefulWidget { class InventarioPage extends StatefulWidget {
@@ -18,6 +19,10 @@ class _InventarioPageState extends State<InventarioPage>
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _fadeAnimation; late Animation<double> _fadeAnimation;
// GlobalKey para manejar el overlay de manera segura
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
OverlayEntry? _loadingOverlay;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -37,10 +42,69 @@ class _InventarioPageState extends State<InventarioPage>
@override @override
void dispose() { void dispose() {
// Limpiar el overlay si existe antes de dispose
_removeLoadingOverlay();
_animationController.dispose(); _animationController.dispose();
super.dispose(); super.dispose();
} }
// Método para mostrar overlay de loading de manera segura
void _showLoadingOverlay(String message) {
_removeLoadingOverlay(); // Remover cualquier overlay existente
if (mounted) {
_loadingOverlay = OverlayEntry(
builder: (context) => Material(
color: Colors.black.withOpacity(0.7),
child: Center(
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
);
Overlay.of(context).insert(_loadingOverlay!);
}
}
// Método para remover overlay de manera segura
void _removeLoadingOverlay() {
if (_loadingOverlay != null) {
try {
_loadingOverlay!.remove();
} catch (e) {
// Ignorar errores si el overlay ya fue removido
}
_loadingOverlay = null;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLargeScreen = MediaQuery.of(context).size.width > 1200; final isLargeScreen = MediaQuery.of(context).size.width > 1200;
@@ -135,7 +199,7 @@ class _InventarioPageState extends State<InventarioPage>
], ],
), ),
), ),
// Botón para añadir componente // Botón para añadir componente - ACTUALIZADO
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2), color: Colors.white.withOpacity(0.2),
@@ -143,10 +207,24 @@ class _InventarioPageState extends State<InventarioPage>
), ),
child: TextButton.icon( child: TextButton.icon(
onPressed: () { onPressed: () {
// TODO: Abrir dialog para añadir componente // Verificar que tengamos un negocio seleccionado
if (componentesProvider.negocioSeleccionadoId == null) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('Función de añadir componente próximamente'), content: Text(
'Debe seleccionar un negocio antes de añadir componentes'),
backgroundColor: Colors.orange,
),
);
return;
}
// Abrir el diálogo para añadir componente
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AddComponenteDialog(
provider: componentesProvider,
), ),
); );
}, },
@@ -879,64 +957,667 @@ class _InventarioPageState extends State<InventarioPage>
void _showComponentDetails(dynamic componente, ComponentesProvider provider) { void _showComponentDetails(dynamic componente, ComponentesProvider provider) {
if (componente == null) return; if (componente == null) return;
// Detectar el tamaño de pantalla
final screenSize = MediaQuery.of(context).size;
final isDesktop = screenSize.width > 1024;
final isMobile = screenSize.width <= 768;
// Obtener la URL de la imagen del componente
final imagenUrl = provider.componentesRows
.where((row) => row.cells['id']?.value == componente.id)
.firstOrNull
?.cells['imagen_url']
?.value
?.toString();
final categoria = provider.getCategoriaById(componente.categoriaId);
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( barrierDismissible: true,
backgroundColor: AppTheme.of(context).primaryBackground, builder: (context) => Dialog(
title: Row( backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(isDesktop ? 40 : 20),
child: Container(
width: isDesktop ? 900 : (isMobile ? screenSize.width * 0.95 : 700),
height: isDesktop ? 650 : (isMobile ? screenSize.height * 0.8 : 600),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 30,
offset: const Offset(0, 15),
spreadRadius: 5,
),
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
blurRadius: 40,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryBackground,
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
stops: const [0.0, 0.6, 1.0],
),
),
child: isDesktop
? _buildDesktopDetailLayout(
componente, provider, categoria, imagenUrl)
: _buildMobileDetailLayout(
componente, provider, categoria, imagenUrl),
),
),
),
),
);
}
Widget _buildDesktopDetailLayout(
dynamic componente,
ComponentesProvider provider,
dynamic categoria,
String? imagenUrl,
) {
return Row(
children: [ children: [
Icon( // Panel izquierdo con imagen espectacular
Container(
width: 350,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
AppTheme.of(context).tertiaryColor,
],
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.4),
blurRadius: 20,
offset: const Offset(5, 0),
),
],
),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Imagen principal del componente - MÁS GRANDE
Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: RadialGradient(
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1),
Colors.transparent,
],
),
border: Border.all(
color: Colors.white.withOpacity(0.4),
width: 3,
),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.3),
blurRadius: 30,
spreadRadius: 10,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(17),
child: imagenUrl != null && imagenUrl.isNotEmpty
? Image.network(
imagenUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(17),
),
child: Center(
child: CircularProgressIndicator(
color: Colors.white,
value: loadingProgress.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (context, error, stackTrace) {
return Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(17),
),
child: const Icon(
Icons.devices, Icons.devices,
color: AppTheme.of(context).primaryColor, color: Colors.white,
size: 80,
),
);
},
)
: Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(17),
),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 80,
),
),
),
),
const SizedBox(height: 24),
// Título del componente
Text(
componente.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// Categoría con estilo
if (categoria != null)
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.category,
color: Colors.white,
size: 16,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Text(
categoria.nombre,
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
const SizedBox(height: 20),
// Estados con iconos
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildStatusIndicator(
componente.activo ? 'Activo' : 'Inactivo',
componente.activo ? Icons.check_circle : Icons.cancel,
componente.activo ? Colors.green : Colors.red,
),
_buildStatusIndicator(
componente.enUso ? 'En Uso' : 'Libre',
componente.enUso
? Icons.trending_up
: Icons.trending_flat,
componente.enUso ? Colors.orange : Colors.grey,
),
],
),
const SizedBox(height: 20),
// ID con estilo
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text( child: Text(
componente.nombre, 'ID: ${componente.id.substring(0, 8)}...',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 12,
fontFamily: 'monospace',
),
),
),
],
),
),
),
// Panel derecho con detalles
Expanded(
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header del panel de detalles
Row(
children: [
Icon(
Icons.info_outline,
color: AppTheme.of(context).primaryColor,
size: 24,
),
const SizedBox(width: 12),
Text(
'Detalles del Componente',
style: TextStyle( style: TextStyle(
color: AppTheme.of(context).primaryText, color: AppTheme.of(context).primaryText,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
color: AppTheme.of(context).secondaryText,
),
style: IconButton.styleFrom(
backgroundColor:
AppTheme.of(context).secondaryBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
const SizedBox(height: 24),
// Información detallada
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
if (componente.ubicacion != null &&
componente.ubicacion!.isNotEmpty)
_buildEnhancedDetailCard(
'Ubicación',
componente.ubicacion!,
Icons.location_on,
Colors.blue,
),
if (componente.descripcion != null &&
componente.descripcion!.isNotEmpty)
_buildEnhancedDetailCard(
'Descripción',
componente.descripcion!,
Icons.description,
Colors.purple,
),
_buildEnhancedDetailCard(
'Fecha de Registro',
componente.fechaRegistro?.toString().split(' ')[0] ??
'No disponible',
Icons.calendar_today,
Colors.green,
),
_buildEnhancedDetailCard(
'Estado Operativo',
componente.activo
? 'Componente activo y operativo'
: 'Componente inactivo',
componente.activo
? Icons.power_settings_new
: Icons.power_off,
componente.activo ? Colors.green : Colors.red,
),
_buildEnhancedDetailCard(
'Estado de Uso',
componente.enUso
? 'Componente en uso actual'
: 'Componente disponible para uso',
componente.enUso ? Icons.work : Icons.work_off,
componente.enUso ? Colors.orange : Colors.grey,
),
],
),
),
),
// Botones de acción
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_editComponent(componente, provider);
},
icon: const Icon(Icons.edit, size: 18),
label: const Text('Editar'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, size: 18),
label: const Text('Cerrar'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.of(context).secondaryText,
side: BorderSide(
color: AppTheme.of(context)
.secondaryText
.withOpacity(0.5),
),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
],
),
],
),
),
),
],
);
}
Widget _buildMobileDetailLayout(
dynamic componente,
ComponentesProvider provider,
dynamic categoria,
String? imagenUrl,
) {
return Column(
children: [
// Header con imagen para móvil
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
AppTheme.of(context).tertiaryColor,
],
),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Detalles',
style: TextStyle(
color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
), ),
], ],
), ),
content: Container( const SizedBox(height: 16),
width: double.maxFinite, // Imagen del componente en móvil
constraints: const BoxConstraints(maxHeight: 400), Container(
width: 120,
height: 120,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.white.withOpacity(0.3), width: 2),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(13),
child: imagenUrl != null && imagenUrl.isNotEmpty
? Image.network(
imagenUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.white.withOpacity(0.1),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 50,
),
);
},
)
: Container(
color: Colors.white.withOpacity(0.1),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 50,
),
),
),
),
const SizedBox(height: 12),
Text(
componente.nombre,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
if (categoria != null) ...[
const SizedBox(height: 8),
Text(
categoria.nombre,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
],
),
),
// Contenido de detalles para móvil
Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildDetailRow('ID', componente.id.substring(0, 8) + '...'), Row(
_buildDetailRow( children: [
'Categoría', Expanded(
provider.getCategoriaById(componente.categoriaId)?.nombre ?? child: _buildStatusIndicator(
'Sin categoría'), componente.activo ? 'Activo' : 'Inactivo',
_buildDetailRow( componente.activo ? Icons.check_circle : Icons.cancel,
'Estado', componente.activo ? 'Activo' : 'Inactivo'), componente.activo ? Colors.green : Colors.red,
_buildDetailRow('En Uso', componente.enUso ? '' : 'No'), ),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatusIndicator(
componente.enUso ? 'En Uso' : 'Libre',
componente.enUso
? Icons.trending_up
: Icons.trending_flat,
componente.enUso ? Colors.orange : Colors.grey,
),
),
],
),
const SizedBox(height: 16),
if (componente.ubicacion != null && if (componente.ubicacion != null &&
componente.ubicacion!.isNotEmpty) componente.ubicacion!.isNotEmpty)
_buildDetailRow('Ubicación', componente.ubicacion!), _buildEnhancedDetailCard(
'Ubicación',
componente.ubicacion!,
Icons.location_on,
Colors.blue,
),
if (componente.descripcion != null && if (componente.descripcion != null &&
componente.descripcion!.isNotEmpty) componente.descripcion!.isNotEmpty)
_buildDetailRow('Descripción', componente.descripcion!), _buildEnhancedDetailCard(
_buildDetailRow( 'Descripción',
componente.descripcion!,
Icons.description,
Colors.purple,
),
_buildEnhancedDetailCard(
'Fecha de Registro', 'Fecha de Registro',
componente.fechaRegistro?.toString().split(' ')[0] ?? componente.fechaRegistro?.toString().split(' ')[0] ??
'No disponible'), 'No disponible',
Icons.calendar_today,
Colors.green,
),
_buildEnhancedDetailCard(
'ID del Componente',
componente.id.substring(0, 8) + '...',
Icons.fingerprint,
Colors.indigo,
),
const SizedBox(height: 20),
// Botones de acción para móvil
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
_editComponent(componente, provider);
},
icon: const Icon(Icons.edit, size: 18),
label: const Text('Editar'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.of(context).primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, size: 18),
label: const Text('Cerrar'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.of(context).secondaryText,
side: BorderSide(
color: AppTheme.of(context)
.secondaryText
.withOpacity(0.5),
),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
),
],
),
], ],
), ),
), ),
), ),
actions: [ ],
TextButton( );
onPressed: () => Navigator.of(context).pop(), }
child: Text(
'Cerrar', Widget _buildStatusIndicator(String text, IconData icon, Color color) {
style: TextStyle(color: AppTheme.of(context).primaryColor), return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: color, size: 16),
const SizedBox(width: 6),
Text(
text,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.bold,
), ),
), ),
], ],
@@ -944,39 +1625,68 @@ class _InventarioPageState extends State<InventarioPage>
); );
} }
Widget _buildDetailRow(String label, String value) { Widget _buildEnhancedDetailCard(
String title, String value, IconData icon, Color color) {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppTheme.of(context).secondaryBackground, gradient: LinearGradient(
borderRadius: BorderRadius.circular(8), begin: Alignment.topLeft,
border: Border.all( end: Alignment.bottomRight,
color: AppTheme.of(context).primaryColor.withOpacity(0.2), colors: [
AppTheme.of(context).secondaryBackground,
AppTheme.of(context).tertiaryBackground,
],
), ),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: color.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox( Container(
width: 100, padding: const EdgeInsets.all(8),
child: Text( decoration: BoxDecoration(
label, color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle( style: TextStyle(
color: AppTheme.of(context).primaryColor, color: color,
fontSize: 12, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
), const SizedBox(height: 4),
Expanded( Text(
child: Text(
value, value,
style: TextStyle( style: TextStyle(
color: AppTheme.of(context).primaryText, color: AppTheme.of(context).primaryText,
fontSize: 12, fontSize: 13,
height: 1.4,
), ),
), ),
],
),
), ),
], ],
), ),
@@ -1034,27 +1744,29 @@ class _InventarioPageState extends State<InventarioPage>
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
// Cerrar el diálogo de confirmación
Navigator.of(context).pop(); Navigator.of(context).pop();
// Mostrar indicador de carga // Capturar el ScaffoldMessenger antes de la operación asíncrona
showDialog( final scaffoldMessenger = ScaffoldMessenger.of(context);
context: context,
barrierDismissible: false,
builder: (context) => Center(
child: CircularProgressIndicator(
color: AppTheme.of(context).primaryColor,
),
),
);
try { try {
// Mostrar loading de manera segura
_showLoadingOverlay('Eliminando componente...');
// Realizar la eliminación
final success = final success =
await provider.eliminarComponente(componente.id); await provider.eliminarComponente(componente.id);
Navigator.of(context).pop(); // Cerrar indicador de carga // Remover loading de manera segura
_removeLoadingOverlay();
// Verificar que el widget sigue montado antes de mostrar mensajes
if (!mounted) return;
// Mostrar resultado usando el ScaffoldMessenger capturado
if (success) { if (success) {
ScaffoldMessenger.of(context).showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: const [ children: const [
@@ -1071,10 +1783,11 @@ class _InventarioPageState extends State<InventarioPage>
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
duration: const Duration(seconds: 3),
), ),
); );
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: const [ children: const [
@@ -1091,12 +1804,19 @@ class _InventarioPageState extends State<InventarioPage>
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
duration: const Duration(seconds: 4),
), ),
); );
} }
} catch (e) { } catch (e) {
Navigator.of(context).pop(); // Cerrar indicador de carga // Asegurar que el overlay se remueva en caso de error
ScaffoldMessenger.of(context).showSnackBar( _removeLoadingOverlay();
// Verificar que el widget sigue montado antes de mostrar error
if (!mounted) return;
// Mostrar error usando el ScaffoldMessenger capturado
scaffoldMessenger.showSnackBar(
SnackBar( SnackBar(
content: Row( content: Row(
children: [ children: [
@@ -1115,6 +1835,7 @@ class _InventarioPageState extends State<InventarioPage>
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
duration: const Duration(seconds: 4),
), ),
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -62,10 +61,26 @@ class ComponentesProvider extends ChangeNotifier {
DetalleRouterFirewall? detalleRouterFirewall; DetalleRouterFirewall? detalleRouterFirewall;
DetalleEquipoActivo? detalleEquipoActivo; DetalleEquipoActivo? detalleEquipoActivo;
// Variable para controlar si el provider está activo
bool _isDisposed = false;
ComponentesProvider() { ComponentesProvider() {
getCategorias(); getCategorias();
} }
@override
void dispose() {
_isDisposed = true;
super.dispose();
}
// Método seguro para notificar listeners
void _safeNotifyListeners() {
if (!_isDisposed) {
notifyListeners();
}
}
// Métodos para categorías // Métodos para categorías
Future<void> getCategorias([String? busqueda]) async { Future<void> getCategorias([String? busqueda]) async {
try { try {
@@ -82,7 +97,7 @@ class ComponentesProvider extends ChangeNotifier {
.toList(); .toList();
_buildCategoriasRows(); _buildCategoriasRows();
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en getCategorias: ${e.toString()}'); print('Error en getCategorias: ${e.toString()}');
} }
@@ -122,7 +137,7 @@ class ComponentesProvider extends ChangeNotifier {
.toList(); .toList();
_buildComponentesRows(); _buildComponentesRows();
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en getComponentesPorNegocio: ${e.toString()}'); print('Error en getComponentesPorNegocio: ${e.toString()}');
} }
@@ -177,7 +192,7 @@ class ComponentesProvider extends ChangeNotifier {
imagenToUpload = picker.files.single.bytes; imagenToUpload = picker.files.single.bytes;
} }
notifyListeners(); _safeNotifyListeners();
} }
Future<String?> uploadImagen() async { Future<String?> uploadImagen() async {
@@ -328,15 +343,44 @@ class ComponentesProvider extends ChangeNotifier {
Future<bool> eliminarComponente(String componenteId) async { Future<bool> eliminarComponente(String componenteId) async {
try { try {
// Primero obtener la información del componente para obtener la URL de la imagen
final componenteData = await supabaseLU
.from('componente')
.select('imagen_url')
.eq('id', componenteId)
.maybeSingle();
// Guardar la URL de la imagen para eliminarla después
String? imagenUrl;
if (componenteData != null && componenteData['imagen_url'] != null) {
imagenUrl = componenteData['imagen_url'] as String;
}
// Eliminar todos los detalles específicos primero // Eliminar todos los detalles específicos primero
await _eliminarDetallesComponente(componenteId); await _eliminarDetallesComponente(componenteId);
// Luego eliminar el componente // Eliminar el componente de la base de datos
await supabaseLU.from('componente').delete().eq('id', componenteId); await supabaseLU.from('componente').delete().eq('id', componenteId);
if (negocioSeleccionadoId != null) { // Actualizar la lista ANTES de eliminar la imagen
if (!_isDisposed && negocioSeleccionadoId != null) {
await getComponentesPorNegocio(negocioSeleccionadoId!); await getComponentesPorNegocio(negocioSeleccionadoId!);
} }
// AHORA eliminar la imagen del storage (después de que la UI se haya actualizado)
if (imagenUrl != null) {
try {
await supabaseLU.storage
.from('nethive')
.remove(["componentes/$imagenUrl"]);
print('Imagen eliminada del storage: $imagenUrl');
} catch (storageError) {
print(
'Error al eliminar imagen del storage: ${storageError.toString()}');
// No retornamos false aquí porque el componente ya fue eliminado exitosamente
}
}
return true; return true;
} catch (e) { } catch (e) {
print('Error en eliminarComponente: ${e.toString()}'); print('Error en eliminarComponente: ${e.toString()}');
@@ -344,42 +388,6 @@ class ComponentesProvider extends ChangeNotifier {
} }
} }
Future<void> _eliminarDetallesComponente(String componenteId) async {
// Eliminar de todas las tablas de detalles
await supabaseLU
.from('detalle_cable')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_switch')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_patch_panel')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_rack')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_organizador')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_ups')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_router_firewall')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_equipo_activo')
.delete()
.eq('componente_id', componenteId);
}
// Métodos para obtener detalles específicos // Métodos para obtener detalles específicos
Future<void> getDetallesComponente( Future<void> getDetallesComponente(
String componenteId, int categoriaId) async { String componenteId, int categoriaId) async {
@@ -408,7 +416,7 @@ class ComponentesProvider extends ChangeNotifier {
} }
showDetallesEspecificos = true; showDetallesEspecificos = true;
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en getDetallesComponente: ${e.toString()}'); print('Error en getDetallesComponente: ${e.toString()}');
} }
@@ -535,7 +543,7 @@ class ComponentesProvider extends ChangeNotifier {
detalleRouterFirewall = null; detalleRouterFirewall = null;
detalleEquipoActivo = null; detalleEquipoActivo = null;
notifyListeners(); _safeNotifyListeners();
} }
void buscarComponentes(String busqueda) { void buscarComponentes(String busqueda) {
@@ -687,7 +695,7 @@ class ComponentesProvider extends ChangeNotifier {
// Cargar toda la información de topología para este negocio // Cargar toda la información de topología para este negocio
await cargarTopologiaCompleta(negocioId); await cargarTopologiaCompleta(negocioId);
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en setNegocioSeleccionado: ${e.toString()}'); print('Error en setNegocioSeleccionado: ${e.toString()}');
} }
@@ -714,7 +722,7 @@ class ComponentesProvider extends ChangeNotifier {
// Cargar toda la información de topología de forma optimizada // Cargar toda la información de topología de forma optimizada
Future<void> cargarTopologiaCompleta(String negocioId) async { Future<void> cargarTopologiaCompleta(String negocioId) async {
isLoadingTopologia = true; isLoadingTopologia = true;
notifyListeners(); _safeNotifyListeners();
try { try {
// Cargar datos en paralelo para mejor performance // Cargar datos en paralelo para mejor performance
@@ -733,7 +741,7 @@ class ComponentesProvider extends ChangeNotifier {
]; ];
} finally { } finally {
isLoadingTopologia = false; isLoadingTopologia = false;
notifyListeners(); _safeNotifyListeners();
} }
} }
@@ -952,73 +960,6 @@ class ComponentesProvider extends ChangeNotifier {
} }
} }
// Obtener switches principales (core/distribución)
List<Componente> getSwitchesPrincipales() {
return componentes.where((c) {
final categoria = getCategoriaById(c.categoriaId);
final isSwitch =
categoria?.nombre?.toLowerCase().contains('switch') ?? false;
final isCore = c.ubicacion?.toLowerCase().contains('mdf') ?? false;
return isSwitch && isCore;
}).toList();
}
// Obtener routers/firewalls
List<Componente> getRoutersFirewalls() {
return componentes.where((c) {
final categoria = getCategoriaById(c.categoriaId);
final nombre = categoria?.nombre?.toLowerCase() ?? '';
return nombre.contains('router') || nombre.contains('firewall');
}).toList();
}
// Obtener servidores
List<Componente> getServidores() {
return componentes.where((c) {
final categoria = getCategoriaById(c.categoriaId);
final nombre = categoria?.nombre?.toLowerCase() ?? '';
return nombre.contains('servidor') || nombre.contains('server');
}).toList();
}
// Obtener cables por tipo
List<Componente> getCablesPorTipo(String tipoCable) {
return componentes.where((c) {
final categoria = getCategoriaById(c.categoriaId);
final nombre = categoria?.nombre?.toLowerCase() ?? '';
return nombre.contains('cable') &&
(c.descripcion?.toLowerCase().contains(tipoCable.toLowerCase()) ??
false);
}).toList();
}
// Obtener estadísticas de conectividad
Map<String, int> getEstadisticasConectividad() {
int componentesActivos = componentes.where((c) => c.activo).length;
int componentesEnUso = componentes.where((c) => c.enUso).length;
int conexionesActivas = conexiones.where((c) => c.activo).length;
int totalConexiones = conexiones.length;
return {
'componentesActivos': componentesActivos,
'componentesEnUso': componentesEnUso,
'conexionesActivas': conexionesActivas,
'totalConexiones': totalConexiones,
'porcentajeUso': componentesActivos > 0
? ((componentesEnUso / componentesActivos) * 100).round()
: 0,
};
}
// Cargar toda la información de topología
Future<void> cargarTopologia(String negocioId) async {
await Future.wait([
getComponentesPorNegocio(negocioId),
getDistribucionesPorNegocio(negocioId),
getConexionesPorNegocio(negocioId),
]);
}
// Validar integridad de topología mejorado // Validar integridad de topología mejorado
List<String> validarTopologia() { List<String> validarTopologia() {
List<String> problemas = []; List<String> problemas = [];
@@ -1158,4 +1099,63 @@ class ComponentesProvider extends ChangeNotifier {
return sugerencias; return sugerencias;
} }
// Obtener estadísticas de conectividad
Map<String, double> getEstadisticasConectividad() {
final totalComponentes = componentes.length;
final componentesActivos = componentes.where((c) => c.activo).length;
final componentesEnUso = componentes.where((c) => c.enUso).length;
final conexionesActivas = conexiones.where((c) => c.activo).length;
return {
'totalComponentes': totalComponentes.toDouble(),
'componentesActivos': componentesActivos.toDouble(),
'componentesEnUso': componentesEnUso.toDouble(),
'conexionesActivas': conexionesActivas.toDouble(),
'porcentajeActivos': totalComponentes > 0
? (componentesActivos / totalComponentes) * 100
: 0,
'porcentajeUso': componentesActivos > 0
? (componentesEnUso / componentesActivos) * 100
: 0,
'densidadConexiones':
componentesActivos > 0 ? (conexionesActivas / componentesActivos) : 0,
};
}
Future<void> _eliminarDetallesComponente(String componenteId) async {
// Eliminar de todas las tablas de detalles
await supabaseLU
.from('detalle_cable')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_switch')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_patch_panel')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_rack')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_organizador')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_ups')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_router_firewall')
.delete()
.eq('componente_id', componenteId);
await supabaseLU
.from('detalle_equipo_activo')
.delete()
.eq('componente_id', componenteId);
}
} }

View File

@@ -35,10 +35,28 @@ class EmpresasNegociosProvider extends ChangeNotifier {
String? empresaSeleccionadaId; String? empresaSeleccionadaId;
Empresa? empresaSeleccionada; Empresa? empresaSeleccionada;
// Variable para controlar si el provider está activo
bool _isDisposed = false;
EmpresasNegociosProvider() { EmpresasNegociosProvider() {
getEmpresas(); getEmpresas();
} }
@override
void dispose() {
_isDisposed = true;
busquedaEmpresaController.dispose();
busquedaNegocioController.dispose();
super.dispose();
}
// Método seguro para notificar listeners
void _safeNotifyListeners() {
if (!_isDisposed) {
notifyListeners();
}
}
// Métodos para empresas // Métodos para empresas
Future<void> getEmpresas([String? busqueda]) async { Future<void> getEmpresas([String? busqueda]) async {
try { try {
@@ -56,7 +74,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
.toList(); .toList();
_buildEmpresasRows(); _buildEmpresasRows();
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en getEmpresas: ${e.toString()}'); print('Error en getEmpresas: ${e.toString()}');
} }
@@ -105,7 +123,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
.toList(); .toList();
_buildNegociosRows(); _buildNegociosRows();
notifyListeners(); _safeNotifyListeners();
} catch (e) { } catch (e) {
print('Error en getNegociosPorEmpresa: ${e.toString()}'); print('Error en getNegociosPorEmpresa: ${e.toString()}');
} }
@@ -164,7 +182,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
logoToUpload = picker.files.single.bytes; logoToUpload = picker.files.single.bytes;
// Notificar inmediatamente después de seleccionar // Notificar inmediatamente después de seleccionar
notifyListeners(); _safeNotifyListeners();
} }
} }
@@ -186,7 +204,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
imagenToUpload = picker.files.single.bytes; imagenToUpload = picker.files.single.bytes;
// Notificar inmediatamente después de seleccionar // Notificar inmediatamente después de seleccionar
notifyListeners(); _safeNotifyListeners();
} }
} }
@@ -297,7 +315,10 @@ class EmpresasNegociosProvider extends ChangeNotifier {
// Luego eliminar la empresa // Luego eliminar la empresa
await supabaseLU.from('empresa').delete().eq('id', empresaId); await supabaseLU.from('empresa').delete().eq('id', empresaId);
// Solo actualizar si el provider sigue activo
if (!_isDisposed) {
await getEmpresas(); await getEmpresas();
}
return true; return true;
} catch (e) { } catch (e) {
print('Error en eliminarEmpresa: ${e.toString()}'); print('Error en eliminarEmpresa: ${e.toString()}');
@@ -309,7 +330,8 @@ class EmpresasNegociosProvider extends ChangeNotifier {
try { try {
await supabaseLU.from('negocio').delete().eq('id', negocioId); await supabaseLU.from('negocio').delete().eq('id', negocioId);
if (empresaSeleccionadaId != null) { // Solo actualizar si el provider sigue activo y hay una empresa seleccionada
if (!_isDisposed && empresaSeleccionadaId != null) {
await getNegociosPorEmpresa(empresaSeleccionadaId!); await getNegociosPorEmpresa(empresaSeleccionadaId!);
} }
return true; return true;
@@ -324,7 +346,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
empresaSeleccionadaId = empresaId; empresaSeleccionadaId = empresaId;
empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId); empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId);
getNegociosPorEmpresa(empresaId); getNegociosPorEmpresa(empresaId);
notifyListeners(); _safeNotifyListeners();
} }
void resetFormData() { void resetFormData() {
@@ -332,7 +354,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
imagenFileName = null; imagenFileName = null;
logoToUpload = null; logoToUpload = null;
imagenToUpload = null; imagenToUpload = null;
notifyListeners(); _safeNotifyListeners();
} }
void buscarEmpresas(String busqueda) { void buscarEmpresas(String busqueda) {