Solucionado el error de eliminacion del componente
This commit is contained in:
@@ -619,20 +619,89 @@ class NegociosCardsView extends StatelessWidget {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// Cerrar el diálogo antes de la operación asíncrona
|
||||
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);
|
||||
|
||||
// Cerrar indicador de carga
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
// Mostrar resultado solo si el contexto sigue válido
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
success ? Icons.check_circle : Icons.error,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
success
|
||||
? 'Sucursal eliminada correctamente'
|
||||
: 'Error al eliminar la sucursal',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
'Eliminar',
|
||||
|
||||
@@ -514,20 +514,90 @@ class NegociosTable extends StatelessWidget {
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
// Cerrar el diálogo antes de la operación asíncrona
|
||||
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);
|
||||
|
||||
// Cerrar indicador de carga
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
// Mostrar resultado solo si el contexto sigue válido
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(
|
||||
success ? Icons.check_circle : Icons.error,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
success
|
||||
? 'Sucursal eliminada correctamente'
|
||||
: 'Error al eliminar la sucursal',
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(
|
||||
'Eliminar',
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:pluto_grid/pluto_grid.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/edit_componente_dialog.dart';
|
||||
import 'package:nethive_neo/pages/infrastructure/widgets/add_componente_dialog.dart';
|
||||
import 'package:nethive_neo/theme/theme.dart';
|
||||
|
||||
class InventarioPage extends StatefulWidget {
|
||||
@@ -18,6 +19,10 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
// GlobalKey para manejar el overlay de manera segura
|
||||
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
|
||||
OverlayEntry? _loadingOverlay;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -37,10 +42,69 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Limpiar el overlay si existe antes de dispose
|
||||
_removeLoadingOverlay();
|
||||
_animationController.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
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
@@ -143,10 +207,24 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
),
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Abrir dialog para añadir componente
|
||||
// Verificar que tengamos un negocio seleccionado
|
||||
if (componentesProvider.negocioSeleccionadoId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
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) {
|
||||
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(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
backgroundColor: AppTheme.of(context).primaryBackground,
|
||||
title: Row(
|
||||
barrierDismissible: true,
|
||||
builder: (context) => Dialog(
|
||||
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: [
|
||||
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,
|
||||
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),
|
||||
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(
|
||||
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(
|
||||
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,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
constraints: const BoxConstraints(maxHeight: 400),
|
||||
const SizedBox(height: 16),
|
||||
// Imagen del componente en móvil
|
||||
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(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildDetailRow('ID', componente.id.substring(0, 8) + '...'),
|
||||
_buildDetailRow(
|
||||
'Categoría',
|
||||
provider.getCategoriaById(componente.categoriaId)?.nombre ??
|
||||
'Sin categoría'),
|
||||
_buildDetailRow(
|
||||
'Estado', componente.activo ? 'Activo' : 'Inactivo'),
|
||||
_buildDetailRow('En Uso', componente.enUso ? 'Sí' : 'No'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatusIndicator(
|
||||
componente.activo ? 'Activo' : 'Inactivo',
|
||||
componente.activo ? Icons.check_circle : Icons.cancel,
|
||||
componente.activo ? Colors.green : Colors.red,
|
||||
),
|
||||
),
|
||||
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 &&
|
||||
componente.ubicacion!.isNotEmpty)
|
||||
_buildDetailRow('Ubicación', componente.ubicacion!),
|
||||
_buildEnhancedDetailCard(
|
||||
'Ubicación',
|
||||
componente.ubicacion!,
|
||||
Icons.location_on,
|
||||
Colors.blue,
|
||||
),
|
||||
|
||||
if (componente.descripcion != null &&
|
||||
componente.descripcion!.isNotEmpty)
|
||||
_buildDetailRow('Descripción', componente.descripcion!),
|
||||
_buildDetailRow(
|
||||
_buildEnhancedDetailCard(
|
||||
'Descripción',
|
||||
componente.descripcion!,
|
||||
Icons.description,
|
||||
Colors.purple,
|
||||
),
|
||||
|
||||
_buildEnhancedDetailCard(
|
||||
'Fecha de Registro',
|
||||
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',
|
||||
style: TextStyle(color: AppTheme.of(context).primaryColor),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusIndicator(String text, IconData icon, Color color) {
|
||||
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(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.of(context).secondaryBackground,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: AppTheme.of(context).primaryColor.withOpacity(0.2),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Text(
|
||||
label,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
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(
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: AppTheme.of(context).primaryText,
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1034,27 +1744,29 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
// Cerrar el diálogo de confirmación
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Mostrar indicador de carga
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: AppTheme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
// Capturar el ScaffoldMessenger antes de la operación asíncrona
|
||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
|
||||
try {
|
||||
// Mostrar loading de manera segura
|
||||
_showLoadingOverlay('Eliminando componente...');
|
||||
|
||||
// Realizar la eliminación
|
||||
final success =
|
||||
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) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: const [
|
||||
@@ -1071,10 +1783,11 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: const [
|
||||
@@ -1091,12 +1804,19 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Navigator.of(context).pop(); // Cerrar indicador de carga
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
// Asegurar que el overlay se remueva en caso de error
|
||||
_removeLoadingOverlay();
|
||||
|
||||
// Verificar que el widget sigue montado antes de mostrar error
|
||||
if (!mounted) return;
|
||||
|
||||
// Mostrar error usando el ScaffoldMessenger capturado
|
||||
scaffoldMessenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
@@ -1115,6 +1835,7 @@ class _InventarioPageState extends State<InventarioPage>
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
duration: const Duration(seconds: 4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
1255
lib/pages/infrastructure/widgets/add_componente_dialog.dart
Normal file
1255
lib/pages/infrastructure/widgets/add_componente_dialog.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -62,10 +61,26 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
DetalleRouterFirewall? detalleRouterFirewall;
|
||||
DetalleEquipoActivo? detalleEquipoActivo;
|
||||
|
||||
// Variable para controlar si el provider está activo
|
||||
bool _isDisposed = false;
|
||||
|
||||
ComponentesProvider() {
|
||||
getCategorias();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Método seguro para notificar listeners
|
||||
void _safeNotifyListeners() {
|
||||
if (!_isDisposed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Métodos para categorías
|
||||
Future<void> getCategorias([String? busqueda]) async {
|
||||
try {
|
||||
@@ -82,7 +97,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
.toList();
|
||||
|
||||
_buildCategoriasRows();
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
print('Error en getCategorias: ${e.toString()}');
|
||||
}
|
||||
@@ -122,7 +137,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
.toList();
|
||||
|
||||
_buildComponentesRows();
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
print('Error en getComponentesPorNegocio: ${e.toString()}');
|
||||
}
|
||||
@@ -177,7 +192,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
imagenToUpload = picker.files.single.bytes;
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
|
||||
Future<String?> uploadImagen() async {
|
||||
@@ -328,15 +343,44 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
|
||||
Future<bool> eliminarComponente(String componenteId) async {
|
||||
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
|
||||
await _eliminarDetallesComponente(componenteId);
|
||||
|
||||
// Luego eliminar el componente
|
||||
// Eliminar el componente de la base de datos
|
||||
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!);
|
||||
}
|
||||
|
||||
// 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;
|
||||
} catch (e) {
|
||||
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
|
||||
Future<void> getDetallesComponente(
|
||||
String componenteId, int categoriaId) async {
|
||||
@@ -408,7 +416,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
}
|
||||
|
||||
showDetallesEspecificos = true;
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
print('Error en getDetallesComponente: ${e.toString()}');
|
||||
}
|
||||
@@ -535,7 +543,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
detalleRouterFirewall = null;
|
||||
detalleEquipoActivo = null;
|
||||
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
|
||||
void buscarComponentes(String busqueda) {
|
||||
@@ -687,7 +695,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
// Cargar toda la información de topología para este negocio
|
||||
await cargarTopologiaCompleta(negocioId);
|
||||
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
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
|
||||
Future<void> cargarTopologiaCompleta(String negocioId) async {
|
||||
isLoadingTopologia = true;
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
|
||||
try {
|
||||
// Cargar datos en paralelo para mejor performance
|
||||
@@ -733,7 +741,7 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
];
|
||||
} finally {
|
||||
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
|
||||
List<String> validarTopologia() {
|
||||
List<String> problemas = [];
|
||||
@@ -1158,4 +1099,63 @@ class ComponentesProvider extends ChangeNotifier {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,28 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
String? empresaSeleccionadaId;
|
||||
Empresa? empresaSeleccionada;
|
||||
|
||||
// Variable para controlar si el provider está activo
|
||||
bool _isDisposed = false;
|
||||
|
||||
EmpresasNegociosProvider() {
|
||||
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
|
||||
Future<void> getEmpresas([String? busqueda]) async {
|
||||
try {
|
||||
@@ -56,7 +74,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
.toList();
|
||||
|
||||
_buildEmpresasRows();
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
print('Error en getEmpresas: ${e.toString()}');
|
||||
}
|
||||
@@ -105,7 +123,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
.toList();
|
||||
|
||||
_buildNegociosRows();
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
} catch (e) {
|
||||
print('Error en getNegociosPorEmpresa: ${e.toString()}');
|
||||
}
|
||||
@@ -164,7 +182,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
logoToUpload = picker.files.single.bytes;
|
||||
|
||||
// Notificar inmediatamente después de seleccionar
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +204,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
imagenToUpload = picker.files.single.bytes;
|
||||
|
||||
// Notificar inmediatamente después de seleccionar
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +315,10 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
// Luego eliminar la empresa
|
||||
await supabaseLU.from('empresa').delete().eq('id', empresaId);
|
||||
|
||||
// Solo actualizar si el provider sigue activo
|
||||
if (!_isDisposed) {
|
||||
await getEmpresas();
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error en eliminarEmpresa: ${e.toString()}');
|
||||
@@ -309,7 +330,8 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
try {
|
||||
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!);
|
||||
}
|
||||
return true;
|
||||
@@ -324,7 +346,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
empresaSeleccionadaId = empresaId;
|
||||
empresaSeleccionada = empresas.firstWhere((e) => e.id == empresaId);
|
||||
getNegociosPorEmpresa(empresaId);
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
|
||||
void resetFormData() {
|
||||
@@ -332,7 +354,7 @@ class EmpresasNegociosProvider extends ChangeNotifier {
|
||||
imagenFileName = null;
|
||||
logoToUpload = null;
|
||||
imagenToUpload = null;
|
||||
notifyListeners();
|
||||
_safeNotifyListeners();
|
||||
}
|
||||
|
||||
void buscarEmpresas(String busqueda) {
|
||||
|
||||
Reference in New Issue
Block a user