Files
energymedia_content_manager/lib/pages/infrastructure/widgets/edit_componente_dialog.dart

1498 lines
55 KiB
Dart

import 'package:flutter/material.dart';
import 'package:nethive_neo/helpers/globals.dart';
import 'package:nethive_neo/providers/nethive/componentes_provider.dart';
import 'package:nethive_neo/theme/theme.dart';
import 'package:nethive_neo/models/nethive/componente_model.dart';
class EditComponenteDialog extends StatefulWidget {
final ComponentesProvider provider;
final Componente componente;
const EditComponenteDialog({
Key? key,
required this.provider,
required this.componente,
}) : super(key: key);
@override
State<EditComponenteDialog> createState() => _EditComponenteDialogState();
}
class _EditComponenteDialogState extends State<EditComponenteDialog>
with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nombreController;
late TextEditingController _descripcionController;
late TextEditingController _ubicacionController;
bool _isLoading = false;
late AnimationController _scaleController;
late AnimationController _slideController;
late AnimationController _fadeController;
late Animation<double> _scaleAnimation;
late Animation<Offset> _slideAnimation;
late Animation<double> _fadeAnimation;
bool _isAnimationInitialized = false;
// Variables del formulario
late int _categoriaSeleccionada;
late bool _activo;
late bool _enUso;
bool _actualizarImagen = false;
@override
void initState() {
super.initState();
_initializeControllers();
_initializeAnimations();
// Escuchar cambios del provider
widget.provider.addListener(_onProviderChanged);
}
void _initializeControllers() {
_nombreController = TextEditingController(text: widget.componente.nombre);
_descripcionController =
TextEditingController(text: widget.componente.descripcion ?? '');
_ubicacionController =
TextEditingController(text: widget.componente.ubicacion ?? '');
_categoriaSeleccionada = widget.componente.categoriaId;
_activo = widget.componente.activo;
_enUso = widget.componente.enUso;
}
void _onProviderChanged() {
if (mounted) {
setState(() {
// Forzar rebuild cuando cambie el provider
});
}
}
void _initializeAnimations() {
_scaleController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_slideController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_fadeController = AnimationController(
duration: const Duration(milliseconds: 400),
vsync: this,
);
_scaleAnimation = CurvedAnimation(
parent: _scaleController,
curve: Curves.elasticOut,
);
_slideAnimation = Tween<Offset>(
begin: const Offset(0, -1),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOutBack,
));
_fadeAnimation = CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
);
// Pequeño delay para asegurar que el widget esté completamente montado
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_isAnimationInitialized = true;
});
_startAnimations();
}
});
}
void _startAnimations() {
_fadeController.forward();
Future.delayed(const Duration(milliseconds: 100), () {
if (mounted) _scaleController.forward();
});
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) _slideController.forward();
});
}
@override
void dispose() {
widget.provider.removeListener(_onProviderChanged);
_scaleController.dispose();
_slideController.dispose();
_fadeController.dispose();
_nombreController.dispose();
_descripcionController.dispose();
_ubicacionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_isAnimationInitialized) {
return const SizedBox.shrink();
}
// Detectar el tamaño de pantalla con mejor precisión
final screenSize = MediaQuery.of(context).size;
final isDesktop = screenSize.width > 1024;
final isTablet = screenSize.width > 768 && screenSize.width <= 1024;
final isMobile = screenSize.width <= 768;
// Ajustar dimensiones según el tipo de pantalla para mejor responsividad
double maxWidth;
double maxHeight;
EdgeInsets insetPadding;
if (isMobile) {
// Configuración específica para smartphones
maxWidth = screenSize.width * 0.95; // 95% del ancho de pantalla
maxHeight = screenSize.height * 0.9; // 90% del alto de pantalla
insetPadding = const EdgeInsets.all(10);
} else if (isTablet) {
// Configuración para tablets
maxWidth = 750.0;
maxHeight = 700.0;
insetPadding = const EdgeInsets.all(20);
} else {
// Configuración para desktop
maxWidth = 1000.0;
maxHeight = 750.0;
insetPadding = const EdgeInsets.all(40);
}
return AnimatedBuilder(
animation:
Listenable.merge([_scaleAnimation, _slideAnimation, _fadeAnimation]),
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: Dialog(
backgroundColor: Colors.transparent,
insetPadding: insetPadding,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: maxWidth,
height: maxHeight,
constraints: BoxConstraints(
maxWidth: maxWidth,
maxHeight: maxHeight,
minHeight: isMobile ? 400 : (isDesktop ? 650 : 500),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(isMobile ? 20 : 30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 40,
offset: const Offset(0, 20),
spreadRadius: 8,
),
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 60,
offset: const Offset(0, 10),
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(isMobile ? 20 : 30),
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
? _buildDesktopLayout()
: _buildMobileLayout(isMobile),
),
),
),
),
),
);
},
);
}
Widget _buildDesktopLayout() {
final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada);
return Row(
children: [
// Header lateral compacto para desktop
Container(
width: 300,
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.5),
blurRadius: 25,
offset: const Offset(5, 0),
),
],
),
child: SlideTransition(
position: _slideAnimation,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 25),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Imagen del componente
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.1),
Colors.transparent,
],
),
border: Border.all(
color: Colors.white.withOpacity(0.6),
width: 3,
),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.4),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: ClipOval(
child: widget.componente.imagenUrl != null &&
widget.componente.imagenUrl!.isNotEmpty
? Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 40,
),
);
},
)
: Container(
padding: const EdgeInsets.all(20),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 40,
),
),
),
),
const SizedBox(height: 20),
// Título compacto
Text(
'Editar Componente',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 15, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Text(
'📝 ${widget.componente.nombre}',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 16),
// Info adicional
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
'Categoría: ${categoria?.nombre ?? 'N/A'}',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 12,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'ID: ${widget.componente.id.substring(0, 8)}...',
style: TextStyle(
color: Colors.white.withOpacity(0.7),
fontSize: 10,
),
textAlign: TextAlign.center,
),
],
),
),
],
),
),
),
),
// Contenido principal del formulario
Expanded(
child: Padding(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
children: [
// Formulario en columnas para aprovechar el espacio
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
// Primera fila - Nombre y Categoría
Row(
children: [
Expanded(
flex: 2,
child: _buildCompactFormField(
controller: _nombreController,
label: 'Nombre del componente',
hint: 'Ej: Switch Core Principal',
icon: Icons.devices_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildCategoriaDropdown(),
),
],
),
const SizedBox(height: 16),
// Segunda fila - Ubicación
_buildCompactFormField(
controller: _ubicacionController,
label: 'Ubicación',
hint: 'Ej: MDF Principal - Rack 1',
icon: Icons.location_on_rounded,
validator: (value) {
// La ubicación es opcional
return null;
},
),
const SizedBox(height: 16),
// Tercera fila - Descripción
_buildCompactFormField(
controller: _descripcionController,
label: 'Descripción',
hint: 'Descripción detallada del componente',
icon: Icons.description_rounded,
maxLines: 3,
validator: (value) {
// La descripción es opcional
return null;
},
),
const SizedBox(height: 20),
// Switches de estado
Row(
children: [
Expanded(
child: _buildStatusSwitch(
'Activo',
'El componente está operativo',
_activo,
(value) => setState(() => _activo = value),
Icons.power_settings_new,
Colors.green,
),
),
const SizedBox(width: 16),
Expanded(
child: _buildStatusSwitch(
'En Uso',
'El componente está siendo utilizado',
_enUso,
(value) => setState(() => _enUso = value),
Icons.trending_up,
Colors.orange,
),
),
],
),
const SizedBox(height: 20),
// Sección de imagen
_buildImageSection(),
const SizedBox(height: 25),
// Botones de acción
Row(
children: [
// Botón cancelar
Expanded(
child: Container(
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context)
.secondaryText
.withOpacity(0.4),
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: TextButton(
onPressed: _isLoading
? null
: () {
widget.provider.resetFormData();
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.close_rounded,
color: AppTheme.of(context)
.secondaryText,
size: 18,
),
const SizedBox(width: 8),
Text(
'Cancelar',
style: TextStyle(
color: AppTheme.of(context)
.secondaryText,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
const SizedBox(width: 20),
// Botón guardar cambios
Expanded(
flex: 2,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
AppTheme.of(context).tertiaryColor,
],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: ElevatedButton(
onPressed:
_isLoading ? null : _guardarCambios,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: const [
Icon(
Icons.save_rounded,
color: Colors.white,
size: 20,
),
SizedBox(width: 12),
Text(
'Guardar Cambios',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
),
),
],
),
],
),
),
),
],
),
),
),
),
],
);
}
Widget _buildMobileLayout(bool isMobile) {
final categoria = widget.provider.getCategoriaById(_categoriaSeleccionada);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header espectacular con animación
SlideTransition(
position: _slideAnimation,
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 25),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
AppTheme.of(context).tertiaryColor,
],
),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.5),
blurRadius: 25,
offset: const Offset(0, 15),
spreadRadius: 2,
),
],
),
child: Column(
children: [
// Imagen del componente
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.1),
Colors.transparent,
],
),
border: Border.all(
color: Colors.white.withOpacity(0.6),
width: 3,
),
boxShadow: [
BoxShadow(
color: Colors.white.withOpacity(0.4),
blurRadius: 20,
spreadRadius: 5,
),
],
),
child: ClipOval(
child: widget.componente.imagenUrl != null &&
widget.componente.imagenUrl!.isNotEmpty
? Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 35,
),
);
},
)
: Container(
padding: const EdgeInsets.all(20),
child: const Icon(
Icons.devices,
color: Colors.white,
size: 35,
),
),
),
),
const SizedBox(height: 16),
// Título
Text(
'Editar Componente',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
letterSpacing: 1.5,
),
),
const SizedBox(height: 8),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: Colors.white.withOpacity(0.3),
),
),
child: Text(
'📝 ${widget.componente.nombre}',
style: TextStyle(
color: Colors.white.withOpacity(0.95),
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
),
// Contenido del formulario
Flexible(
child: SingleChildScrollView(
padding: const EdgeInsets.all(25),
child: Form(
key: _formKey,
child: Column(
children: [
// Campos del formulario
_buildCompactFormField(
controller: _nombreController,
label: 'Nombre del componente',
hint: 'Ej: Switch Core Principal',
icon: Icons.devices_rounded,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'El nombre es requerido';
}
return null;
},
),
_buildCategoriaDropdown(),
_buildCompactFormField(
controller: _ubicacionController,
label: 'Ubicación',
hint: 'Ej: MDF Principal - Rack 1',
icon: Icons.location_on_rounded,
),
_buildCompactFormField(
controller: _descripcionController,
label: 'Descripción',
hint: 'Descripción detallada del componente',
icon: Icons.description_rounded,
maxLines: 3,
),
const SizedBox(height: 20),
// Switches de estado
_buildStatusSwitch(
'Activo',
'El componente está operativo',
_activo,
(value) => setState(() => _activo = value),
Icons.power_settings_new,
Colors.green,
),
const SizedBox(height: 12),
_buildStatusSwitch(
'En Uso',
'El componente está siendo utilizado',
_enUso,
(value) => setState(() => _enUso = value),
Icons.trending_up,
Colors.orange,
),
const SizedBox(height: 20),
// Sección de imagen
_buildImageSection(),
const SizedBox(height: 25),
// Botones de acción
Row(
children: [
// Botón cancelar
Expanded(
child: Container(
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
border: Border.all(
color: AppTheme.of(context)
.secondaryText
.withOpacity(0.4),
width: 2,
),
),
child: TextButton(
onPressed: _isLoading
? null
: () {
widget.provider.resetFormData();
Navigator.of(context).pop();
},
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.close_rounded,
color: AppTheme.of(context).secondaryText,
size: 18,
),
const SizedBox(width: 8),
Text(
'Cancelar',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
const SizedBox(width: 16),
// Botón guardar
Expanded(
flex: 2,
child: Container(
height: 50,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryColor,
AppTheme.of(context).secondaryColor,
AppTheme.of(context).tertiaryColor,
],
),
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: AppTheme.of(context)
.primaryColor
.withOpacity(0.5),
blurRadius: 20,
offset: const Offset(0, 8),
spreadRadius: 2,
),
],
),
child: ElevatedButton(
onPressed: _isLoading ? null : _guardarCambios,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
),
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white),
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.save_rounded,
color: Colors.white,
size: 20,
),
SizedBox(width: 10),
Text(
'Guardar',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
],
),
),
),
),
],
),
],
),
),
),
),
],
);
}
Widget _buildCompactFormField({
required TextEditingController controller,
required String label,
required String hint,
required IconData icon,
int maxLines = 1,
TextInputType? keyboardType,
String? Function(String?)? validator,
}) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: TextFormField(
controller: controller,
maxLines: maxLines,
keyboardType: keyboardType,
validator: validator,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
fontWeight: FontWeight.w500,
),
decoration: InputDecoration(
labelText: label,
hintText: hint,
prefixIcon: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: Icon(
icon,
color: Colors.white,
size: 18,
),
),
labelStyle: TextStyle(
color: AppTheme.of(context).primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
hintStyle: TextStyle(
color: AppTheme.of(context).secondaryText.withOpacity(0.7),
fontSize: 12,
),
filled: true,
fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(
color: AppTheme.of(context).primaryColor,
width: 2,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(
color: Colors.red,
width: 2,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
),
),
);
}
Widget _buildCategoriaDropdown() {
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: DropdownButtonFormField<int>(
value: _categoriaSeleccionada,
onChanged: (value) {
if (value != null) {
setState(() {
_categoriaSeleccionada = value;
});
}
},
validator: (value) {
if (value == null) {
return 'Seleccione una categoría';
}
return null;
},
decoration: InputDecoration(
labelText: 'Categoría',
prefixIcon: Container(
margin: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: const Icon(
Icons.category_rounded,
color: Colors.white,
size: 18,
),
),
labelStyle: TextStyle(
color: AppTheme.of(context).primaryColor,
fontWeight: FontWeight.w600,
fontSize: 14,
),
filled: true,
fillColor: AppTheme.of(context).secondaryBackground.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide(
color: AppTheme.of(context).primaryColor,
width: 2,
),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
),
items: widget.provider.categorias.map((categoria) {
return DropdownMenuItem<int>(
value: categoria.id,
child: Container(
child: Text(
categoria.nombre,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 14,
),
),
),
);
}).toList(),
),
);
}
Widget _buildStatusSwitch(
String title,
String subtitle,
bool value,
ValueChanged<bool> onChanged,
IconData icon,
Color color,
) {
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
color.withOpacity(0.1),
color.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: color.withOpacity(0.3),
width: 2,
),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
color: color,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: AppTheme.of(context).primaryText,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Text(
subtitle,
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
),
],
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: color,
activeTrackColor: color.withOpacity(0.3),
inactiveThumbColor: Colors.grey,
inactiveTrackColor: Colors.grey.withOpacity(0.3),
),
],
),
);
}
Widget _buildImageSection() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppTheme.of(context).primaryColor.withOpacity(0.1),
AppTheme.of(context).tertiaryColor.withOpacity(0.1),
AppTheme.of(context).secondaryColor.withOpacity(0.05),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.4),
width: 2,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header de la sección
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
gradient: AppTheme.of(context).primaryGradient,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.image_rounded,
color: Colors.white,
size: 18,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Imagen del Componente',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: AppTheme.of(context).primaryText,
),
),
Text(
'Actualizar imagen (opcional)',
style: TextStyle(
fontSize: 12,
color: AppTheme.of(context).secondaryText,
),
),
],
),
),
],
),
const SizedBox(height: 16),
// Botón para seleccionar imagen
GestureDetector(
onTap: () async {
await widget.provider.selectImagen();
setState(() {
_actualizarImagen = widget.provider.imagenToUpload != null;
});
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppTheme.of(context).primaryColor.withOpacity(0.3),
width: 2,
),
color: AppTheme.of(context).secondaryBackground,
),
child: Column(
children: [
// Preview de imagen
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
AppTheme.of(context).primaryColor.withOpacity(0.4),
width: 2,
),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: widget.provider.imagenToUpload != null
? widget.provider.getImageWidget(
widget.provider.imagenToUpload,
height: 80,
width: 80,
)
: widget.componente.imagenUrl != null &&
widget.componente.imagenUrl!.isNotEmpty
? Image.network(
"${supabaseLU.supabaseUrl}/storage/v1/object/public/nethive/componentes/${widget.componente.imagenUrl}",
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
padding: const EdgeInsets.all(20),
child: Icon(
Icons.devices,
color:
AppTheme.of(context).primaryColor,
size: 24,
),
);
},
)
: Container(
padding: const EdgeInsets.all(20),
child: Icon(
Icons.devices,
color: AppTheme.of(context).primaryColor,
size: 24,
),
),
),
),
const SizedBox(height: 12),
// Texto explicativo
Text(
widget.provider.imagenFileName ??
'Toca para seleccionar imagen',
style: TextStyle(
color: widget.provider.imagenFileName != null
? AppTheme.of(context).primaryColor
: AppTheme.of(context).secondaryText,
fontSize: 14,
fontWeight: widget.provider.imagenFileName != null
? FontWeight.w600
: FontWeight.normal,
),
textAlign: TextAlign.center,
),
if (widget.provider.imagenFileName == null)
Text(
'PNG, JPG (Max 2MB)',
style: TextStyle(
color: AppTheme.of(context).secondaryText,
fontSize: 12,
),
textAlign: TextAlign.center,
),
],
),
),
),
],
),
);
}
Future<void> _guardarCambios() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
});
try {
final success = await widget.provider.actualizarComponente(
componenteId: widget.componente.id,
negocioId: widget.componente.negocioId,
categoriaId: _categoriaSeleccionada,
nombre: _nombreController.text.trim(),
descripcion: _descripcionController.text.trim().isEmpty
? null
: _descripcionController.text.trim(),
enUso: _enUso,
activo: _activo,
ubicacion: _ubicacionController.text.trim().isEmpty
? null
: _ubicacionController.text.trim(),
actualizarImagen: _actualizarImagen,
);
if (mounted) {
if (success) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: const [
Icon(Icons.check_circle, color: Colors.white),
SizedBox(width: 12),
Text(
'Componente actualizado exitosamente',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: const [
Icon(Icons.error, color: Colors.white),
SizedBox(width: 12),
Text(
'Error al actualizar el componente',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
backgroundColor: Colors.red,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
} catch (e) {
if (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),
),
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}